对象的生命周期和可回收性判断
对象年龄分代
GC对于不同年龄的对象有不同的回收规则,其中对象根据根据其内部文件头的年龄计数器大体上被分为三类
内存空间 | 分代 | |||||
---|---|---|---|---|---|---|
堆 | 新生代(YoungGen)↓ | |||||
Eden区 | Survivor 0区 | Survivor 1区 | ||||
老年代(OldGen) | ||||||
方法区 | 永久代(PermGen) | 元空间(MetaSpace) |
分代规则
根据GC回收区域的不同,GC操作大体上可以分为两大类:
- Paritial GC/Minor GC:针对堆局部区域的垃圾回收
- Young GC:只对新生代进行GC的操作,由于新生代的对象达到年龄阈值后会变为OldGen,这会导致老年代内存占用提高
- Old GC:只对老年代进行GC的操作
- Mixed GC:对新生代和部分老年代进行GC的操作
- Full GC/Major GC:针对所有代(包括新生代、老年代和永久代)进行GC的操作,收集整个GC堆
- 如果YoungGen的平均空间大小大于OldGen的剩余空间,或者PermGen的剩余空间不够,则会触发一次Full GC,
- 也可以通过手动调用
System.gc()
触发一次Full GC
为了提高GC效率,对于对象的内存分配和回收遵循以下规则
新对象分配在Eden区
大部分对象默认分配在堆的Eden区中,当Eden区空间不足时,将会触发一次Minor GC
大对象直接进入老年代
对于内存消耗较高的对象(如数组、String等)会直接进入老年代,防止分代晋升造成多余的内存复制
长期存活对象进入老年代
大部分对象在创建初分配在Eden区中,在经过一次Minor GC仍然存活后,被移动到Survivor区(0区或1区)中,并开始年龄计数,初始年龄为1,每经过一次Minor GC后仍存活则年龄+1,在达到一个年龄阈值后进入老年代
年龄阈值一般是动态计算的,并遵循以下规则:
- VM参数中一般存在2个关于年龄阈值的参数:
-XX:MaxTenuringThreshold
,用于设置最大的年龄阈值,默认为15-XX:TargetSurvivorRatio
,用于设置一个百分比,默认为50%
- 当新生代的某个年龄的空间总大小超过Survivor区的上述百分比空间(即50%)时,则取这个年龄(小于等于上述的最大值)为新的年龄阈值
空间分配担保机制
- VM参数中存在1个关于分配担保的参数:
-XX:HandlePromotionFailure
,用于设置是否允许担保失败,在JDK 6后默认不允许担保失败
当进行Minor GC时,虚拟机会检查老年代的剩余连续空间是否大于新生代所有的对象空间或者历次晋升的平均大小,若是则则担保成功,反之则担保失败
- 如果VM不允许担保失败,则此时改为进行Full GC
- 如果VM允许担保失败,则此时冒险进行一次Minor GC
死亡对象判断
判断对象死亡主要有两种方式:引用计数法和可达性分析算法
引用计数法
给每个对象增加一个引用计数器,每当该对象被引用时计数器++,引用失效时则计数器–,计数器为0的对象即为死亡对象
大部分虚拟机并不采用这个算法,因为这个算法无法解决循环引用的问题
可达性分析算法
可达性分析会暂停线程,GC会从一系列GC Roots节点往下搜索
- GC Roots是当前绝对不能被回收的对象,并根据以下规则判断
- 当前虚拟机栈和本地方法栈栈顶栈帧引用的对象(即当前执行方法调用的对象)
- 常量引用对象和静态引用对象
- 被同步锁持有的对象
对于两个对象,如果有直接或者间接的引用关系,则引用对象是被引用对象的父节点,GC从Roots往下搜索,那些根节点非Roots的树中的对象即为死亡对象
对于早期的可达性分析算法采用一次性的标记-清除,但是因为会导致STW,性能低下,现在CMS GC和G1 GC更多采用三色标记算法:
三色标记将对象分为三类:
- Black:已经被扫描的过的对象,且该对象调用的其他对象也被扫描完毕
- Grey:已经被扫描过的对象,但该对象调用的其他对象还没被扫描完毕
- White:未被扫描过的对象
三色标记算法主要分为四个步骤:
- 初始标记:将GC Root直接调用过的所有节点全部标记为灰色,这部分会导致STW
- 并发标记:从灰色节点开始搜索他们调用的节点,并将其标记为黑色,这部分不会导致STW
- 重新标记:矫正那些并发标记中的错误,需要STW
- 并发清楚:将确定为White的节点清除掉,不需要STW
对于常量和Class对象的判断
对于字符串常量池,如果没有String引用再引用该字符串的话,则该字符串对象是可以被回收的
对于Class对象的可回收性判断比较严苛,需要同时满足以下条件:
- 堆中不存在该Class对象的实例
- 该Class的Classloader被回收
- 该Class对象没有被任何地方通过反射被引用
引用判断
不论是引用计数法还是可达性分析,都需要判断对象间的引用关系,其中引用关系主要分为四类
强引用
1 | String a="abc"; |
引用类型直接指向堆中的一个对象即为强引用,在这种情况下该对象绝对不会被回收
软引用
1 | String a="abc"; |
在调用SoftReference后可以将强引用转换为软引用
在内存空间足够的时候,GC不会回收这些软引用对象,反之则会回收这些对象的内存,通常被用来实现内存敏感的高速缓存
弱引用
1 | String a="abc"; |
在调用WeakReference后可以将强引用转换为软引用
只要GC执行了垃圾回收,不论内存空间是否足够都会收集该对象的内存
虚引用
1 | String a="abc"; |
虚引用对于GC而言实际上相当于没有引用,如果一个对象只有虚引用,那就相当于没有被引用
虚引用主要用来跟踪对象是否被垃圾回收,必须和引用队列结合使用,对虚引用调用get方法只会得到null值
引用队列
无论是软引用、弱引用还是虚引用,如果程序中实例化了ReferenceQueue
,在这三种引用被回收后都会加入引用队列中,用以判断该引用是否被回收