Java垃圾回收概况
Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代 码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对 JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,放置出现内存泄露和溢出问题。
Java GC机制主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执行GC。
学习Java GC机制,可以帮助我们在日常工作中排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。
GC的阶段
对每个对象而言,垃圾回收分为两个阶段:finalization和reclamation。
finalization: 指运行这个对象的finalize的方法。
reclamation: 回收被这个对象使用的内存。
GC的过程的基本步骤
首先确认对象是不可达的,即将被回收。
其次,如果对象有finalize方法,那么对象被添加进finalization queue中;然后在某个时间点finalize方法被调用以释放finalize中的资源。
最后,回收对象占用的内存。
关于finalize方法的问题
finalize方法使得GC过程做了更多的事情,增加的GC的负担。
如果某个对象的finalize方法运行时间过长,它会使得其他对象的finalize方法被延迟执行。
finalize方法中如果创建了strong reference引用了其他对象,这会阻止此对象被GC。
finalize方法有可能以不可确定的顺序执行(也就是说要在安全性要求严格的场景中尽量避免使用finalize方法)。
不确保finalize方法会被及时调用,也许程序都退出了,但是finalize方法还没被调用。
衡量GC的指标(GC Metrics)
Throughput(吞吐量):所有没有花在执行GC上的时间占总运行时间的比重。
Pauses(暂停):当GC在运行时程序的暂停次数。或者是在感兴趣的暂停次数中,暂停的平均时长和最大时长。
Footprint(足迹?):当前使用的堆内存大小。
Promptness(及时性):不再使用的对象多久能被清除掉并释放其内存。
通用GC算法
Java所使用的所有的GC算法都是通用GC算法概念的变种。
通用GC算法的假设:
最近创建的对象很可能很快就不可达了(unreachable,即可被回收了),比如方法内部声明的本地变量,当程序运行出了本地变量的作用范围后,本地变量引用的对象就很快不可达了。
一个对象保持可达(reachable)的越久就越不可能被回收。
在Java GC中,对象被划分为generations(代)或spaces(空间)。Java把对象分为young(年轻代),tenured(年老代)和perm(永久代)。在GC过程中,对象从一个space(空间)移动到另一个space。
Object Spaces(对象空间)
- Young:年轻代中保存着刚创建的对象,这个代中的对象能够“minor” or “major” 收集中被回收。
- Tenured:年老代中保存着从年轻代中幸存下来的对象,只能够在“major”中被回收。
- Perm:永久代中保存着JVM所需的对象,比如Class对象和Method对象,以及他们的字节码和内部字符串等。对Perm中的对象GC意味着所有的Class都被卸载了。
每块空间的大小由当前的对内存大小决定,并且能够在运行时改变。每个空间之间的关系如下图所示:
Young Spaces(年轻空间)
- Eden space:存储自从上次GC完毕之后新创建的对象,除了属于Perm的对象。当minor collection发生时,Eden space中的对象或者GC清理掉,或者被移到survivor space。
- Survivor spaces:这个空间中存储的是自从上次GC幸存下来的young object。在minor GC中,这些对象或者被GC清理掉,或者被移到另外一个survivor空间中。
Minor collections和Major collections
- Minor collection当young space被占满时执行。它比major collections快,因为minor collection仅仅检查major collection相应的一个子集对象。minor collection比major collection发生的频率高。
- Major collection当tenured space被占满时执行。他会清理tenured和young。
GC运行的三种方式
在java5和java6中有4中垃圾回收的算法,有一种算法将不再支持,剩余的三种垃圾回收算法是:serial,throughput and concurrent low pause。
Stop the world(停止所有程序的方式):在这种方式运行的GC,在GC完成前,JVM中的所有程序都不允许运行。Serial collector此时做minor和major收集。Throughput collector此时做major collector。
Incremental(增量运行方式):目前没要Java GC算法支持这种运行方式。GC以这种方式运行时,GC允许程序做一小段时间的工作,然后做垃圾回收工作。
Concurrent(并行运行):Throughput collector此时做minor collect,Concurrent low pause collector此时做minor和major收集。在这种运行方式下,GC和程序并行的运行,因此程序仅仅被短暂的暂停。
GC算法
Serial算法: 使用-XX:+UseSerialGC开启此算法的GC。GC使用和应用程序相同的线程去做minor collection和major collection。 Throughput:使用-XX:+UseParallelGC开启此算法GC。GC使用多线程去做minor collection以减少程序停止的时间。但是对于major collection,还是使用同程序相同的线程去做。当具有多核cpu时,并且程序有大量的短生命周期的对象时,并且对程序停顿时间不限制时较好。 Concurrent Low Pause: 使用-XX:+UseConcMarkSweepGC开启此算法GC。使用多线程去做minor和major collection。当具有多核cpu,并且程序有大量的长生命周期的对象,并且对程序停顿时间有限制时,效果较好。
什么时候发生GC
GC发生的时刻受堆内存大小的影响。如果堆内存小,GC会执行的很快,但是又会很快的被填满,因此GC比频繁;如果堆内存很大,GC会执行的较慢,而且不会很快被填满,因此执行的比较频率比较低
基本的GC调试
throughput goal -XX:GCTimeRatio=n: 表示花费总时间百分之多少的CPU时间去运行程序。 maximum pause time goal -XX:MaxGCPauseMillis=n:每次GC时程序暂停最多多少毫秒。 footprint goal:如果其他目标都达到了,那么首先减少heap size,直到前两个goal不再满足,然后再慢慢增加。直到满足前面两个goal。 -Xms=n (starting) and -Xmx=n (maximum) heap size,这两个参数应该都很熟悉,就是JVM使用的最小堆内存数和最大堆内存数。 -XX:MinHeapFreeRatio=n, -XX:MaxHeapFreeRatio=n:最小和最大的空闲堆内存和被使用堆内存的比例。当空闲堆内存比例小于MinHeapFreeRatio时,内存空间开始扩展。当空闲堆内存比例大于MaxHeapFreeRatio时,内存空间开始减小。 -XX:NewSize=n, -XX:MaxNewSize=n:默认的young space的大小(包括eden + survivor 1 + survivor 2)。 -XX:NewRatio=n:young和tenured的比例。 -XX:SurvivorRatio=n:每个survivor space 和 eden之间的比例。 -XX:MaxPermSize=n:perm的最大size。 -XX:TargetSurvivorRatio=n:每次GC之后幸存下来的空间的目标比例。 -XX:+DisableExplicitGC:当此参数打开时,在程序中调用System.gc()将会不起作用。默认是off。 -XX:+ScavengeBeforeFullGC:当打开此参数时,在每次major collection时先执行一次minor collection。默认打开。 -XX:+UseGCOverheadLimit:当打开此参数时,如果总运行时间的98%的时间都在做GC,则抛出OutOfMemmoryError。默认打开。
4个方面学习Java GC机制
1,内存是如何分配的;
2,如何保证内存不被错误回收(即:哪些内存需要回收);
3,在什么情况下执行GC以及执行GC的方式;
4,如何监控和优化GC机制。
GC机制的基本算法是:分代收集
下面阐述每个分代的收集方法。
年轻代:
事实上,在上一节,已经介绍了新生代的主要垃圾回收方法,在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。
这里也可以发现,停止复制算法中,用来复制的两部分并不总是相等的(传统的停止复制算法两部分内存相等,但新生代中使用1个大的Eden区和2个小的Survivor区来避免这个问题)
由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到 老年代。用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,代表Eden:Survivor1:Survivor2=8:1:1.
老年代:
方法区(永久代):
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
- 类的所有实例都已经被回收
- 加载类的ClassLoader已经被回收
- 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同
在介绍垃圾收集器之前,需要明确一点,就是在新生代采用的停止复制算法中,“停 止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所 有线程的执行。这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。
- Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)
- ParNew收集器:新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
- Parallel Scavenge 收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)
- Serial Old收集器:老年代收集器,单线程收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存 的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。
- Parallel Old收集器:老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。
- CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见后面),当用户线程内存不足时,采用备用方案Serial Old收集。
CMS收集的方法是:先3次标记,再1次清除,3次标记中前两次是初始标记和重新标记(此时仍然需要停止(stop the world)), 初始标记(Initial Remark)是标记GC Roots能关联到的对象(即有引用的对象),停顿时间很短;并发标记(Concurrent remark)是执行GC Roots查找引用的过程,不需要用户线程停顿;重新标记(Remark)是在初始标记和并发标记期间,有标记变动的那部分仍需要标记,所以加上这一部分 标记的过程,停顿时间比并发标记小得多,但比初始标记稍长。在完成标记之后,就开始并发清除,不需要用户线程停顿。所以在CMS清理过程中,只有初始标记和重新标记需要短暂停顿,并发标记和并发清除都不需要暂停用户线程,因此效率很高,很适合高交互的场合。CMS也有缺点,它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担(CMS默认启动线程数为(CPU数量+3)/4)。另外,在并发收集过程中,用户线程仍然在运行,仍然产生内存垃圾,所以可能产生“浮动垃圾”,本次无法清理,只能下一次Full GC才清理,因此在GC期间,需要预留足够的内存给用户线程使用。所以使用CMS的收集器并不是老年代满了才触发Full GC,而是在使用了一大半(默认68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction来设置)的时候就要进行Full GC,如果用户线程消耗内存不是特别大,可以适当调高-XX:CMSInitiatingOccupancyFraction以降低GC次数,提高性能,如果预留的用户线程内存不够,则会触发Concurrent Mode Failure,此时,将触发备用方案:使用Serial Old 收集器进行收集,但这样停顿时间就长了,因此-XX:CMSInitiatingOccupancyFraction不宜设的过大。还有,CMS采用的是标记清除算法,会导致内存碎片的产生,可以使用-XX:+UseCMSCompactAtFullCollection来设置是否在Full GC之后进行碎片整理,用-XX:CMSFullGCsBeforeCompaction来设置在执行多少次不压缩的Full GC之后,来一次带压缩的Full GC。
- G1收集器:在JDK1.7中正式发布,与现状的新生代、老年代概念有很大不同,目前使用较少,不做介绍。