垃圾收集器
图片看不清楚,可以下载或在页面中单独查看图片
1. 概述
Garbage Collection, GC:1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集器技术的语言。
程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题。
而Java堆和方法区则不一样:一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只能在运行期才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
2. 对象已死吗?
堆里面存放着Java中几乎所有的对象实例,垃圾收集器在对堆进行回收前,需要确定那些还“存活”,哪些已经“死去”,即不可能再被任何途径使用的对象。
2.1 引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
应用:微软的COM计数,AS3,Python语言等。
2.2 可达性分析算法
Reachability Analysis:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
对象object5,object6,object7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为可回收的对象。
在Java中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般的Native方法)引用的对象
2.3 强引用,软引用,弱引用,虚引用
jdk1.2之前,Java中引用的定义:如果reference类型的数据中的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。
在jdk1.2之后,Java对引用的概念进行了扩充,将引用分为强引用,软引用,弱引用,虚引用。强度依次逐渐减弱。
(1)强引用:程序中普遍存在的,类似Object obj = new Object(),这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象;
(2)软引用:描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在jdk1.2之后,提供了SoftReference类来实现软引用;
(3)弱引用:当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被引用关联的对象。在jdk1.2之后,提供了WeakReference类来实现弱引用;
(4)虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,在jdk1.2之后,提供了PhantomReference类来实现虚引用。
2.4 finalize()方法
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。(1)当对象没有覆盖finalize()方法;(2)finalize方法已经被虚拟机调用过;虚拟机这两种情况都视为“没有必要执行”。
如果对象被判定为有必要执行finalize方法,那么对象将会放置在一个叫做F-Queue的队列中,虚拟机自动建立一个优先级低的Finalizer线程去执行它(这里“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象的finlize方法执行缓慢或发生死循环,将很可能导致F-Queue队列中其他对象永久处于等待)。稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象重新与引用链上的任何对象建立关联即可,那个第二次标记时它将被移除“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
实例:
package org.github.oom; public class FinalizeEscape { public static FinalizeEscape fe = null; public void alive() { System.out.println("yes, i am still alive..."); } public static void dead() { System.out.println("no, i am dead..."); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!!!"); fe = this; } public static void main(String[] args) throws InterruptedException { fe = new FinalizeEscape(); // 对象第一次成功拯救自己 fe = null; System.gc(); // 因为finalize方法优先级很低,所以暂停1秒,等待它 Thread.sleep(1000); if (fe != null) { fe.alive(); } else { dead(); } // 拯救失败 fe = null; System.gc(); // 因为finalize方法优先级很低,所以暂停1秒,等待它 Thread.sleep(1000); if (fe != null) { fe.alive(); } else { dead(); } } }
运行结果:
注意,任何一个对象的finalize方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize方法不会被再次执行。
2.5 回收方法区
HotSpot虚拟机中的实现是永久代,主要回收两个部分:废弃常量和无用的类。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足3个条件才能算是“无用的类”:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不是了就必然回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class,-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看类加载和卸载信息。
注:在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
3. 垃圾收集算法
由于垃圾收集算法的实现涉及大量的程序细节,而且各个平台的虚拟机操作内存的方法又各不相同,因此不打算过多讨论算法的实现。
3.1 标记 - 清除算法
Mark-Sweep算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
不足:(1)效率问题,标记和清除的过程的效率都不高;(2)空间问题,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能导致在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.2 复制算法
为了解决效率问题,一种称为“复制”的收集算法,它将可用内存按容量划分为大小相等的两块。每次只使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。每次都是对整个半区进行内存回收,内存分配时就不用考虑内存碎片等复杂情况。
现在的商业虚拟机都采用这种收集算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(Eden + 一个Survivor),只有10%会被浪费。当存活对象大于10%,另一Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保。
分配担保:如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
3.3 标记 - 整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率就会变低。根据老年代的特点,有人提出了另外一种“标记 - 整理”算法,标记过程与“标记 - 清除”算法一致。但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
3.4 分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法(Generational Collection)。根据对象存活周期的不同将内存划分为几块,Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记 - 清理”或“标记 - 整理”算法进行回收。
3.5 HotSpot的算法实现
上面介绍了对象存活判定算法和垃圾收集算法,而在HotSpot虚拟机上实现这些算法时,必须对算法的执行效率有严格的考量,才能保证虚拟机高效运行。
3.5.1 枚举根节点
从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(常量或类静态属性等)与执行上下文(栈帧中的本地变量表)中。
现在很多应用仅仅方法区都有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行。
目前的虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,HotSpot虚拟机中使用一组称为OopMap的数据结构来达到这个目的。
3.5.2 安全点
在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,将会需要大量的额外空间,这样GC的空间成本将会变得很高。
实际上,HotSpot也的确没有为每条指令都生成OopMap,只有在特定的位置记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在达到安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。
“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
对于安全点,另一个需要考虑的问题是如何在GC发生时让所有线程都“跑”到最近的安全点上再停顿下来。有2种方案可供选择:抢先式中断和主动式中断。
(1)抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在JVM没有采用这种方法。
(2)主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的方法和安全点是重合的,另外再加上创建对象需要分配内存的地方。
3.5.3 安全区域
使用Safepoint似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定。Safepoint机制保障了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?
所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于sleep状态或Blocked状态,这时候程序无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间,对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,也可以把Safe Region看做是被扩展了的Safepoint。
在线程执行到Safe Region中的代码时,首先标示自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不会管标识自己为Safe Region状态的线程了。
在线程要离开Safe Region时,它要检测系统是否已经完成了根节点枚举(或整个GC过程),如果完成了,那线程就继续执行,否则它必须等待知道收到可以安全离开Safe Region的信号为止。
4. 垃圾收集器
收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特征和要求组合出各个年代所使用的收集器。
目前主要有7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。收集器所处的区域,则表示它是属于新生代收集器还是老年代收集器。
注:对各个收集器进行比较,但并非为了挑选一个最好的收集器,因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。
4.1 Serial收集器
虚拟机运行在Client模式下的默认新生代收集器。只会使用一个CPU或一条收集线程去完成垃圾收集工作。
简单高效,对于限定单个CPU的环境来说,Serial收集器由于没有先交互额开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
4.2 ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数,收集算法,Stop The World,对象分配规则,回收策略等都与Serial收集器完全一样。
ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中一个很重要的原因是除了Serial收集器外,只有它能与CMS收集器配合工作。
备注:此处解释并行与并发的区别
并行:指多条垃圾收集器线程并行工作,但此时用户线程仍然处于等待状态;
并发:指用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
4.3 Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的并行收集器。
Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:
(1)-XX:MaxGCPauseMillis最大垃圾收集停顿时间,参数允许的值是一个大于0的毫秒数。
不要以为把这个参数的值设置得小一点就能使得系统的垃圾收集速度变得更快。GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一点,收集300MB新生代肯定比收集500MB快,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间的确实下降了,但是次数多了,吞吐量也就降下来了。
(2)-XX:GCTimeRatio设置吞吐量大小,值应该是一个0 < xx < 100的整数,默认值为99,就是允许最大1%(即1 /(1 + 99))的垃圾收集时间。
Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy这是一个开关参数,打开就不需要手动指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量,这种调节方式称为GC自适应的调节策略。
4.4 Serial Old收集器
Serial Old是Serial收集器的老年代版本,单线程,标记 - 整理算法, 这个收集器主要用在Client模式下。如果是Server模式下,有两个用途(1)与Parallel Scavenge收集器搭配使用;(2)在CMS并发收集发生Concurrent Mode Failure时作为CMS收集器的后备预案。
注:Concurrent Mode Failure后面会介绍。
4.5 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,多线程,标记 - 整理算法。
4.6 CMS收集器
Concurrent Mark Sweep是一种以获取最短回收停顿时间为目标的收集器,多线程,标记 - 清除算法(碎片问题)。目前很大一部分的Java应用集中在互联网或者B/S系统的服务器上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
它的运行过程分为4个步骤:
(1)初始标记(CMS initial mark),需要STW,只是标记GC Roots能直接关联到的对象,速度很快;
(2)并发标记(CMS concurrent mark),进行GC Roots Tracing的过程;
(3)重新标记(CMS remark),需要STW,为了修正并发标记阶段因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿时间比初始标记阶段稍长一些,但是远比并发标记的时间短;
(4)并发清除(CMS concurrent sweep)
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS是一款优秀的收集器,它的主要优点:并发收集、低停顿。
但是也有如下3个明显的缺点:
(1)CMS收集器对CPU资源非常敏感:其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(sizeof(Cpu) + 3)/ 4。
(2)CMS收集器无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。由于CMS并清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就需要预留有足够的内存空间给用户线程使用,因此CMS不能像其他收集器那样等到老年代几乎完全填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用,CMS在老年代占用到92%时运行CMS,可以通过-XX:CMSInitiatingCccupancyFraction的值来改变。如果CMS运行期间预留的空间无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启动Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
(3)CMS是一款基于“标记 - 清除”算法实现的收集器(因为并发清除阶段,用户线程不停顿,没法使用标记 - 整理算法)。意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCOmpactAtFullCollection开关参数(默认是开启的),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标示每次进入Full GC时都进行碎片整理)
4.7 G1
Garbage-First:一款面向服务器端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉CMS收集器。
G1收集器的运行大致可划分为以下几个步骤:
(1)初始标记(Initial marking):标记以下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
(2)并发标记(Concurrent marking):从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这阶段耗时较长,但可与用户程序并发执行。
(3)最终标记(Final marking):为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,需要停顿线程,但是可并行执行。
(4)筛选回收(Live data couting and evacuation):首先对各个Region的回收价值和成本记性排序,根据用户所期望的GC停顿时间来指定回收计划。需要停顿,只回收一部分Region,时间上是用户可控制的,而且停顿用户线程将大幅提高收集效率。
与其他收集器相比,G1具备如下特点:
(1)并行与并发:能充分利用多CPU,多核环境下的硬件优势。
(2)分代收集:分代概念在G1中依然得以保留。G1可以不需要其他收集配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
(3)空间整合:G1从整体上看是基于“标记 - 整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法。都不会产生内存空间碎片。
(4)可预测的停顿:降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
在G1之前的其他收集器进行收集的范围都是整个新生代或老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region的集合。
G1之所以能够建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后天唯一一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(G1名称的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
5. GC日志
每一种收集器的日志形式都是由它们自身的实现所决定的,即每个收集器的日志格式都可以不一样,但是各个收集器的日志也有一定的共性。
6. 配置垃圾收集器及参数
6.1 配置垃圾收集器
从上面图中可以看出新生代与老年代之间的收集器一共有6种组合,下面通过实例验证收集器:
6.1.1 UseSerialGC(client模式下的默认值)
打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收。
如何设置Serial + CMS + Serial Old ?
实例:
package com.huawei.jvm; public class Test00 { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] b1 = new byte[6 * _1MB];
byte[] b2 = new byte[4 * _1MB]; } }
设置jvm的参数为:
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC
-Xms20M -Xmx20M参数限制Java堆大小为20MB,不可扩展。
-Xmn10M其中10MB分配给新生代,剩下的10MB分配给老年代。
-XX:SurvivorRatio=8设置了新生代中Eden区与一个Survivor区的空间比例为8:1,即Eden为8MB,to Survivor与from Survivor为1MB,即新生代可用空间为9MB(Eden区加一个Survivor区)。
-XX:+PrintGCDetails打印GC详细信息。
-XX:+UseSerialGC使用Serial + Serial Old的收集器组合进行内存回收
运行上面的程序,控制台打印如下的GC日志和堆信息:
过程如下:
6.1.2 UseParNewGC【deprecated】
使用ParNew + Serial Old的收集器组合。
实例:
将上面实例中的收集器修改为-XX:+UseParNewGC。
GC日志与使用Serial + Serial Old的收集器类似,只是有个提示:将来会移除这种收集器的组合,主要原因是这种组合方式的收集器很少使用,但是却花费了很大的开发,维护和测试。具体可以参考:【http://openjdk.java.net/jeps/173】,【http://openjdk.java.net/jeps/214】
6.1.3 UseConcMarkSweepGC
使用ParNew + CMS + Serial Old收集器组合,其中Serial Old收集器作为CMS出现Concurrent Mode Failure失败后的后备收集器。
将上面实例中的收集器修改为-XX:+UseConcMarkSweepGC。
GC日志也是类似的,可以看出不同的收集器对新生代和老年代的命名是有部分区别的
6.1.4 UseParallelGC(server模式下的默认值)
使用Parallel Scavenge + Serial Old(PS MarkSweep)
将上面实例中的收集器修改为-XX:+UseParallelGC。
奇怪,Eden不足,却没有发生GC,直接将4MB存入了老年代。
修改代码:
package com.huawei.jvm; public class Test00 { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] b1 = new byte[6 * _1MB]; byte[] b2 = new byte[3 * _1MB]; } }
通过对比发现,当整个新生代剩余的空间(Eden加一个Survivor)无法存放某个对象时,Parallel Scavenge/Parallel Old中该对象会直接进入老年代;
而如果整个新生代剩余的空间可以存放但只是Eden区空间不足,则会尝试一次Minor GC;
而对于Serial/Serial Old当发现Eden区不足以存放对象时,就进行一次Minor GC。
此外,为什么触发了一次新生代GC,然后又触发了一次Full GC呢?
其实,Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)
6.1.5 UseParallelOldGC
使用Parallel Scavenge + Parallel Old
修改使用-XX:+UseParallelOldGC
也是没有触发GC的,第二次分配改成3MB就会触发GC,原理同(4)UseParallelGC。
6.2 参数
(1)SurvivorRatio: 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Survivor = 8:1
(2)PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数时对象将直接在老年代分配。该参数只对新生代的Serial和ParNew收集器才起作用。实例见下面章节【大对象直接进入老年代】
(3)MaxTenuringThreshold: 晋升到老年代的对象年龄,每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时进入老年代
(4)UseAdaptiveSizePoliy: 动态调整Java堆中个区域的大小以及进入老年代的年龄
(5)HandlePromotionFailure: 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况
(6)ParallelGCThreads: 设置并行GC时进行内存回收的线程数
(7)GCTimeRatio: GC时间占总时间的比率,默认值为99,即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效
(8)MaxGCPauseMillis: 设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效。
(9)CMSInitiatingOccupancyFraction: 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效
(10)UseCMSCompactAdFullCollection: 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用CMS收集器时生效
(11)CMSFullCsBeforeCompaction: 设置CMS收集器在进行若干次垃圾收集器再启动一次内存碎片整理,仅在使用CMS收集器时生效
7. 内存分配与回收策略
对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按照线程优先在TLAB上分配,少数情况下也可能会直接分配在老年代中。分配规则并不是固定的,其细节取决于当前使用的哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。
7.1 对象优先在Eden分配
7.2 大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串和数组。
虚拟机提供了一个-XX:PretenureSizeThread参数,令大于这个设置值的对象直接在老年代分配,这样可以避免在Eden区和两个Survivor区直接发生大量的内存复制。
测试代码:
package com.huawei.jvm; public class Test00 { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] b1 = new byte[4 * _1MB]; } }
VM参数
-server -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
-XX:+UseSerialGC -XX:PretenureSizeThreshold=5242880
即大于等于5MB的对象才会直接分配到老年代中,分别测试分配4MB和6MB的对象,然后堆的情况:
4MB的堆:
6MB的堆:
7.3 长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设置为1,。对象在Survivor中每熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认15),就会晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
7.4 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
7.5 空间分配担保
- 在发生Minor之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间;
- 如果这个条件成立,那么Minor GC可以确保是安全的,则执行Minor GC;
- 如果不成立,虚拟机会接着查看HandlePromotionFailure设置值是否允许担保失败;
- 如果允许,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;
- 如果大于,将尝试一次Minor GC,如果小于或HandlePromotionFailure设置不允许,则进行一次Full GC
从JDK6 Update 24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。
从JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC ,否则将进行Full GC。
x. 参考资料
http://blog.csdn.net/canot/article/details/51069424
http://blog.csdn.net/z69183787/article/details/51606410
http://openjdk.java.net/jeps/173
http://openjdk.java.net/jeps/214