javaGC回收机制

在面试java后端开发的时候一般都会问到java的自动回收机制(GC)。在了解java的GC回收机制之前,我们得先了解下Java虚拟机的内存区域。

java虚拟机运行时数据区

java虚拟机在执行的过程中会将其管理的内存划分为不用的数据区域,不同的区域有不同的作用以及线程时间。

数据区划分如下:

 

下面将介绍不同区域的作用,如果已经了解可以跳过

  • 程序计数器(线程私有)

    程序计数器的作用很简单,就是记录当前线程所执行的位置(所以为线程私有),可以看成当前线程所执行的字节码的行号指示器。如果执行的是native方法,则这个计数器为空。

  • Java虚拟机栈(线程私有,生命周期与线程相同)

    虚拟机栈描述的是Java方法执行的内存模型:每个Java方法在执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息

     
  • 本地方法栈(线程共享)

    本地方法栈与虚拟机栈发挥的作用类似,不过它执行的是虚拟机使用的Native方法。

  • Java堆(线程共享)

    Java堆是Java虚拟机管理内存中最大的一块,在虚拟机启动的时候创建。此区域的唯一目的就是存放对象示例,几乎所有的对象实例都是在这分配内存的。

  • 方法区(线程共享)

    刚开始的时候,看到方法区域,第一想法就是Java中的方法,不过实际上并不是这样。方法区储存的是已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。我们可以想一想,当我们需要创建一个对象的时候,我们需要根据类的信息去创建,那么类的信息在哪?当然是在方法区!

    • 运行时常量池

      运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

垃圾收集(Garbage Collection)GC

前面说了这么多,现在我们终于可以来说说垃圾回收机制了。

首先我们得说下垃圾回收回收的是哪一部分内存区域。在前面我们知道:程序计数器,虚拟机栈,本地方法栈都是线程私有的,随着线程生或灭。这部分我们就不需要考虑了。所以我们需要考虑的就是Java堆方法区

垃圾回收的内容

回收java堆

  • 对象是否可以被回收

    判断对象是否被回收就是当一个对象死了的时候就需要进行回收。那么如何判断一个对象是否死亡,在Java中,我们使用了可达性分析算法来判断对象是否存活。

     

    当一个对象到GC Roots没有任何链(称为引用链)相连(也就是对象到GC Roots不可达)则判定对象已经死亡(如图中的Object5,Object6),可进行回收。

    可作为GC Roots的对象:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象

    在前面中,我们知道,不可达就意味着回收,可是当我们的内存很够时,有一些对象又是“食之无味弃之可惜”的时候,我们怎么办呢?在JDK1.2中,Java对引用进行扩张,分为以下引用:

    1. 强引用(Strong Reference):只要强引用还在,则不回收
    2. 软引用(Soft Reference):描述一些有用但非必须的对象,在系统将要发生内存溢出之前,将这些对象列入回收范围之中进行第二次回收。<java.lang.ref.SoftReference>
    3. 弱引用(Weak Reference):比软引用还要弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。<java.lang.ref.WeakReference>
    4. 虚引用(Phantom Reference):不会对生存时间构成影响,唯一的作用就是这个对象被回收的时候会收到一个通知。<java.lang.ref.PhantomReference>
  • 最终判断对象是否能够存活

    在可达性分析算法中,如果一个对象不可达,那么这个对象就进入到了“缓刑”阶段,真正宣告一个对象死亡还需要进行两次标记。

    1. 第一次标记进行筛选

      对不可达的对象进行第一次标记并进行筛选。筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过(意思就是finalize()方法只能被调用一次,也就是对象只能够有一次避免被回收),虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

    2. 第二次标记

      如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。

      finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

回收方法区

在Java虚拟机规范中说过不要求方法区实现垃圾收集,并且进行垃圾收集的“性价比”也较低。不过既然写了,那必定有方法区的垃圾收集,主要回收以下两部分内容:

  • 废弃常量:字面量和符号引用

  • 无用的类:

    1. 该类的所有实例都被回收,即:Java堆中不存在该类的任何实例
    2. 该类的Classloader已经被回收
    3. 该类对用的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问到该类的方法。
      当满足以上三个条件时,也未必说是一定要被回收。也仅仅是可以。

垃圾收集算法

年代划分

我们通过对象的存活周期来将JVM堆中内存空间划分为新生代和老年代。

  1. 新生代:主要是用来存放新生的对象。一般占据堆的1/3空间。

  2. 老年代:主要存放应用程序中生命周期长的内存对象。

