Java虚拟机专题之垃圾回收(读书笔记)
一 如何判断对象是垃圾对象
1.1 引用计数法 (Reference Counting)
在对象中添加一个引用计数器,当有其他地方引用这个对象的时候,引用计数器就加1,当引用失效的时候就-1. 当垃圾回收器检查到引用为0,就会认为是垃圾对象,进行回收。
但是有一个问题,比如对象之间循环引用,诸如A,B两个对象,都有一个属性instance, 假设A.instance = B,B.instance=A。他们互相引用着对方,导致对方引用计数器都不为0。
所以JVM一般都不使用这种方法,因为难以解决对象之间相互循环引用问题。
1.2 可达性分析法 (ReachabilityAnalysis)
通过一系列GC Roots对象作为起点,这些节点开始向下搜索,搜索走过的路径叫做引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连接,则证明此对象是不可用的。
对于可达性分析,我们知道GC Roots是很重要的,那么有哪些对象可以作为GC Roots呢?
# 虚拟机栈中引用的对象
# 方法区中静态类属性引用的对象
# 方法区中常量池引用的对象
# 本地方法栈中JNI引用的对象
如下面2图所示:
第一幅图中,除了object4 没有到任何GC Roots的引用链相连接,其余的都有,所以可以认为object4 是不可用的
第二幅图中因为object1到GC Roots的引用链没了,所以object1下面的引用链上对象都不可用,因为都没引用链连接到对应的GC Roots
二 垃圾回收算法(回收策略)
2.1 标记-清除算法(Mark-Sweep)
标记出需要回收的对象,如何标记呢,就是通过可达性分析法,然后有一个清除线程来将标记的对象清除掉。
# 效率问题:标记清除效率都不高
# 空间问题: 内存碎片问题
标记清除之后,会产生大量不连续的没存碎片,空间碎片太多可能会导致以后程序要分配较大的对象时,无法找到足够的连续内存,而不得不提前触发一次新的垃圾收集动作。
2.2 复制算法(Copying)
虚拟机将内存容量划分为大小相等的2块,每一次只使用其中一块。当其中一块用完了,就将存活的对象复制到另一块上去,然后已使用这块内存清理。
优点:
效率高,解决了内存碎片问题
缺点:
浪费内存;对象存活率较高的时候,会进行较多的复制操作,效率将变得低下
所以其特点也决定,只适合在新生代区域使用,不适合老年代,因为老年代的对象生命周期都比较长,所以如果出现极端情况,100%的存活率,就容易发生Full GC
2.3 标记-整理算法(Mark-Compact)
我们知道,老年代不适合使用复制算法,所以根据老年代的特点,提出了标记整理算法。
# 先进行标记
# 标记完了之后,不是直接进行清理,而是让所有存活的对象都往一段移动
# 移动完了之后,直接从边界处清理掉不可用对象
优点
解决了标记-清除算法中的碎片问题;也解决了复制算法中内存浪费问题
缺点:效率不高
所以不适合频繁进行GC的场景,而老年代GC的发生次数比较少,所以适合老年代
2.4 分代收集算法(GenerationalCollection)
根据对象对象不同的存活周期,将堆内存划分成新生代和老年代。
针对不同的年龄代使用不同的垃圾收集算法。
比如新生代,经常有大量对象死去,只有少量存活可以使用复制算法,而老年代因为存活率较高,所以一般使用标记清理或者标记整理算法。
三 垃圾收集器
3.1 Serial收集器
JDK1.3 之前使用的垃圾收集器,它是单线程的。但是这个单线程必须暂停所有的工作线程,然后再进行垃圾回收,直到它收集结束。
很明显,这种垃圾回收方式是令人难以接受的。
开启方式:-XX:+UseSerialGC
3.2 ParNew收集器
ParNew除了是多线程进行垃圾回收以外,其余的特点和Serial几乎是一样的。可以认为是Serial的多线程版本。
Serial 和 ParNew比较
相同点:
# 暂停所有工作线程,然后进行垃圾回收
# 采用复制算法,对新生代进行垃圾回收
不同点:
# Serial是单线程;ParNew是多线程开启方式:-XX:+UseParNewGC
3.3 ParallelScavenge 收集器
Parallel也是一个新生代采用复制算法的收集器,而且也是多线程收集器,感觉和ParNew差不多。
ParNew 和 Parallel 收集器比较
相同点:
# 采用复制算法,对新生代进行垃圾回收
# 使用多线程进行垃圾回收
不同点:
关注点不一样,CMS,ParNew等收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间;而Parallel 收集器目标是达到一个可控制的吞吐量。
什么是吞吐量?
吞吐量 = (运行用户代码时间) / (运行用户代码时间 + 垃圾收集时间)
如果虚拟机总共运行了100分钟,而垃圾收集花了1分钟,那么吞吐量就是99%。
Parallel提供了2个参数用于精确控制吞吐量
-XX:MaxGCPauseMills: 垃圾收集器最大停顿时间(毫秒)
-XX:GCTimeRatio: 吞吐量大小(值应该大于0且小于100)
开启Parallel和Parallel Old收集器的方式:
-XX:+UseParallelGC -XX:+UseParallelOldGC
3.4CMS收集器
CMS是Concurrent Mark Sweep,即并发标记删除,而不是内容管理系统的缩写。在这个过程中,部分阶段所有线程暂停,部分阶段会和应用线程一起并发执行
工作过程
# 初始标记 (CMS initial mark)
会暂停整个虚拟机,然后标记所有的跟对象(GCRoots),但是GC Roots一般都很少,所以这个过程很快。
# 并发标记 (CMS concurrent mark )
虚拟机会从根节点开始,将所有引用到的对象都打上标记,而且这个阶段是和应用线程一起并发执行
# 重新标记 (CMS remark)
会暂停整个虚拟机。由于之前是并发执行,那么一边标记,一边可能会有一些新的更新,比如之前是有引用的对象,现在没有引用了,所以需要重新标记
# 并发清除 (CMS concurrent sweep)
将没有标记的对象作为垃圾回收掉,这个阶段也是和应用线程一起工作的。
优点
并发收集,低停顿
缺点
# 占用的CPU资源更多
# 无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Model Failure
# 由于是基于标记清除算法,所以存在内存碎片问题
为什么存在无法处理浮动垃圾(FloatingGarbage),可能出现ConcurrentModel Failure而导致另一次的Full GC产生的原因分析?
浮动垃圾:CMS并发清理阶段,用户线程还在继续运行,就有垃圾不断产生,由于出现在了重新标记之后,CMS无法再次收集处理掉,只能留到下一次GC时再来清理。这就是浮动垃圾。
垃圾收集阶段,用户线程还要继续运行,那也就需要预留有足够的内存 空间给用户线程使用,因此CMS不像其他的收集器,等待老年代都填满了才再进行收集。
如果老年代不是增长的很快,那么可以适当提高参数:
-XX:CMSInitiatingOccupancyFractiont,以提升触发的百分比
JDK1.6 中,CMS收集器启动阀值时92%,要是CMS运行期间预留的内存无法满足程序需要就会出现Concurrent Model Failure失败,这时候虚拟机启动预备方案:临时启用Serial Old对老年代进行收集,这样停顿时间就长了。
所以说参数-XX:CMSInitiatingOccupancyFraction设置太高容易导致Concurrent Model Failure失败,性能反而降低了。