垃圾回收机制
识别垃圾的算法
引用计数法
简而言之,对象被引用一次,就在它的对象头上添加一次引用次数,若对象没有被引用 (引用次数为0),则该对象可以被回收。
但是,引用计数法无法解决 循环引用 问题
可达性算法
现代虚拟机基本采用该方法判断对象是否存活。该算法原理是 以一系列叫作 GC Root 的对象作为起点,引出它们指向的下一个节点,再以下个节点为起点,引出此节点的下一个节点 (通过 GC Root 串成的一条线就叫应用链),直到所有的节点都遍历完毕。若相关对象不在任意一个以 GC Root为起点的引用链中,则这些对象会被判断为「垃圾」,会被 GC 回收。
可作为 GC Root 的对象类别: - 虚拟机栈中引用的对象 (局部变量) - 方法区中类静态属性引用的对象 (静态变量) - 方法区中常量引用的对象 (常量 static final) - 本地方法栈中 JNI 引用的对象 (指向本地方法的对象) - JVM内部的引用,如基本数据类型对应的 Class 对象 - 所有被同步锁(synchronized关键字)持有的对象 - 反应JVM内部情况的 JMXBean、本地代码缓存
垃圾收集算法
进行垃圾回收的算法,主要有以下几种: - 标记清除算法 - 复制算法 - 标记整理算法 - 分代收集算法
- 标记-清除算法
分为 标记 和 清除 两个阶段
- 先根据可达性算法 标记 出相应的可回收对象
-
回收可回收对象的内存空间,但是会产生 内存碎片
-
复制算法
把堆分成两块区域 A 和 B,区域A负责分配对象,区域B部分配,对区域A使用上述 标记法 把存活的对象标记出来,然后把区域A中存活的对象都复制到区域B中(存活对象都一次紧邻排列),最后把A区对象全部清理掉释放空间,解决了内存碎片问题。
缺点是可用内存空间减少一半,且每次回收都需要移动存活对象,效率低下。
- 标记-整理算法
前两步与标记-清除法一样,不同的是添加了一个整理的过程,即所有的存活对象都向一端以供,再清理掉另一端的所有区域。
缺点是每次垃圾清除都要频繁移动存活对象,效率低
- 分代收集算法
整合上述算法的有点,尽量避免其缺点,是现代虚拟机采用的首选算法,类似将上述收集算法整合
根据对象存活周期的不同,将堆分为新生代和老年代,默认比例为 1:2,新生代又分为 Eden区,from Survivor区(简称S0),to Survivor区(简称S1),三者的比例为 8:1:1,这样就可以根据新老生代的特点选择最合适的垃圾回收算法,把新生代发生的 GC 成为 Young GC (也叫Minor GC),老年代发生的 GC 成为 Old GC。
分代收集工作原理
- 对象在新生代的分配与回收
大部分对象会在很短的时间内被回收,对象一般分配在 Eden 区,当 Eden 区将满时,触发 Minor GC。将大部分对象回收(接近98%),只留下少量存活对象,将其移动到 S0 或 S1,同时对象年龄加一,最后将 Eden区对象全部清理以释放空间。
当触发下一次 Minor GC时,会把 Eden 区 和 S0中的存活对象一起移动到 S1中,同时清空Eden和S0的空间。
若再触发下一次 Minor GC,则重复上一步,只不过此时变成了 从 Eden,S1区将存活对象复制到 S0 区,每次垃圾回收,S0,S1角色互换。在Eden区的垃圾回收采用的是 复制算法,因为Eden区分配的对象大部分在 Minor GC后都消亡了,只剩下极少部分存活对象,S0,S1区域也比较小,所以最大限度地降低了复制算法造成的对象频繁拷贝带来的开销。
-
对象何时晋升老年代
-
当对象的年龄达到了设定的阈值,则会从 S0(或S1)晋升到老年代,默认阈值为15,即当对象年龄达到15时,会晋升到老年代
-
大对象,当某个对象分配需要大量的连续内存时,此时对象的创建不回分配在 Eden区,而会直接分配在老年代,防止大对象移动的巨大开销,典型的大对象有 很长的字符串和数组
-
空间分配担保
在 Minor GC执行之前,JVM会检查老年代最大可用连续空间是否大于新生代所有对象的总空间,大于则安全进行Minor GC,否则会查看虚拟机设置HandlePromotionFailure是否允许担保失败。若允许,则会继续进行空间检查,否则可能进行一次 Full GC。
- Stop The World
若老年代满了,会触发 Full GC,会同时回收新生代和老年代,即对整个堆进行GC,会造成 Stop The World(简称STW),此时只有垃圾回收器线程在工作,其他工作线程会被挂起