算法

OK,说了这么多,我们现在终于可以来说说垃圾收集的算法了。

下面的图片来源于这位大佬,这位大佬讲的真滴不错。

  • 标记-清除算法(Mark-Sweep)

    标记:首先标记需要回收的对象,标记完成统一回收

    清除:就是清除对象,释放空间

     

    缺点:标记和清除的效率不高,同时产生大量不连续的内存碎片(可能不利于下次的空间分配)。

  • 标记整理法

    标记整理算法相比较于标记清除算法,标记-整理算法在清除的时候并不是一个一个的清除对象释放空间,而是一次清除全部的可回收的空间。这样使得空间变得连续,有利于对象空间的分配。

     
  • 复制算法

    1. 将内存分成两块大小相等的空间。
    2. 每次使用其中一块。
    3. 进行垃圾回收的时候,将不要的回收的对象复制到另外一个空间
    4. 完全清除原来的空间。

     

    优点:速度快,效率高,不会产生内存碎片。

    缺点:显而易见,空间浪费大,缩小了一半。

    解决方法

    IBM研究表明:新生代98%的对象是“朝生夕死”,所以我们并不需要将空间划分为1:1,而是将空间划分为Eden:Survivor:Survivor = 8:1:1。每次使用Eden和其中一块Survivor。

    1. 使用其中Eden和一块Survivor。
    2. 进行回收时,讲Eden和Survivor还存活的对象一次性的复制到另外一块Survivor上。
    3. 清理第一步中的Eden和Survivor。

    如果第二步中Survivor的空间不足,则依赖于其他内存(老年代)进行分配担保(也就是讲存活的对象放入老年代)。

     
  • 分代收集算法

    分代收集算法其实就是前面几种算法的应用。根据年代使用不同的算法

    1. 新生代GC(MinorGC,回收速度快):复制算法
    2. 老年代(Full GC/Major GC,比Minor慢10倍以上):标记整理法和标记清除法。

对象分配内存区域

  1. 新生代:大多数情况下爱,对象在新生代Eden区中分配。如果没有足够的空间,则发起一次MinorGC。
  2. 老年代:
    • 大对象直接进入老年代。比如说很长的字符串或数组。
    • 长期存活的对象:没熬过一次MinorGC,年龄age增加一岁,当它的年龄超过一定岁数时(默认15,可设置),则进入老年代中。
    • 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

参考书籍:《深入理解Java虚拟机》——周志明,这本书写的太好了,写的通熟易懂。强烈推荐去看看。

最新文章

  1. 在知乎上看到 Web Socket这篇文章讲得确实挺好,从头看到尾都非常形象生动,一口气看完,没有半点模糊,非常不错
  2. SqlMapConfig.xml
  3. JavaScript -- 小试牛刀
  4. Java中List,ArrayList、Vector,map,HashTable,HashMap区别用法
  5. Java并发之ScheduledExecutorService(schedule、scheduleAtFixedRate、scheduleWithFixedDelay)
  6. linux (RHEL) 添加和删除用户
  7. Cannot change version of project facet Dynamic web的解决方法
  8. JDBC基础一
  9. UI:基础
  10. Caused by: java.lang.NoClassDefFoundError: freemarker/cache/TemplateLoader
  11. OpenJudge 2775 文件结构“图”/ Poj 1057 FILE MAPPING
  12. 校省选赛第一场D题TwoDecks题解
  13. IOS开发网络篇之──ASIHTTPRequest详解
  14. php各种编译错误汇总
  15. JavaScript技巧&amp;写法
  16. 微信小程序后台音乐播放注意事项
  17. vue 的准备项目架构环境配置
  18. jmeter将JDBC Request查询出的数据作为下一个接口的参数
  19. 【Redis学习之六】Redis数据类型:集合和有序集合
  20. Linux中怎么通过PID号找到对应的进程名及所在目录

热门文章

  1. 使用哈希加盐法来为密码加密(补充JAVA的实现)
  2. SQL中关键字的执行顺序
  3. 算法模型的 Motivations
  4. 目标检测的图像特征提取(一)HOG特点
  5. intel dpdk IPv4 Fragmentation Sample Application 测试
  6. dedecms织梦出现“Upload filetype not allow”原因和解决方案
  7. asp .net 文件浏览功能
  8. WPF SystemParameters的使用
  9. Mysql事务,并发问题,锁机制-- 幻读、不可重复读--专题
  10. datacontract helper