jvm理论:垃圾收集器与内存分配策略

Scroll Down

1、可达性分析算法

可达性分析算法的基本思路就是通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连或者不可达时,则证明此对象不可能再被使用。

jvmgclink.png

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
(1)、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
(2)、在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
(3)、在方法区中常量引用的对象,譬如字符串常量池(String table)里的引用。
(4)、在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
(5)、Java虚拟机内部的引用,如基本类型对应的Class对象,一些常驻的异常对象等,还有系统类加载器。
(6)、所有被同步锁(synchronized关键字)持有的对象。
(7)、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

2、再谈引用

无论是通过引用计数法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和引用离不开关系。在JDK1.2版之前,Java里面的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。这种定义并没有什么不对,只是现在看来有些狭隘了。
在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。
(1)、强引用:是最传统的引用定义,是指在程序代码之中普遍存在的引用赋值,即类似Object obj = new Object()这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象。
(2)、软引用:是用来描述一些还有用,但是非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2版之后提供了SoftReference类来实现软引用。
(3)、弱引用:是用来描述那些非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用。
(4)、虚引用:也被称为幽灵引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供了PhantomReference类来实现虚引用。

3、对象生存还是死亡

即使在可达性分析算法中判定为不可达对象,也不是代表着立即被回收,这时候它们暂时还处于待处理阶段,要真正回收对象,至少还要经历两次标记过程:如果对象在进行可达性分析算法后发现没有GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为没有必要执行。
如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。这里所说的执行是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统崩溃。finalize()方法是对象逃逸被回收的最后一次机会,稍后收集器将会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()方法中重新与引用链上的任何一个对象建立关联即可;如果对象这个时候还没有逃脱,那基本上就真正被回收了。

4、垃圾收集算法

4.1、分代收集理论

分代收集理论建立在两个分代假说之上:
(1)、弱分代假说:绝大多数对象都是朝生夕灭的。
(2)、强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;如果剩下的都是难以消亡的对象,那么把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
假如要现在进行一次只局限于新生代区域内的收集,但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。遍历整个老年代中所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:
(3)、跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

4.2、标记-清除算法

算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
标记-清除算法的主要缺点有两个:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须要进行大量标记和清除动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。

4.3、标记-复制算法

标记复制算法简称为复制算法。为了解决标记-清除算法面对大量可回收对象时,执行效率低的问题。出现了半区复制的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动栈顶指针,按顺序分配即可。复制算法的代价是将可用内存缩小为原来的一半。

4.4、标记-整理算法

标记整理算法其中标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象将会是一种极为负重的操作,而且这种对象移动操作必须完全暂停用户应用程序才能进行。
对于是否移动对象都存在弊端,移动则内存回收时会更加复杂,不移动则内存分配时更复杂。从垃圾收集的停顿时间角度来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。因此,HotSpot虚拟机里面关注吞吐量的parallel scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。

5、并发的可达性分析

可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析,这就意味着必须全程冻结用户线程的运行。在根结点枚举这个步骤中,由于GC Roots相比整个Java堆中全部的对象毕竟还算少数,且在各种优化技巧的加持下,它带来的停顿已经非常短暂且相对固定。可从GC Roots再继续往下遍历对象图,这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大,存储对象越多,对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长。
在解决或降低用户线程的停顿,就要先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历,为了能解释这个问题,引入了三色标记(Tri-color-Marking)作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照是否访问过这个条件标记成以下三种颜色:
(1)、白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束阶段,仍然是白色的对象,即代表不可达。
(2)、黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接指向某个白色对象。
(3)、灰色:表示对象已经被垃圾收集器访问过,但对象上至少还存在一个引用还没有被扫描过。

