一、如何判断对象是否为垃圾
1、对象被判定为垃圾的标准
- 没有被其他对象引用
2、判断对象的引用数量来决定是否是垃圾
判定标准:
- 通过判断对象的引用数两来决定对象是否可以被回收;
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1;
- 任何引用计数为0的对象实例可以当作垃圾
优点:
- 执行效率高,程序执行受影响小
缺点:
- 无法检测出循环引用的情况,如父对象引用子对象,子对象反过来引用父对象,这样这两个对象永远不可能为0,导致内存泄漏。
3、可达性分析算法来决定对象是否为垃圾
\quad 通过判断对象的引用链是否可达来决定对象是否可以被回收。
- 从GC Root开始查找所有可达的对象,并标记这些对象可达,表示后续还会用到;未被标记为可达的对象会被认为是垃圾
可以当作GC Root的对象:
- 虚拟机栈中引用的对象
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程的引用对象
二、垃圾回收算法
1、标记-清除算法(Mark and Sweep)
- 标记:从根集合进行扫描,对存活对象进行标记
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存
-
缺点:
标记-清除算法不需要对垃圾对象进行移动,因此清除垃圾对象后会产生大量不连续的内存碎片,空间碎片太多会导致以后遇到需要分配较大内存的对象时无法找到足够的内存以至于触发又一次垃圾回收工作。
2、复制算法(Copying)
- 将可用内存按容量和比例划分为两块或多个块,并选择其中一块或者两块作为对象面,其他作为空闲面
- 对象再对象面上创建
- 当对象面对应的块的内存用完时,将存活的对象从对象面复制到空闲面
- 将对象面所有对象内存清除
优点:
- 解决了碎片化的问题
- 顺序分配内存,简单高效
- 适用于对象存活率低的场景
缺点:
- 在对象存活率高的情况下,每次需要复制大量对象到空闲面,导致效率较低
3、标记-整理算法
- 标记:从根集合进行扫描,对存活对象进行标记
- 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收
优点:
- 避免了内存的不连续性
- 不用设置两块内存互换
- 使用与存活率高的场景
4、分代收集算法(Generational Collector)
老年代和年轻代:
- 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old)(Java8以前还有永久代)。新生代 ( Young ) 又被划分为三个区域:Eden、S0、S1。 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收
- 年轻代用来存放新近创建的对象,尺寸随堆大小的增大和减小而相应的变化。年轻代的特点是产生大量的死亡对象,并且要是产生连续可用的空间, 所以使用复制清除算法和并行收集器进行垃圾回收。对年轻代的垃圾回收称作初级回收 (minor gc)
- Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。 另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作
分代收集算法核心:
- 垃圾回收算法的组合拳
- 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
- 目的:提高JVM运行效率
- 年轻代对象存活率低,使用复制算法
- 老年代对象存活率高,使用标记-整理算法
对象如何晋升到老年代:
- 经历一定Minor次数依然存活的对象(默认15岁,每次经历+1岁)
- survivor区中存放不下的对象
- 新生成的大对象直接去老年代
触发Full GC的条件:
- 老年代空间不足
- 永久代空间不足(JDK7以下)
- Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- 在程序里面调用System.gc()
- 使用RMI来进行RPC或管理的JDK应用,每小时执行1次full GC
Stop-the-World:
- JVM由于要执行GC而停止了应用程序的执行
- 任何一种GC算法中都会发生
- 多数GC优化通过减少Stop-the-World发生的时间来提升程序性能,从而使程序具有高吞吐,低停顿的特点
SafePoint:
- 进行对象可达性分析时会暂停活动,使得对象引用关系不会改变
三、常见的垃圾收集器
JVM的运行模式:
- Server
- Client
1、年轻代垃圾收集器
Serial收集器(复制算法):
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器
ParNew收集器(复制算法):
- 多线程收集,其余行为、特点和Serial收集器一样
- 单核执行下不如Serial,在多核下执行才有优势
Parallel Scavenge收集器(复制算法):
- 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)
- 比起关注用户线程的停顿时间,更关注系统的吞吐量
- 在多核下执行才有优势,Server模式下默认的年轻代收集器
2、老年代垃圾收集器
Serial Old收集器(标记-整理算法):
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(标记-整理算法):
- 多线程,吞吐量优先
CMS收集器(标记-整理算法):
- 初始标记:Stop-the-World
- 并发标记:并发追溯标记,程序不会停顿
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
- 有内存碎片化的问题
Garbage First(G1)收集器(复制+标记-整理算法):
- 并发和并行
- 分代收集
- 空间整合
- 可预测的停顿
- 将整个Java堆内存划分为多个大小相等的Region
- 年轻代和老年代不再物理隔离
各个收集器之间的关系,两者之间连线表示二者可兼容。