如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。虚拟机具体如何进行内存回收动作,是由虚拟机所采用的GC收集器所决定的,而通常虚拟机中往往不止有一种GC收集器,像目前(JDK 7时代)的HotSpot里面就包含有Serial、Serial Old、ParNew、Parallel Scavenge(简称PS)、Parallel Old、Concurrent Mark Sweep(简称CMS)和Garbage First(简称G1)七种收集器。下面,简单的介绍一下内存回收的具体过程。
上图中的7个收集器,分为两块:新生代收集器,老年代收集器。如果两个收集器之间存在连线,则说明它们之间可以搭配使用。
一. 几个概念
1. 并发和并行
并发和并行,这两个名词都是并发编程中的概念,它们的区别就是一个处理器同时存在多个任务和多核处理器同时处理多个不同的任务。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。
在并发系统中可以同时拥有两个或者两个以上线程,这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的,每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。所以并发编程的目的,是充分利用处理器的每一个核,以达到最优的处理性能。
我相信你已经能够得出结论——并发概念是并行的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。
2. JVM启动模式
JVM在启动时包含2种模式:Client模式和Server模式。
- Client模式:启动速度较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试。在32位环境下直接运行Java程序时默认启用该模式;
- Server模式:启动比Client模式慢10%,但运行时性能和内存管理效率较高,适用于生产环境。在具有64位能力的JDK环境下默认启用该模式。
在JVM启动的时候指定:-Client或者-Server来判断启动模式,一般大多数应用环境采用的是Server模式启动。
3. 什么是GC?
叫做Garbage Collection或者Garbage Collector,它既是一种内存管理机制,也可以看做是一种内存管理程序。Java跟C++之间很大的一个区别就是,内存自动分配和回收。所以Java程序在运行时,JVM中有一个专门线程负责做这件事,这就是GC。GC所关注的,就是前文提到的Heap和Method Area,它主要工作在Heap上面。
GC主要有以下职责:
- 内存分配
为新对象的分配请求寻找足够的内存空间;
对象的内存分配,大体上来说就是在堆上分配(但也可能经过JIT实时编译后被拆散为标量类型并间接的在栈上分配),对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲(-XX:+UseTLAB,默认开启),将按照线程优先在 TLAB 上分配。少数情况下也可能会直接分配在老年代中(“大对象直接进入老年代”),分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。 - 回收垃圾内存
当内存满了、不能为新对象分配内存时,或是内存使用达到一定的百分比时,回收掉那些不再被使用的“垃圾对象”; - 整理内存碎片
回收完成后,可能会有很多不连续的内存空间,而整理这些内存碎片,有助于下次更快的为新对象分配内存。
如何衡量一个GC的性能呢?
- 生产能力
应用程序运行的时间与总运行时间的比例; - GC运行的额外消耗
GC程序运行的时间与总运行时间的比例; - 停顿时间
GC运行时,应用程序暂停的时间; - GC频率
GC的执行频率; - 覆盖的区域大小
GC工作的区域大小; - 垃圾回收的及时性
一个对象成为垃圾对象到被回收的时间。
4. GC的回收策略
HotSpot VM采用了分代收集的方式,不同的内存区域的对象,存活的年龄不一样。
JVM堆内存分为2块:Permanent和Heap{ Old + New = {Eden, From, To} }。
- 新生代(Young)
新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。新生代一般分3个区:1个Eden区,2个Survivor区(From和To)。
大多数新生成的对象首先都是直接分配在新生代的Eden区域(一些大对象直接分配在老年代),Survivors的 From 区域保存那些在至少一次新生代GC之后存活下来,但是还不能被认为是“足够老”的对象。它们有更多死亡的机会,而这时 To 区域是空的。这两个区域交替变换身份(下一次GC,现在的To区域就变为From区域)。2个Survivor区是对称的,没有先后关系。而且,因为需要交换的关系,Survivor区至少有一个是空的。特殊的情况下,根据程序需要,Survivor区是可以配置为多个的(多于2个),这样可以增加对象在新生代中的存活时间,减少被放到老年代的可能。
- 老年代(Old)
那些从新生代存活下来的对象,以及一些直接分配在老年代的大对象。在新生代中历经了N次(可配置化)垃圾回收后仍然存活的对象,就会被复制到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
- 持久代(Permanent)
主要存放静态类型数据,如类或者方法的描述对象以及类本身。存在于Method Area 和 Run-Time Constant Pool。Class在被Load的时候被放入PermGen区域。
持久代对垃圾回收没有显著影响。但是有些应用可能动态生成或调用一些Class,例如CGLib,在这种时候往往需要设置一个比较大的持久代空间来存放这些运行过程中动态增加的类型。
在GC收集的时候,按收集频率分为:频繁收集生命周期短的区域(Young area);较少收集生命周期较长的区域(Old area);很少收集或基本不收集的区域(Perm area)。
当一组对象生成时,内存申请过程如下:
- JVM会试图为相关Java对象在新生代的Eden区中初始化一块内存区域;
- 当Eden内存区域没有足够的空间进行分配时,虚拟机将触发一次 Minor GC(Young GC)。Minor GC期间虚拟机将Eden区存活的对象移动到其中一块Survivor区域。
- Survivor区被用来作为Eden区及老年代的中间交换区域。一般情况下,Survivor区中存活了一定GC次数的对象会被移动到老年代。
- 当老年代内存区域不够时,JVM会在这两个区域{ Old + New }进行完全的垃圾回收(Full GC)。
- Full GC后,如果Survivor区及老年代仍然无法存放从Eden区复制过来的对象,则导致JVM无法在Eden区为新生对象申请内存,即出现“Out of Memory”。
Garbage Collection时有哪些类型?
- Minor GC(Minor collection)
定义:指发生在新生代的垃圾收集动作。因为新生的对象大多都具有朝生夕灭的特性,所以Minor GC非常频繁,回收速度也比较快。
触发条件:当新生代内存区域不够的时候,会被触发。
回收区域:只对新生代进行回收。
Minor GC 日志 :
- Full GC(major collection)
定义:指发生在老年代中的GC。出现Major GC后,经常会伴随至少一次的 Minor GC(但非绝对,在Parallel收集器的收集策略里就有直接进行Major GC的策略选择)。Major GC的速度一般会比Minor GC慢10倍以上。
触发条件:老年代区域不够的时候,会被触发。
回收区域:对整个堆进行垃圾收集。
Full GC 日志:
- 疑问:Minor GC后,Eden是空的吗?
是的,Minor GC会把Eden中的所有存活的对象都移动到Survivor区域中去,如果Survivor区中放不下,那么剩下的存活的对象就被移到Old中。
5. 内存异常
- 栈溢出
表现为:java.lang.StackOverflowError,出现这个错误的原因一般都是调用层次太深,或者无限的递归造成的。
JVM提供了一个参数来让我们调节运行时栈空间的大小。-XX:Xss=n表示栈空间最大为n,建议不要对此参数进行调节。
- 内存溢出
Out of Memory 内存溢出(OOM),该异常一般主要有如下2种原因:
- 老年代溢出,表现为:java.lang.OutOfMemoryError:Java heap space
最常见的情况,产生的原因可能是:设置的内存参数Xmx过小或程序的内存泄露及使用不当问题。例如循环上万次的字符串处理、创建上千万个对象、在一段代码内申请上百M甚至上G的内存。
有的时候虽然不会报内存溢出,却会使系统不间断的垃圾回收,也无法处理其它请求。这种情况下除了检查程序、打印堆内存等方法排查,还要借助一些内存分析工具,比如MAT。 - 持久代溢出,表现为:java.lang.OutOfMemoryError:PermGen space
通常由于持久代设置过小,动态加载了大量Java类而导致溢出,解决的办法可以将参数 -XX:MaxPermSize 调大一点(一般256m能满足绝大多数应用程序需求)。将部分Java类放到容器共享区(例如Tomcat share lib)去加载的办法也可以是一个思路,但前提是,这些Java类是容器里部署的多个应用之间的共享类库。
二. 串行GC
1. Serial收集器
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。
Serial收集器是最基本、历史最悠久 ①的收集器。它是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
Serial收集器到JDK1.7为止,它依然是JVM Client模式下默认的新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程相比)。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。
-
新生代Serial收集器
1)特点:
–仅仅使用单线程进行内存回收;
–它是独占式的内存回收 ;
–进行内存回收时, 暂停所有的工作线程(“Stop-The-World”) ;
–使用复制算法;
–适合CPU等硬件一般的场合;
2)设置参数:
-XX:+UseSerialGC 指定使用新生代Serial和老年代SerialOld。
2. SerialOld收集器
SerialOld收集器是Serial收集器的老年代版本,它同样也是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给JVM Client模式下的老年代收集器。如果在Server模式下,那么它主要有两大用途:
- 在JDK 1.5及之前的版本中与Parallel收集器搭配使用;
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure(回收时有对象要分配)时使用。
-
老年代SerialOld收集器
1)特点:
–同新生代Serial收集器一样,单线程、独占式的垃圾收集器;
–使用“标记-整理”算法;
–通常老年代内存回收比新生代内存回收要更长时间,所以可能会使应用程序停顿较长时间;
2)设置参数:
-XX:+UseSerialGC 新生代、老年代都使用串行GC;
-XX:+UseParNeGC 新生代使用ParNew,老年代使用SerialOld;
-XX:+UseParallelGC 新生代使用Parallel,老年代使用SerialOld。
三. 并行GC(吞吐量优先)
1. ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了多条线程收集之外,其余行为包括Serial收集器可用的设置参数、收集算法、Safepoint、对象分配规则、回收策略等都与Serial收集器完全一样,并没有太多特别之处。但它却是运行在JVM Service模式下首选的新生代收集器,其中一个很重要的原因是:除了Serial收集器外,目前只有它能于CMS收集器(并发GC)配合工作。
ParNew收集器在单CPU环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程切换的开销,ParNew收集器在两个CPU环境中都不能100%保证优于Serial收集器。随着可以使用的CPU数量的增加,它对于GC时系统资源的有效利用还是有利的。
-
新生代ParNew收集器
1)特点:
–Serial的多线程版本;
–使用复制算法 ;
–垃圾回收时,应用程序仍会暂停,只不过由于是多线程回收,在多核CPU上,回收效率会高于串行GC。反之在单核CPU,效率会不如串行GC;
2)设置参数:
-XX:+UseParNewGC 新生代使用ParNew,老年代使用SerialOld;
-XX:+UseConcMarkSweepGC 新生代使用ParNew,老年代使用CMS;
-XX:ParallelGCThreads=n 指定ParNew收集器工作时的收集线程数,当CPU核数小于8时,默认开启的线程数等于CPU数量,当高于8时,可使用公式:3+((5*CPU_count)/8) 。
2. Parallel收集器
Parallel收集器是一个新生代收集器,也是使用复制算法又是并行的多线程收集器。
Parallel收集器的特点是它的关注点与其他收集器不同,Parallel收集器的目标并不是尽可能地缩短垃圾收集时用户线程的停顿时间,而是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
垃圾收集时停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,适合后台运算而不需要太多交互的任务。由于与吞吐量关系密切,Parallel收集器也经常被称为“吞吐量优先”的收集器。
Parallel收集器支持“GC自适应的调节策略(GC Ergonomics)”,这也是Parallel收集器与ParNew收集器的一个重要区别。设置参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,当这个参数打开后,不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或者最大的吞吐量,这种调节方式称为“GC自适应的调节策略”。你也可以在使用Parallel收集器自适应调节策略时,把基本的内存数据设置好(如-Xmx 最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,具体细节参数的调节任务就交由虚拟机去完成。
-
新生代Parallel收集器
1)特点:
–同ParNew回收器一样, 不同的地方在于,它非常关注系统的吞吐量(通过参数控制) ;
–使用复制算法;
–支持自适应的GC调节策略;2)设置参数:
-XX:+UseParallelGC 新生代使用Parallel,老年代使用SerialOld;
-XX:+UseParallelOldGC 新生代使用Parallel,老年代使用ParallelOld;
-XX:MaxGCPauseMillis=n 设置内存回收的最大停顿时间,单位ms ②;
-XX:GCTimeRatio=n 设置吞吐量的大小,假设值为n(在0-100之间),那么系统将花费不超过1/(n+1)的时间用于内存回收。默认值为99,就是允许最大1%的垃圾收集时间 ③;
-XX:+UseAdaptiveSizePolicy 自适应GC策略的开关参数。
3. ParallelOld收集器
ParallelOld是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
这个收集器是在JDK 1.6中才开始提供,在此之前,如果新生代选择了Parallel收集器,老年代除了SerialOld收集器外别无选择。由于老年代SerialOld收集器在服务端应用性能上的“拖累”,使用Parallel收集器未必能在整体应用上获得吞吐量最大化的效果,而单线程的老年代收集无法充分利用服务器多核CPU的处理能力,在老年代很大且硬件较高级的环境中,这种组合的吞吐量甚至还没有ParNew加CMS的组合“给力”。直到ParallelOld收集器出现,“吞吐量优先”收集器终于有了名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel加ParallelOld收集器。
-
老年代ParallelOld收集器
1)特点:
–关注吞吐量的老年代并发收集器;
–使用“标记-整理”算法;
2)设置参数:
-XX:+UseParallelOldGC 新生代使用Parallel,老年代使用ParallelOld,是非常关注系统吞吐量的收集器组合,适合用于对吞吐量要求较高的系统 。
四. 并发GC(响应时间优先)
1. CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器适用于目前大部分集中在互联网站或B/S系统服务端上的Java应用,这类应用尤其重视服务响应速度,系统停顿时间越短,用户体验越好。
CMS收集器基于“标记—清除”算法实现,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为:
- 初始标记(CMS initial mark);
- 并发标记(CMS concurrent mark);
- 重新标记(CMS remark);
- 并发清除(CMS concurrent sweep);
其中,初始标记、重新标记这两个步骤仍然需要“Stop-The-World”,是独占的,不能与用户线程一起执行,而其它阶段则可以与用户线程一起执行。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是进行GC Roots Tracing的过程;重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程,收集线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS是一款优秀的收集器,它的主要优点例如“并发收集、低停顿 ④”,但是它也有以下3个明显的缺点:
- CMS收集器对CPU资源非常敏感。
其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(比方说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,内存回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,这让人无法接受。
为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep / i-CMS)的CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,同时尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,也就是速度下降没有那么明显。但实践证明,增量时的CMS收集器效果很一般,在目前版本中,i-CMS已经被声明为“deprecated”,即不再提倡用户使用。 - CMS收集器无法处理浮动垃圾(Floating Garbage)
这可能会出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS当然无法在当前收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是很快,可适当调高参数值 -XX:CMSInitiatingOccupancyFraction 来提高触发百分比,以便降低内存回收次数而获取更好的性能。在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。但是,如果CMS在运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数 -XX:CMSInitiatingOccupancyFraction 设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。 - CMS收集器带来的空间碎片问题
CMS基于“标记—清除”算法实现,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发下一次Full GC。为了解决这个问题,CMS收集器提供了一个开关参数(-XX:+UseCMSCompactAtFullCollection 默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程。内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
-
老年代的CMS收集器
1)特点:
–非独占式的老年代并发收集器,大部分时候应用程序不会停止运行;
–使用“标记-清除”算法,因此回收后会有内存碎片,可设置参数进行内存碎片的压缩整理 ;
–与Parallel和ParallelOld不同,CMS主要关注系统停顿时间;
2)设置参数:
-XX:-CMSPrecleaningEnabled 关闭预清理,默认在并发标记后,会有一个预清理的操作,可减少停顿时间;
-XX:+UseConcMarkSweepGC 老年代使用CMS,新生代使用ParNew;
-XX:ConcGCThreads=n 设置并发线程数;
-XX:ParallelCMSThreads=n 同上,设置并发线程数;
-XX:CMSInitiatingOccupancyFraction=n 指定老年代回收阀值,即当老年代内存使用率达到该值时执行一次CMS垃圾回收,默认值为68。设置技巧: (Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100)>=Xmn ;
-XX:+UseCMSCompactAtFullCollection 开启内存碎片整理,即当CMS垃圾回收完成后执行一次内存碎片整理。需要注意的是,内存碎片的整理并不是并发进行的,因此可能会引起程序停顿;
-XX:CMSFullGCsBeforeCompation=n 用于指定进行多少次CMS垃圾回收后, 再进行一次内存压缩;
-XX:+CMSParallelRemarkEnabled 在使用UseParNewGC参数的情况下,尽量减少 mark(标记)的时间;
-XX:+UseCMSInitiatingOccupancyOnly 表示只有达到阀值时才进行CMS垃圾回收
五. G1收集器
G1(Garbage-First)收集器是当前SUN公司收集器技术发展以来最新的GC收集器,早在2009年刚刚确立JDK 1.7 RoadMap以来,它就被视为JDK 1.7中HotSpot虚拟机的一个重要进化特征。它是一款面向服务端应用的垃圾收集器。HotSpot赋予它的使命是在未来可以替换掉JDK 1.5中发布的CMS收集器。
与其他GC收集器相比,G1具备有以下特点:
- 并行与并发
充分利用多CPU、多核环境下的硬件优势,使用多个CPU(或CPU核心)来缩短GC停顿的时间,部分其他收集器原本需要停顿用户线程执行的GC动作,G1收集器仍然可以通过并发的方式让用户线程继续执行。 - 分代收集
分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。 - 空间整合
与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这都意味着G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。 - 可预测的停顿
这是G1相对于CMS的一大优势。降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是RTSJ(Java 实时规范)的垃圾收集器的特征了。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。因此,G1收集器可以有计划地避免在Java堆中进行全区域的垃圾收集,它跟踪各个Region里面堆积的垃圾的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region ⑤。这种使用Region划分内存空间以及有优先级的区域内存回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才有,只是在G1中更加突出而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如果回收新生代时也不得不同时扫描老年代的话,那么Minor GC的效率可能下降不少。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier(写屏障)暂时中断写操作,然后检查Reference引用的对象是否处于不同的Region之中。如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可分为下面几个步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
你可以发现G1的前几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短;并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行;而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行;最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
G1收集器的运作步骤中并发和需要停顿的阶段:
G1收集器的运行步骤:
- 新生代GC
- 标记周期:
初始标记新生代GC(此时是并行, 应用程序会暂时停止)–>根区域扫描–>并发标记–>重新标记(此时是并行, 应用程序会暂时停止)–>独占清理(此时应用程序会暂时停止)–>并发清理。 - 混合回收:
这个阶段即会执行正常的新生代GC,也会选取一些被标记的老年代区域进行回收,同时处理新生代和老年代。 - 若需要, 会进行FullGC:
–混合GC时发生空间不足;
–在新生代GC时,Survivor区和老年代无法容纳幸存对象时;
–以上两者都会导致一次FullGC产生。
由于目前G1成熟版本的发布时间还很短,G1收集器几乎可以说还没有经过实际应用的考验,网络上关于G1收集器的性能测试也非常贫乏。强调“生产环境下的测试报告”是因为对于垃圾收集器来说,仅仅通过简单的Java代码写个Microbenchmark程序来创建、移除Java对象,再用-XX:+PrintGCDetails等参数来查看GC日志是很难做到准确衡量其性能的。关于这方面,我们引用一段在StackOverflow.com上的经验与读者分享:“我在一个真实的、较大规模的应用程序中使用过G1:大约分配有60~70GB内存,存活对象大约在20~50GB之间。服务器运行Linux操作系统,JDK版本为6u22。G1与PS/PS Old相比,最大的好处是停顿时间更加可控、可预测,如果我在PS中设置一个很低的最大允许GC时间,譬如期望50毫秒内完成GC(-XX:MaxGCPauseMillis=50),但在65GB的Java堆下有可能得到的直接结果是一次长达30秒至2分钟的漫长的Stop-The-World过程;而G1与CMS相比,虽然它们都立足于低停顿时间,CMS仍然是我现在的选择,但是随着Oracle对G1 的持续改进,我相信G1会是最终的胜利者。如果你现在采用的收集器没有出现问题,那就没有任何理由现在去选择G1,如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处”。
1)特点:
–独特的垃圾回收策略,属于分代垃圾回收器;
–使用分区算法,不要求Eden,年轻代或老年代的空间都连续;
–并行性:回收期间,可由多个线程同时工作,有效利用多核CPU资源;
–并发性:与应用程序可交替执行,部分工作可以和应用程序同时执行;
–分代GC:分代收集器;同时兼顾新生代和老年代;
–空间整理:回收过程中,会进行适当对象移动,减少空间碎片;
–可预见性:G1可选取部分区域进行回收,可缩小回收范围,减少全局停顿;
2)设置参数:
-XX:+UseG1GC 打开G1收集器开关;
-XX:MaxGCPauseMillis=n 指定目标的最大停顿时间,任何一次停顿时间超过这个值,G1就会尝试调整新生代和老年代的比例,调整堆大小,调整晋升年龄;
-XX:ParallelGCThreads=n 用于设置并行回收时,GC的工作线程数量
-XX:InitiatingHeapOccpancyPercent=n 指定整个堆的使用率达到多少时,执行一次并发标记周期,默认45。过大会导致并发标记周期迟迟不能启动,增加FullGC的可能,过小会导致GC频繁,会导致应用程序性能有所下降。
小结:
JDK1.7中的各种垃圾收集器到此已全部介绍完毕,现在我们简单的做个小结:
- 垃圾收集器参数
参数 | 描述 |
---|---|
-XX:+UseSerialGC | Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
-XX:+UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 |
-XX:+UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。 |
-XX:+UseParallelGC | Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 |
-XX:+UseParallelOldGC | 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 |
-XX:SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1 |
-XX:PretenureSizeThreshold | 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代 |
-XX:UseAdaptiveSizePolicy | 动态调整java堆中各个区域的大小以及进入老年代的年龄 |
-XX:+HandlePromotionFailure | 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 |
-XX:ParallelGCThreads | 设置并行GC进行内存回收的线程数 |
-XX:GCTimeRatio | GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 |
-XX:MaxGCPauseMillis | 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70 |
-XX:+UseCMSCompactAtFullCollection | 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效 |
-XX:+CMSFullGCBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用 |
-XX:+UseFastAccessorMethods | 原始类型优化 |
-XX:+DisableExplicitGC | 是否关闭手动System.gc |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m |
- Client、Server模式下默认的GC
新生代GC方式 | 老年代和持久代GC方式 | |
---|---|---|
Client | Serial 串行GC | Serial Old 串行GC |
Server | Parallel Scavenge 并行回收GC | Parallel Old 并行GC |
- HotSpot GC组合方式
新生代GC方式 | 老年代和持久代GC方式 | |
---|---|---|
-XX:+UseSerialGC | Serial 串行GC | Serial Old 串行GC |
-XX:+UseParallelGC | Parallel Scavenge 并行回收GC | Serial Old 并行GC |
-XX:+UseConcMarkSweepGC | ParNew 并行GC | CMS 并发GC 当出现“Concurrent Mode Failure”时 采用Serial Old 串行GC |
-XX:+UseParNewGC | ParNew 并行GC | Serial Old 串行GC |
-XX:+UseParallelOldGC | Parallel Scavenge 并行回收GC | Parallel Old 并行GC |
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC | Serial 串行GC | CMS 并发GC 当出现“Concurrent Mode Failure”时 采用Serial Old 串行GC |
六. JVM参数说明
-
关于JVM参数
- 标准参数(-):所有JVM都必须支持这些参数的功能,并且向后兼容。例如-server和-client;
- 非标准参数(-X):JVM默认实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容;
- 非稳定参数(-XX):各个JVM实现会有所不同,将来可能会不被支持,需要慎重使用。
参数形式通常是这样的:
-XX:+<option> 启用选项
-XX:-<option> 不启用选项
-XX:<option>=<number>
-XX:<option>=<string>
-
堆设置
-Xmxnn :最大堆大小
-Xmsnn :初始堆大小。此值可以设置与-Xmx相同,减少GC。
-Xmnn :设置新生代大小。整个Heap=新生 + 老年代,所以如果增大新生代,将会减小老年代。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xssnn :设置每个线程栈的大小。JDK1.5以后默认每个线程栈大小为1M,以前每个线程栈大小为256K。该参数决定了Java函数调用的深度,若值太小则容易导致栈溢出错误(StackOverflowError) ,所以应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不是无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。
-XX:NewSize=n :设置新生代初始值
-XX:MaxNewSize=n :设置新生代最大值
-XX:NewRatio=n :设置新生代(包括1个Eden和2个Survivor区)和老年代的比值。例如:3,表示年轻代与年老代比值为1:3
-XX:SurvivorRatio=n :新生代中Eden区与两个Survivor区的比值。例如:3,表示Eden:Survivor=3:2
-XX:PermSize=n :设置持久代初始值,默认是物理内存的1/64。在Java8以后永久区被移除,代之的是元数据区, 由-XX:MetaspaceSize指定
-XX:MaxPermSize=n :设置持久代最大值,默认是物理内存的1/4。在Java8中,由-XX:MaxMetaspaceSize指定元数据区的大小。
-
收集器设置
略,见上文
-
调试信息设置
-verbose:gc :表示输出虚拟机中GC的详细情况
-Xloggc :指定gc以文件输出(默认在控制台), 后面跟日志文件路径
-XX:-CITime :打印消耗在JIT编译的时间
-XX:ErrorFile=./hs_err_pid.log :保存错误日志或数据到指定文件中
-XX:HeapDumpPath=./java_pid.hprof :指定Dump堆内存时的路径
-XX:-HeapDumpOnOutOfMemoryError :当首次遭遇内存溢出时Dump出此时的堆内存
-XX:OnError=";" :出现致命ERROR后运行自定义命令
-XX:OnOutOfMemoryError=";" :当首次遭遇内存溢出时执行自定义命令
-XX:-PrintClassHistogram :按下 Ctrl+Break 后打印堆内存中类实例的柱状信息,同JDK的 jmap -histo 命令
-XX:-PrintConcurrentLocks :按下 Ctrl+Break 后打印线程栈中并发锁的相关信息,同JDK的 jstack -l 命令
-XX:-PrintCompilation :当一个方法被编译时打印相关信息
-XX:+PrintGC :打印GC日志
-XX:+PrintGCDetails :打印GC日志详细
-XX:+PrintHeapAtGC :打印GC的前后Heap日志
-XX:+PrintGCTimeStamps :打印每次GC的时间戳
-XX:+PrintGCApplicationConcurrentTime :打印应用程序的执行时间
-XX:+PrintGCApplicationStoppedTime :可以打印应用程序由于GC而产生的停顿时间
-XX:+PrintTenuringDistribution :打印GC年龄信息等
-XX:+PrintReferenceGC :跟踪系统内的对象引用和Finallize队列
-XX:+TraceClassLoading :跟踪类的加载信息
-XX:-TraceClassLoadingPreorder :跟踪被引用到的所有类的加载信息
-XX:-TraceClassResolution :跟踪常量池
-XX:-TraceClassUnloading :跟踪类的卸载信息
-
性能设置
1、System.gc()
1)禁用System.gc()
-XX:+DisableExplicitGC 禁止程序代码中显示调用System.gc() ⑧。若加了此参数,程序若有调用,返回的空函数调用。
2)System.gc()使用并发回收
-XX:+ExplicitGCCinvokesConcurrent 使用并发方式处理显示的gc,开启后,System.gc()这种显示GC才会并发的回收(CMS、G1)。
2、并行GC前额外触发新生代GC
1)使用并行收集器器(-XX:+UseParallelGC或者-XX:+UseParallelOldGC)时,会额外先触发一个新生代GC,目的是尽可能减少停顿时间。
2)若不需要这种特性,可以使用以下参数去除
-XX:-ScavengeBeforeFullGC 去除在FullGC之前的那次新生代GC,默认值为true
3、对象何时进入老年代
1)长期存活的对象将进入老年代
-XX:MaxTenuringThreshold=n 假设值为n,则新生代的对象最多经历n次GC,就能晋升老年代,但这个必不是晋升的必要条件
-XX:TargetSurvivorRatio=n 用于设置Survivor区的目标使用率,即当Survivor区GC后使用率超过这个值,就可能会使用较小的年龄作为晋升年龄。默认为50虚拟机采用分代收集的思想管理内存,那内存回收时就必须能识别那些对象该放到新生代,那些该到老年代中。为了做到这点,虚拟机为每个对象定义了一个对象年龄(Age,由GC的次数决定)。每经过一次新生代GC后仍然存活,将对象的Age增加1岁,当年龄到一定程度(默认为15)时,将会被晋升到老年代中。对象晋升老年代的年龄限定值,可通过以下来设置:
2)如果将 -XX:MaxTenuringThreshold 参数设置为0的话,则新生代对象不经过Survivor区,直接进入老年代,对于需要大量常驻内存的应用,这样做可以提高效率;如果将此值设置为一个较大值,则新生代对象会在Survivor区进行多次复制,这样做可以增加对象在新生代的存活时间,增加对象在新生代被垃圾回收的概率,减少Full GC的频率,可以在某种程度上提高服务稳定性。
3)适龄对象也可能进入老年代
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
4)大对象直接进入老年代
-XX:PretenureSizeThreshold 即对象的大小大于此值,就会绕过新生代,直接在老年代分配(即所谓“大对象直接进入老年代”)。
除年龄外,对象体积也会影响对象的晋升的。若对象体积太大,新生代无法容纳这个对象,则这个对象可能就会直接晋升至老年代。特别是,如果程序中经常出现“短命的大对象”,容易发生内存还有不少空间却不得不提前触发Full GC以获取足够的连续空间,导致GC效率低下。可通过以下参数使用对象直接晋升至老年代的阈值,单位是byte
PretenureSizeThreshold 参数的意义在于,若遇上述情况时,能避免在 Eden 及两个 Survivor 之间发生大量的内存复制。此参数只对串行GC以及ParNew有效,而Parallel并不认识这个参数。Parallel收集器一般并不需要特别设置。如果遇到必须使用此参数的场合,也可以考虑 ParNew加CMS的收集器组合。5)空间分配担保
-XX:+HandlePromotionFailure 老年代分配担保(true或false)
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间:
- 条件成立,那么 Minor GC 可以确保是安全的;
- 条件不成立,则虚拟机会查看HandlePromotionFailure 设置值是否允许担保失败:
- 如果允许,那么会继续坚持老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小:
- 如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有“风险”的;
- 如果小于,那这时要进行一次 Full GC;
- 如果不允许,那这时要进行一次 Full GC。
为什么会“冒险”?新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份。因此,当出现大量对象在Minor GC后仍存活的情况(最极端的情况就是内存回收后新生代中所有对象都还活着!),就需要老年代进行这样的分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会活下来在实际完成GC之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,拿这个与老年代剩余的空间进行比较,决策出是否进行Full GC来让老年代腾出更多的内存空间。
其实,取平均值进行比较仍然是一种动态概率的手段,也就是说,如果某次 Minor GC 存活后的对象突增(存在一个峰值,生活中类似双十一时的交易量和物流压力),远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了 HandlePromotionFailure 失败,那就只好在失败后重新发起一次 Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下老年代的担保都还是有效的,还是会将 HandlePromotionFailure 开关打开,避免Full GC过于频繁。
JDK 6 Update 24 之后,HandlePromotionFailure 参数将不会再影响到虚拟机的空间分配担保策略,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC。
4、在TLAB上分配对象
TLAB(Thread Local Allocation Buffer,线程本地分配缓存)是一个线程专用的内存分配区域,虚拟机为线程分配空间,针对于体积不大的对象,会优先使用TLAB,这个可以加速对象的分配。TLAB默认开启,若是要关闭可以使用以下参数:
-XX:-UseTLAB 关闭TLAB
-XX:+UseTLAB 开启TLAB,
-XX:+PrintTLAB 观察TALB的使用情况
-XX:TLABRefillWasteFraction=n 设置一个比率n,而refill_waste的值就是(TLAB_SIZE/n),即TLAB空间较小时,大对象无法分配在TLAB,所以会直接分配到堆上。TLAB较小时很容易装满,因此当TLAB的空间不够分配一个新对象,就会考虑是否废弃当前TLAB空间直接分配到堆上,就会使用此参数进行判断,小于refill_waste就允许废弃,新建TLAB来分配对象,而大于refill_waste就直接在堆上分配。默认值是64。
-XX:+ResizeTLAB 开启TLAB自动调整大小,默认开启,若是要关闭把+号换成-号即可
-XX:TLABSize=n 设置一个TLAB的大小,前提先关闭TLAB的自动调整
5、其它相关性能设置
-server和-client :略
-XX:+DoEscapeAnalysis :开启逃逸分析 ⑥
-XX:+EliminateAllocations :开启标量替换 ⑦,默认开启
-XX:+UseCompressedOops :开启指针压缩
-XX:+AggressiveOpts :启用JVM最新的调优成果,例如编译优化、启用偏向锁、并行老年代收集等
-XX:+UseBiasedLocking :启用偏向锁,默认开启
-XX:+UseThreadPriorities :启用本地线程优先级API。即使 java.lang.Thread.setPriority()
生效,不启用则无效
-XX:SoftRefLRUPolicyMSPerMB=n :软引用对象在最后一次被访问后能存活n毫秒(默认为1000)
6、疑问:-Xmn,-XX:NewSize/-XX:MaxNewSize,-XX:NewRatio 3组参数都可以影响新生代的大小,混合使用的情况下,优先级是什么呢?
优先级如下:
- 高优先级:-XX:NewSize/-XX:MaxNewSize
- 中优先级:-Xmn(默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=n)
- 低优先级:-XX:NewRatio
推荐使用-Xmn参数,因为是这个参数最简洁,相当于一次设定了 NewSize/MaxNewSIze,而且默认等效,适用于生产环境。-Xmn 配合 -Xms/-Xmx,即可将堆内存布局完成。
注:
① 曾经在JDK 1.3.1之前是虚拟机新生代收集的唯一选择。
② 大家不要认为如果把MaxGCPauseMillis参数值设置得小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:如果系统把新生代调小一些,那么收集更小的新生代肯定会比原来的快一些,这也直接导致垃圾收集发生得更频繁,比如原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
③ GCTimeRatio参数,设置的是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
④ Sun公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause Collector)。
⑤ 这也是Garbage-First名称的来由。
⑥ 逃逸分析的目的,是判断对象的作用域是否可能逃逸出函数体。逃逸分析是栈上分配的技术基础,对于非逃逸对象而言就是一个局部变量,而对象未发生逃逸时,虚拟机就有可能进行栈上分配,不是堆上, 栈上分配速度快,并且能避免垃圾回收带来的负面影响。栈上分配是虚拟机提供的很好的对象分配优化策略。
⑦ 允许对象打散分配在栈上,即对象的属性视为独立局部变量进行分配到栈上。
⑧ System.gc()的调用,会使用FullGC的方式回收整个堆而忽略CMS或G1等相关收集器
参考:
- 《深入理解Java虚拟机》;
- 《JVM高级特性与最佳实践》 ;
- 《实战Java虚拟机—JVM故障诊断与性能优化》:
- 参考链接:http://blog.youkuaiyun.com/java2000_wl/article/details/8030172