GC 回收机制和回收策略

330 阅读5分钟

什么是垃圾

所谓垃圾就是内存中已经没有用的对象。

既然是”垃圾回收",那就必须知道哪些对象是垃圾。Java 虚拟机中使用一种叫作可达性分析的算法来决定对象是否可以被回收。

可达性分析

image.png

特别注意:

上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。

GC Root 对象

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  5. 所有被同步锁(synchronized关键字)持有的对象。
  6. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  7. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

什么时间回收

  • Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
  • System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。
  • 特别注意:

    执行System.gc()函数的作用只是提醒或告诉虚拟机,希望进行一次垃圾回收 至于什么时候进行回收还是取决于虚拟机,而且也不能保证一定进行回收。

    代码验证GCRoot

    1. 验证虚拟机栈(栈帧中的局部变量)中引用的对象作为 GC Root。

    todo

    如何垃圾回收

    标记清除算法

    从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步。

    image.png

    优点:实现简单,不需要将对象进行移动。

    缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。

    复制算法

    将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

    image.png

    image.png

    优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

    缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

    标记-压缩算法

    需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:

    image.png

    优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

    缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。

    JVM分代回收策略

    年轻代

    新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。

    新生代又可以继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。

    image.png

    当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1是空的。如图所示:

    image.png

    下一次 Eden 区满时,再执行一次垃圾回收。此次会将 EdenS0区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0变为空。如图所示:

    如此反复在 S0S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。如图所示:

    老年代

    一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

    老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。

    参考:

    1. Java虚拟机究竟是如何处理SoftReference的