Java GC机制
JVM结构图
理解GC的意义
- 什么是GC?
垃圾回收是指将分配给对象但是不再使用的内存回收或释放的过程,如果一个对象没有指向它的引用或者其赋值为null,则此对象适合进行垃圾回收,简单来说就是为了保证我们程序员开发的项目内存不溢出,让有限的内存空间存放我们需要的对象,将不再使用的对象从内存中释放出来,这就是GC的意义 - 需要GC的内存区域
JVM中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现内存的自动清理,因此,我们的内存垃圾回收主要集中于Java堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。 - GC的对象
需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活常用的有两种方法:引用计数法和可达性分析。
(1)引用计数:每个对象有一个引用计数属性,新增一个引用计数时加1,引用释放时计数减1,计数为0时可以会输。此方法简单,无法解决对象相互循环引用的问题。
(2)可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
在Java语言中,GC Roots包括:
虚拟机栈中引用的对象。
方法区中类静态属性实体引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(本地方法接口)引用的对象。 - 什么时候触发GC
(1)程序调用System.gc时可以触发
(2)系统自身来决定GC触发的时机(根据Eden和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)
GC又分为轻GC和重GC
轻GC触发条件:当Eden区满时,触发轻GC
重GC触发条件:
a.调用System.gc时,系统建议执行重GC,但是不必然执行
b.老年代空间不足
c.方法区空间不足
d.通过轻GC进入老年代的平均大小大于老年代的可用内存
e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代可用内存小于该对象大小
堆内存图示:
GC垃圾回收主要在伊甸园区和养老区,假设内存满了则会报堆内存不够的异常,也就是OOM
Java.lang.OutOfMemoryError:Java heap space
在JDK8以后,永久存储区改了个名字(元空间)
新生区
1.类诞生和成长的地方,甚至死亡
2.伊甸园区,所有的对象都是在伊甸园区new出来的
3.幸存区(0,1)也就是我们常说的From Space 和To Space,哪边内存为空哪边则是To Space
每次GC都会将Eden活的对象移到幸存区中,一旦Eden区被GC后就会是空的
-xx:-xx:MaxTenuringThreshold=15,通过这个参数可以设定进入老年代的时间。
永久区:
这个区域是常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭VM虚拟机就会释放这个区域的内存(注意,JDK1.8以后无永久代,常量池在元空间)
上面提到的元空间实际上并不是替代了永久代,他逻辑上是存在的但是物理上并不存在,因为它与堆共享着内存,元空间也叫“非堆” - GC常用算法
GC常用的算法有:标记-清除算法,标记-压缩算法,复制算法和分代收集算法。目前主流的JVM(HotSpot)采用的是分代收集算法
1.标记-清除算法
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段堆死亡的对象进行清除执行GC操作,图示如下:
优点
最大的优点是,标记-清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重要的是,这个办法并不需要移动对象的位置,不需要额外的空间。
缺点
它的缺点是效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高,严重浪费时间(两次扫描)。没有移动对象,导致可能出现很多碎片空间无法利用的情况
2.标记-压缩算法(标记-整理)
标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。
图示可以说是接着上面的标记清除之后的:
优点
该算法不会像标记-清除算法那样产生大量的碎片空间
缺点
如果存活的对象过多,整理阶段将会执行较多的复制操作,导致算法效率降低(多了一个移动的成本)
3.复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个区域内。
优点
实现简单,不产生内存碎片
缺点
每次运行,总有一半内存是空的,导致可以使用的内存空间只有原来的一半,因此最好用于对象存活率比较低的时候也就是新生区
4.分代收集算法
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存周期短,每次回收都会有大量的对象死去,那么这时候就应该采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理或者标记-清除算法
垃圾收集器
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
1. Serial收集器
串行收集器是最古老,最稳定以及效率高的收集器
可能会产生较长的停顿,只使用一个线程区回收
-XX:+UseSerialGC
- 新生代、老年代使用串行回收
- 新生代复制算法
- 老年代标记-压缩
图示:
2. 并行收集器
2.1 PartNew
-XX:+UseParNewGC(new代表新生代,所以适用于新生代) - 新生代并行
- 老年代串行
Serial收集器新生代的并行版本
在新生代回收时使用复制算法
多线程,需要多核支持
-XX:ParallelGCThreads限制线程数量
2.2 Parallel收集器
类似PartNew
新生代复制算法
老年代标记-压缩
更加关注吞吐量
-XX:UseParallelGC - 使用Parallel收集器+老年代串行
-XX:UseParallelOldGC - 使用Parallel收集器+老年代并行
2.3 其他GC参数
-XX:MaxGCPauseMills - 最大停顿时间,单位毫秒
- GC尽力保证回收时间不超过设定值
-XX:GCTimeRatio - 0-100的取值范围
- 垃圾收集时间占总时间的比
- 默认99.即最大允许1%时间做GC
这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优
3. CMS收集器 - Concurrent Mark Sweep并发标记清除(应用程序和GC线程交替执行)
- 使用标记-清除算法
- 并发阶段会降低吞吐量(停顿时间减少,吞吐量降低)
- 老年代收集器(新生代使用ParNew)
- -XX:+UseConcMarkSweepGC
CMS运行过程比较复杂,着重实现了标记的过程,可分为
1.初始标记(会产生全局停顿)- 根可以直接关联到的对象
- 速度快
2.并发标记(和用户线程一起) - 主要标记过程,标记全部对象
3.重新标记(会产生全局停顿) - 由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
4.并发清除(和用户线程一起) - 基于标记结果,直接清理对象
这里就能很明显的看出,为什么CMS要使用标记清除而不是标记压缩,如果使用标记压缩,需要多对象的内存位置进行改变,这样程序就很难继续执行。但是标记清除会产生大量内存碎片,不利于分配内存。
CMS收集器特点:
尽可能降低停顿
会影响系统整体吞吐量和性能(比如,在用户线程运行过程中,分一半CPU区做GC,系统性能在GC阶段,反应速度就下降一半)
清理不彻底(因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理)
因为和用户线程一起运行,不能在空间快满时再清理(因为也许在并发GC的期间,用户线程又申请了大量内存,导致内存不够) - -XX:CMSlnitiatingOccupancyFraction设置触发GC的阈值
- 如果不幸内存预留空间不够,就会引起concurrent mode failure
一旦concurrent mode failure产生,将使用串行收集器作为后备。
CMS也提供了整理碎片的参数:
-XX:+ UseCMSCompactAFullCollection Full GC后,进行一次清理 - 整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction - 设置几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads - 设定CMS的线程数量(一般情况下约等于可用CPU数量)
CMS的提出是想改善GC的停顿时间,在GC过程中的确做到了减少GC时间,但是同样导致产生大量内存碎片,又需要消耗大量时间区整理碎片 ,从本质上并没有改善时间。
4. G1收集器
G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。
与CMS收集器相比的话G1收集器有以下优点:
(1)空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
(2)可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但是G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们是一部分(可以不连续)Region的集合
G1的新生代收集和PartNew类似,当新生代占用达到一定比例的时候,开始触发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿
步骤:
(1)标记阶段,首先初始标记,这个阶段是停顿的,并且会触发一次普通轻GC。对应GC log:GC pause
(2)Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在轻GC之前完成
(3)Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被轻GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那么这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
(4)Remark,再标记,会有短暂停顿(STW)。再标记阶段是用来收集并发标记阶段产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法
(5)Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。
(6)复制/清除过程后。回收区域的活性对象已经被集中回收。
finalize()方法详解
1.finalize的作用
(1)finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC再回收对象之前调用该方法。
(2)finalize()与C++中的析构函数不是对应的。Java中的finalize的调用具有不确定性
(3)不建议用finalize()方法完成“非内存资源‘的清理工作,但是建议用于:
- 清理本地对象(通过JNI创建的对象);
- 作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize()方法中显式调用其他资源释放方法。
2 finalize的问题
(1)一些与finalize相关的方法,由于一些致命的缺陷,已经被废弃了,例如System.runFinalizersOnExit()方法等
(2)System.gc()与System.runFinalization()方法增加了finalize()方法执行的机会,但是不可盲目依赖他们
(3)Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行
(4)finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行
(5)对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,但是并不影响GC对finalize的行为。