6、CMS垃圾收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
整个过程分为四个步骤:
(1)、初始标记(CMS initial Mark)
(2)、并发标记(CMS Concurrent mark)
(3)、重新标记(CMS remark)
(4)、并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段时间更短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
CMS垃圾收集器有以下三个明显的缺点:
首先,CMS收集器对处理器资源非常敏感,事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低吞吐量。CMS默认是启动的回收线程数是处理器核心数量+3/4,也就是说,如果处理器核心数在四个以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用程序本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
然后,由于CMS收集器无法处理浮动垃圾,有可能出现concurrent mode failure失败进而导致另一次完全stop the world的full GC的产生。在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理它们,只好等待下一次垃圾收集时再清理掉。这一部分垃圾就成为浮动垃圾。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待老年代几乎完全被填满了再进行垃圾收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到JDK1.6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留但内存无法满足程序分配新对象的需要,就会出现一次并发失败(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial old收集器来重新进行老年代的垃圾收集,但这样的停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置的太高将会很容易导致大量的并发产生失败,性能反而降低,用户应在生产环境中根据实际引用情况来权衡设置。
最后,CMS是基于标记清除算法实现的收集器,收集结束以后会有大量的空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前出发一次Full GC的情况。

7、GC调优基础

垃圾收集由两步构成:查找不再使用的对象,以及释放这些对象所管理的内存。

7.1、调整堆的大小

与其他的性能问题一样,选择堆的大小其实是一种平衡。如果分配的堆过于小,程序的大部分时间可能都消耗在GC上,没有足够的时间去运行应用程序的逻辑。但是,简单粗暴地设置一个特别大的堆也不是解决问题的办法。GC停顿消耗的时间取决于堆的大小,如果增大堆的空间,停顿的持续时间也会变长。在这种情况下,停顿的频率会变得更少,但是它们的持续时间会让程序的整体性能变慢。
使用超大堆还有另一个风险。操作系统使用虚拟内存机制管理机器的物理内存。操作系统在需要时会将程序运行时不活跃的数据由内存复制到磁盘。再次需这部分内存的内容时,操作系统再将它们由磁盘重新载入到内存。
系统中运行着大量不同的应用程序时,这个流程工作的很顺畅,因为大多数的应用程序不会同时处于活跃状态。但是,对于Java应用,它工作的并不那么好。如果一个Java应用程序使用了这个系统上大约12G的堆,操作系统可能在RAM上分配了8G的堆空间,另外4G的空间存在于磁盘。JVM不会了解这些:操作系统完全屏蔽了内存交换的细节。这样JVM填满了12G堆空间。但是这样就导致了严重的性能问题,因为操作系统需要将相当一部分的数据由磁盘交换到内存。
更糟糕的是,这种原本期望一次性的内存交换操作在Full GC时一定会再次重演。因为JVM必须访问整个堆的内容。如果Full GC时系统发生了内存交换,停顿时间会以正常停顿时间数个量级的方式增长。类似的,如果使用Concurrent收集器,后台线程在回收堆时,它的速度也可能会被拖慢,因为需要等待从磁盘复制数据到内存,结果导致发生代价昂贵的并发模式失效。
因此,调整堆大小时首要的原则就是永远不要将堆的容量设置得比机器的物理内存还大,另外,如果同一台机器上运行的多个JVM实例,这个原则适用于所有堆的总和。除此之外,如果还需要JVM自身以及机器上其他的应用程序预留一部分的内存空间:通常情况下,对于普通的操作系统,应该预留至少1G的内存空间。
堆大小由两个参数控制:分别是初始值(通过-Xms N设置)和最大值(通过-Xmx N设置)。默认值的调节取决于多个因素,包括操作系统类型、系统内存大小、使用的JVM。

7.2、代空间的调整

一旦堆的大小确定下来,就需要决定分配多少堆给新生代空间,多少给老年代空间。如果新生代分配的比较大,垃圾收集发生的频率就比较低,从新生代晋升到老年代的对象也更少。任何事务都有两面性,采用这种分配的方法,老年代就相对比较小,比较容易被填满,会更频繁地触发Full GC。
所有用于调整代空间的命令行标志调整的都是新生代空间:新生代空间剩下的所有空间都被老年代占用。多个标志都能用于新生代空间的调整,它们分别如下所列:
(1)、-XX:NewRatio=N 设置新生代与老年代的空间占比率。
(2)、-XX:NewSize=N 设置新生代空间的初始大小。
(3)、-XX:MaxNewSize=N 设置新生代空间的最大大小。
(4)、-XmnN 将NewSize和MaxNewSize设定为同一个值的快捷方法。
最初新生代空间大小是由NewRatio指定大小,NewRatio的默认值为2。影响堆空间大小的参数通常以比率的方式指定;这个值被用于一个计算空间分配的公式之中。
Initial young Gen Size = Initial Heap Size / (1 + NewRatio)
代入堆的初始大小和NewRatio的值就能得到新生代的设置值。默认情况下,新生代空间的大小是初始堆大小的33%。
除此之外,新生代的大小也可以通过NewSize标志显示地设定。使用NewSize标志设定的新生代大小其优先级要高于通过NewRatio计算出来的新生代大小。NewSize标志没有默认的设置。NewSize不设置的情况下,初始的新生代大小由NewRatio计算出的值决定。
通常推荐使用-Xmn标志将新生代也设定为固定大小。如果应用程序需要动态调整堆的大小,并希望有一个更大的新生代,那就需要关注NewRatio值的设定。

7.3、控制并发

除serial收集器之外几乎所有的垃圾收集器使用的算法都是基于多线程。启动的线程数由-XX:ParallelGCThreads=N参数控制。这个参数值会影响线程的数目:
(1)、使用-XX:+UseParallelGC收集新生代空间
(2)、使用-XX:+UseParallelOldGC收集老年代空间
(3)、使用-XX:+UseParNewGC收集新生代空间
(4)、使用-XX:+UseG1GC收集新生代空间
(5)、CMS收集器的时空停顿阶段
(6)、G1收集器的时空停顿阶段
由于GC操作会暂停所有的应用程序线程,JVM为了尽量缩短停顿时间就必须尽可能的利用更多CPU资源。这意味着,默认情况下,JVM会在机器的每个CPU上运行一个线程,最多同时运行8个。一旦达到这个上限,JVM会调整算法,每超出5/8个CPU启动一个新的线程。所以总的线程数就是:parallelGCThreads = 8 + ((N - 8) * 5 /8)

7.4、理解Throughput收集器

Throughput收集器有两个基本操作:其一是回收新生代的垃圾,其二是回收老年代垃圾。
通常新生代的垃圾回收发生在Eden空间快用尽时。新生代垃圾收集会把Eden空间中的所有对象挪走:一部分对象会被移动到Survivor空间,其他的会被移动到老年代;

7.5、理解CMS收集器

CMS收集器有3种基本的操作,分别是:
(1)、CMS收集器会对新生代的对象进行回收(所有的应用线程都会被暂停)
(2)、CMS收集器会启动一个并发的线程对老年代空间的垃圾进行回收
(3)、如果有必要,CMS会发起Full GC
CMS收集器的新生代垃圾收集:对象从Eden空间移动到Survivor空间,或者移动到老年代空间。
JVM会依据堆的使用情况启动并发回收。当堆的占用达到某个程度时,JVM会启动后台线程扫描堆,回收不用的对象。请注意,如果使用CMS回收器,老年代空间不会进行压缩整理:老年代空间由已经分配对象的空间和空闲空间共同组成。新生代垃圾收集将对象由Eden空间挪到老年代空间时,JVM会尝试使用那些空闲的空间来保存这些晋升的对象

7.6、针对并发模式失效的调优

调优CMS收集器时最要紧的工作就是要避免发生并发模式失效以及晋升失败。发生并发模式失效往往是由于CMS不能以足够快的速度清理老年代空间:新生代需要进行垃圾回收时,CMS收集器计算发现老年代没有足够的空闲空间可以容纳这些晋升对象,不得不先对老年代进行垃圾回收。
初始时老年代空间中对象是一个接一个整齐有序排列的。当老年代空间占用达到某个程度时,并发回收就开始了。一个CMS后台线程开始扫描老年代空间,寻找无用的垃圾对象时,竞争就开始了:CMS收集器必须在老年代剩余空间用尽之前,完成老年代空间的扫描以及回收工作。如果并发回收的速度不及时,CMS收集器就会发生并发模式失效。
以下途径可以避免发生这种失效。
(1)、想办法增大老年代空间,要么只移动部分的新生代对象到老年代,要么增加更多的堆空间。
(2)、以更高的频率运行后台回收线程。
(3)、使用更多的后台回收线程。

1、给后台线程更多的运行机会
为了让CMS收集器尽早完成收集,方法之一就是更早地启动并发收集周期。显然地,CMS收集器在老年代空间占用达到60%时启动并发周期,这和老年代空间占用到70%时才启动相比,前者完成垃圾收集的几率更大。为了实现这种配置,最简单的方法是同时设置-XX:CMSInitiatingOccupancyFraction=N和-XX:+UseCMSInitiatingOccupancyOnly。
2、调整CMS后台线程
每个CMS后台线程都会100%地占用机器上的一颗CPU。如果应用程序发生并发模式失效,同时又有额外的CPU周期可用,可以设置-XX:ConcGCThreads=N标志,增加后台线程的数目。

7.7、补充JVM调优文档

主要有三个方面:
(1)、确定并调优应用程序内存使用
(2)、确定并调优应用程序延迟
(3)、确定并调优应用程序吞吐量
使用hotspot VM的-XX:+UseParallelOldGC或-XX:UseParallelGC命令行选项可指定使用Throughput收集器。如果使用的hotspot虚拟机不支持-XX:UseParallelOldGC可以使用-XX:UseParallelGC。这两个命令行选项的区别是-XX:UseParallelOldGC将同时启用多线程的新生代和老年代的垃圾收集器,即MinorGC和FullGC都采用多线程,而-XX:UseParallelGC仅启用了多线程的新生代垃圾收集器。使用-XX:UseParallelGC选项。因此,如果需要同时启用多线程的新生代和老年代垃圾收集器,则需要使用-XX:UseParallelOldGC选项。
垃圾收集性能调优的原则:
(1)、吞吐量:指不考虑垃圾收集器引起的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标。
(2)、延迟:度量的标准是缩短由于垃圾收集引起的停顿时间或完全消除因垃圾收集器所引起的停顿,避免应用程序运行时发生抖动。
(3)、内存占用:垃圾收集器流畅运行所需要的内存数量
Jvm垃圾收集器调优的基本原则:
(1)、每次minor GC都尽可能多地收集垃圾对象。遵守这一原则可以减少应用程序发生FullGC的频率。FullGC的持续时间总是最长的,是应用程序无法达到其延迟或吞吐量要求的罪魁祸首。
(2)、处理吞吐量和延迟问题时,垃圾处理器能使用的内存越大,即Java堆空间越大,垃圾收集器的效果越好,应用程序运行也越流畅。
(3)、在这三个性能属性中任意选择两个进行jvm垃圾收集器调优。
Hotspot vm提供了多个GC日志相关的命令行选项,推荐使用-XX:PrintGCTimeStamps -XX:PrintGCDetails -Xloggc:
-XX:PrintGCTimeStmps打印从hotspotVM启动知道GC开始所经历的时间(以秒计时)。-XX:PrintGCDetails提供垃圾收集器相关的统计数据,该选项的输出与使用的垃圾收集器密切相关。所以使用不同的垃圾收集器输出结果会有不同。-Xloggc:选项可以指定将GC的日志信息记录到名为的文件中。
确定内存占用:活跃数据的大小是指,应用程序稳定运行时长期存活的对象所占用的Java堆内存量。换句话说,它是应用程序运行于稳定态时,FullGC之后Java堆所占用的空间大小。
HotspotVM有三个主要的空间,分别是:新生代、老年代以及永久代。Java应用程序分配Java对象时,首先在新生代空间中分配对象。存活下来的对象,即经历几次minorGC之后还保持活跃的对象会被提升进入老年代空间。永久代空间中存放VM和Java类的元数据以及驻留的String和类静态变量。
-Xmx和-Xms命令行选项指定了新生代和老年代空间的大小的初始值和最大值。初始值及最大值也被称为Java堆的大小。-Xms设定了初始及最小值,-Xmx可以设定最大值。当-Xms指定值的小于-Xmx的值时,新生代及老年代的空间的大小根据应用程序的需要动态地扩展或缩减。Java应用程序应该将-Xms和-Xmx设定为同一值。这是因为无论扩展还是缩减新生代空间或老年代空间都需要进行Full GC,而Full GC会降低程序的吞吐量并导致更长的延迟。
新生代空间可以通过下面任何一个命令行选项设置:
(1)、-XX:NewSize 新生代空间的大小的初始值,也是最小值。新生代不会小于该设定值使用-XX:NewSize选项时,应当同时指定-XX:MaxNewSize选项
(2)、-XX:MaxNewSize 新生代空间的大小的最大值。应当同时指定-XX:NewSize
(3)、-Xmn 设置新生代空间的初始值,最小以及最大值。新生代空间的大小会根据该值进行设定。
有一点需要特别注意,如果-Xms和-Xmx并没有设定为同一个值,使用-Xmn选项时,Java堆的大小变化不会影响新生代空间,即新生代空间的大小总保持恒定,而不是随着Java堆大小的扩展或缩减做相应的调整。因此,请注意,只有在-Xms与-Xmx设定为同一值时才使用-Xmn选项。
老年代空间的大小会根据新生代大小设定,老年代空间的初始值为-Xmx的值减去-XX:NewSize的值,老年代空间的最小值为-Xmx的值减去-XX:MaxNewSize的值。如果-Xms与-Xmx设置为同一值,同时使用了-Xmn,或者-XX:NewSize与-XX:MaxNewSize一样,则老年代的大小为-Xmx的值减去-Xmn
永久代空间的大小可以通过下面的命令选项设置
(1)、-XX:PermSize 永久代空间的初始值及最小值。永久代空间的大小不会小于该设定值。
(2)、-XX:MaxPermSize 永久代空间的最大值。永久代空间不会大于该设定值。
永久代大小的初始值和最大值应该设置为同一值,因为永久代空间的大小调整需要进行FullGC才能实现。
新生代、老年代或永久代这三个空间中的任何一个不满足内存分配请求时,就会发生垃圾收集,换句话说,这三个空间中任何一个被用尽,同时又有新的空间请求无法满足时就会触发垃圾收集。新生代没有足够的空间满足Java对象分配请求时,hotspotVM会进行minorGC以释放空间,MinorGC相对于FullGC而言,持续的时间要短。经历过几次MinorGC之后仍然活跃的对象最终会被提升到老年代。老年代空间不足以容纳新提升的对象时,hotspotVM就会进行FullGC。实际上,当HotspotVM发现当前可用空间不足以容纳下一次minorGC提升的对象时就会进行FullGC。永久代没有足够的空间存储新的VM或类元数据时也会发生Full GC。如果Full GC缘于老年代空间已满,即使永久代空间并没有用尽,老年代和永久代都会进行垃圾收集。同样,如果FullGC由永久代空间用尽引起的,老年代和永久代也都会进行垃圾收集,无论老年代是否还有空闲空间。开启-XX:+UseParallelGC或-XX:+UseParallelOldGC时,如果关闭-XX:-ScavengeBeforeFullGC,hotspotVM在Full GC之前不会进行MinorGC,但FullGC过程中依然会收集新生代;如果开始-XX:+ScavengeBeforeFullGC,hotspotVM在Full GC前会先做一次MinorGC,分担一部分FullGC原本要做的工作。
计算活跃数据大小
Java应用的活跃数据大小可以通过GC日志收集。活跃数据大小包括下面的内容:
(1)、应用程序运行于稳定态时,老年代占用的Java堆大小
(2)、应用程序运行于稳定态时,永久代占用的Java堆大小
通用的法则之一,将Java堆的初始值-Xms值和最大值-Xmx设置为老年代活跃数据大小的3-4倍。
通用的法则之二,永久代的初始值-XX:PermSize及最大值-XX:MaxPermSize应该为永久代活跃数据的1.2-1.5倍
补充法则,新生代空间应该为老年代活跃数据的1-1.5倍。
如果Java堆的初始值及最大值为活跃数据大小的3-4倍、新生代为活跃数据的1-1.5倍时,老年代应设置为活跃数据的2-3倍。
优化新生代的大小
根据垃圾收集器的统计数据、minorGC的持续时间和频率可以确定新生代空间的大小。
MinorGC需要的时间与新生代中可访问的对象数直接相关,通常情况下,新生代空间越小,MinorGC持续时间越短,不考虑这对于MinorGC持续时间的影响,减小新生代空间又会增大MinorGC的频率。这是因为以同样的对象分配的频率,较小的新生代空间在很短的时间内就会被填满,增大新生代空间可以减少MinorGC的频率。
分析GC数据时,如果发现MinorGC的间隔时间过长,修正的方法是减少新生代空间。如果MinorGC频率太高,修正的方法是增加新生代的空间。
调整新生代空间时,需要谨记下面几个准则。
(1)、老年代空间的大小不应该小于活跃数据的1.5倍。
(2)、新生代空间至少应为Java堆大小的10%,通过-Xmx和-Xms可以设置该值。新生代过小可能适得其反,会导致频繁的MinorGC。
(3)、增大Java堆大小时,需要注意不要超过JVM可用的物理内存数。堆占用过多内存将导致底层系统交换到虚拟内存,反而会造成垃圾收集器和应用程序性能低下。
优化老年代的大小
这一步的目标是评估FullGC引入的最差停顿时间以及Full GC的频率。
为CMS调优延迟
使用CMS收集器时,老年代垃圾收集器线程与应用程序线程能实现最大的并行度。CMS并不进行压缩,所以这一效果主要是避免老年代发生stop-the-world压缩式垃圾来收集实现的。一旦老年代超标就会触发stop-the-world压缩式收集。
从Throughput收集器迁移到CMS时,如果发生从新生代到老年代的对象的提升,可能会经历较长的MinorGC持续时间,这是由于对象提升到老年代变慢了。
CMS在老年代空间从空闲列表中分配内存。与之相反,Throughput收集器只需要在线程本地分配的提升缓存中移动指针即可。另外由于老年代垃圾收集线程能够与应用程序实现最大程度的并发执行,所以可以预期应用程序的吞吐量会更低。然而,发生这种最差延迟的几率并不是很大,因为应用程序运行时老年代中的不可达对象会进行垃圾收集,从而避免老年代空间被填满。使用CMS时,如果老年代空间用尽,就会触发一个单线程的stop-the-world压缩式垃圾收集。相对于Throughput收集器的Full GC而言,CMS垃圾收集器通常的持续时间更长。因此,采用CMS的绝对最差延迟要比Throughput收集器的最差延迟时间要长。老年大空间耗尽并因此触发stop-the-world压缩式垃圾收集时,由于程序长时间无法响应,会引起关注。从Throughput收集器迁移到CMS收集器时需要遵守的一个通用原则是,将老年代空间增大20%-30%,这样才能更有效的运行CMS收集器。
Survivor空间介绍
Survivor空间是新生代空间的一部分。在所有的hotspot垃圾收集器中,新生代空间都被划分成一个Eden空间和两个Survivor空间。两块Survivor空间中,一块标记为From Survivor空间,另一块空间标记为To Survivor空间。Eden空间是分配新Java对象的空间。当Eden空间被填满时就会发生MinorGC。活跃对象会从Eden空间复制到to的Survivor空间,同时From Survivor空间中存活下来的对象也会复制到To Survivor空间中。一旦完成Minor GC,Eden空间就会被清空,From Survivor空间也变为空,而To Survivor空间中保存了还活跃的对象。之后Survivor空间将相互标记为下次Minor GC作准备。调整Survivor空间的大小,让其有足够的空间容纳存活的对象足够长的时间,知道几个周期之后对象老化,就能避免发生Survivor空间溢出,有效的老化方法可以使老年代中只保存长期活跃的对象。
Survivor空间的大小可以通过hotspot的命令行选项调整:-XX:SurvivorRatio,对于给定的新生代,减少Survivor的比率增大Survivor,同时减少Eden空间。同样,增大Survivor比率会减少Survivor空间,增大Eden空间。意识到减少Eden空间会导致更频繁的Minor GC是非常重要的,与之相反,增大Eden空间可以减少MinorGC的频率。同样非常重要的一点是,垃圾收集发生的频率越高,对象老化的速度就越快。
-XX:MaxTenuringThreshould 不建议将最大的晋升阀值设置为0,这会造成刚刚分配的对象在紧接着MinorGC 中直接从新生代提升到老年代,同时造成老年代空间迅速增长,引起频繁的Full GC。此外,也不建议将最大的晋升阀值设置的远远大于实际的可能的最大值。这会造成对象长期存在于Survivor空间。直到最后溢出。一旦发生溢出,对象将全部被提升到老年代,不再依据其实际的年龄进行提升。这样会造成短期内存在的对象在长期存在对象之前被提升到老年代,严重影响对象老化机制的有效性。
当目标survivor空间的占用等于或小于hotspotVM期望维护的值时,hotspotVM将使用最大晋升阀值作为其计算出的晋升阀值。如果hotspotVM认为它无法维持survivor空间的占用,他会使用一个低于最大值的晋升阀值来保证目标survivor空间的占用。比晋升阀值年龄大的对象都会被提升到老年代。换句话说,当存活下来的对象占用的空间超过目标survivor空间的容量时就会发生溢出。溢出会导致对象被迅速提升至老年代,造成老年代的增长也远快于预期,而这又会引起CMS被频繁调用,降低应用程序的吞吐量,增大了出现碎片的可能性。所有这些都可能导致更频繁的stop-the-world压缩式垃圾收集