JVM系列3-垃圾回收算法
上篇文章中我们了解了Java内存模型,并提到了垃圾回收,那怎么确定一个对象是垃圾,又是如何回收这些垃圾的呢?
如何确定一个对象是垃圾
要想进行垃圾回收,得先知道什么样的对象是垃圾。有两种方式
-
引用计数法
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。
弊端:如果AB相互持有引用,导致永远不能被回收。 -
可达性分析
通过GC Root的对象,开始向下寻找,看某个对象是否可达
那哪些对象可以作为GC ROOT?
类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法、栈的变量等。
垃圾回收算法
已经能够确定一个对象为垃圾之后,接下来要考虑的就是回收,怎么回收呢?
得要有对应的算法,下面聊聊常见的垃圾回收算法。
标记-清除(Mark-Sweep)
-
标记:根据GC ROOT标记对象,分为可达对象、没有引用的对象、未使用的空间
-
清除: 清除掉被标记需要回收的对象(没有引用的对象 ),释放出对应的内存空间
问题:
- 标记和清除两个过程都比较耗时,堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,效率不高
- 会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
复制算法(Copying)
将内存划分为两块相等的区域,每次只使用其中一块。当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
优势: 空间连续
问题: 空间浪费
标记-整理(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
既然上面介绍了3中垃圾收集算法,那么在堆内存中到底用哪一个呢?
- Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
- Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)
垃圾收集器
收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
新生代:适用于少量对象存活的场景, 所有采用复制算法。有3种垃圾收集器
- Serial : 适合单核CPU机器 单线程处理垃圾回收,会STW(stop the wrold)
- ParNew: 适合多核CPU机器 并行处理垃圾回收
- Paraller Scavenge: 类似ParNew 但是更关注吞吐量(业务执行时间/业务执行时间 + 垃圾回收时间)
老年代:采用标记清除 / 标记整理算法
- Serial Old
- Paraller Old
- CMS (Concurrent mark sweep ):并发类的垃圾收集器 ,关注停顿时间(业务线程和垃圾回收线程可以一起执行)
G1垃圾收集器横跨新生代和老年代,G1物理上不将内存分为新生代、老年代、S0、S1、Eden,而是划分为一个个区(region), G1收集器可以设置停顿时间,在这个时间内将尽可能收集多的垃圾
分类
-
串行收集器->Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。 适用于内存比较小的嵌入式设备 。 -
并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 适用于科学计算、后台处理等若交互场 景 。 -
并发收集器[停顿时间优先]->CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。 适用于相对时间有要求的场景,比如Web
吞吐量和停顿时间
-
停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间
-
吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任 务。
如何选择合适的垃圾收集器
官网 :https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
- 如果允许停顿时间超过1秒,选择并行或JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
G1收集器
JDK 7开始使用,JDK 8非常成熟,JDK 9默认的垃圾收集器,适用于新老生代。
判断是否需要使用G1收集器?
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间比较长
如何开启需要的垃圾收集器
设置JVM参数
- 串行
-XX:+UseSerialGC
-XX:+UseSerialOldGC
- 并行(吞吐量优先):
-XX:+UseParallelGC
-XX:+UseParallelOldGC
- 并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGC
-XX:+UseG1G