垃圾收集器入门
最主流的四个垃圾收集器
Serial收集器(常用于单CPU环境)
Throughput(或Parallel)收集器
Concurrent收集器(CMS)
G1收集器
垃圾收集器概述
所有应用线程都停止运行所产生的停顿被称为时空停顿(stop-the-world)。
垃圾收集由两步构成:查找不再使用的对象,以及释放这些对象所管理的内存。
分代垃圾收集器
根据情况将堆划分成不同的代(Generation)。这些代被称为“老年代”(Old Generation或Tenured Generation)和新生代(Young Generation)。新生代又进一步划分为不同的区段,分别称为Eden空间和Survivor空间
新生代是堆得一部分,对象首先在新生代中分配。新生代填满时,垃圾收集器会暂停所有的应用线程,回收新生代空间。不再使用的对象会被回收,仍然在使用的对象会被移动到其他地方。这种操作被称为Minor GC.
所有的垃圾收集算法在对新生代进行垃圾回收时都存在"时空停顿”现象。
简单的垃圾收集算法直接停掉所有的应用线程,找出不再使用的对象,对其进行回收,接着对堆空间进行整理。这个过程被称为Full GC,通常导致应用程序线程长时间的停顿。
为批量应用选择垃圾收集器
1.如果CPU足够强劲,使用Concurrent收集器避免发生Full GC停顿可以让任务运行得更快
2.如果CPU有限,那么Concurrent收集器额外的CPU消耗会让批处理任务消耗更多的时间
快速小结
1.所有的GC算法都将堆划分成了老年代和新生代
2.所有的GC算法在清理新生代对象时,都使用了"时空停顿"方式的垃圾收集方法,通常这是一个能较快完成的操作。
GC算法
JVM提供了以下4种
1.Serial垃圾收集器
Serial收集器使用单线程清理堆得内容。
无论Minor GC还是Full GC,清理堆空间时,所有的应用线程都会被暂停。
启用:-XX:+UseSerialGC
在Serial收集器作为默认收集器的系统上,如果需要关闭Serial收集器,可以通过制定另一种收集器来实现。
2.Throughput垃圾收集器
Server级虚拟机的默认收集器
使用多线程回收新生代和老年代空间
常被称为Parallel收集器
在Minor GC和Full GC时会暂停所有的应用线程
使用-XX:+UseParallelGC、-XX:+UseParallelOldGC启用
CMS收集器
设计的初衷是为了消除Throughput收集器和Serial收集器Full GC周期中的长时间停顿。
使用新算法收集新生代对象(-XX:+UseParNewGC)
Full GC时不再暂停应用线程,而是使用若干个后台线程定期地堆老年代空间进行扫描,即时回收其中不再使用的对象
应用线程只在Minor GC以及后台线程扫描老年代时发生及其短暂的停顿。
通过-XX:+UseConcMarkSweepGC、-XX:+UseParNewGC标志(默认情况下这两个标志都是禁用的)可以启用CMS垃圾收集器
G1垃圾收集器
设计初衷是尽量缩短处理超大堆(大于4GB)时产生的停顿。
G1收集算法将堆划分为若干个区域(Region),不过它依旧属于分代收集器。这些区域中的一部分包含新生代,新生代的垃圾收集仍然采用暂停所有应用线程的方式,将存活对象移动到老年代或者Survivor空间,这些操作都使用多线程的方式完成。
通过标志-XX:+UseG1GC(默认值是关闭的)可以启动G1垃圾收集器。
强制进行GC
执行 jcmd GC.run
jconsole连接到JVM在内存面板上单击“进行GC”按钮
可以通过参数-XX:+DisableExplicitGC显式地禁止System.gc()这种类型的GC;默认情况下该标志是关闭的
快速小结
1.四种垃圾收集算法采用了不同方法缓解GC对应用程序的影响
2.Serial收集器常用于仅有单CPU可用以及当前其他程序会干扰GC的情况(通常是默认值)
3.Throughput收集器在其他的虚拟机上市默认值,它能最大化应用程序的总吞吐量,但是有些操作可能遭遇较长的停顿。
4.CMS收集器能够在应用线程运行的同时并行地对老年代的垃圾进行收集。如果CPU的计算能力足以支撑后台垃圾收集线程的运行,该算法能避免应用程序发生Full GC。
5.G1收集器也能在应用线程运行的同时并发地对老年代的垃圾进行收集,在某种程度上能够减少发生Ful GC的风险。G1的设计理念使得它比CMS更不容易遭遇Full GC。
选择GC算法
一方面取决于应用程序的特征,另一方面取决于应用的性能目标。
1.GC算法及批量任务
监控系统中定义的由CPU使用率触发的规则尤其重要:你需要确保100%的CPU使用率不是由Full GC所引起的临时性CPU暴涨,或者是由于后台并行处理线程所引起的持续时间更长(不过使用率稍低)的CPU高峰。在Java程序的世界里,这些峰值都是正常的状况。
快速小结
1.使用Throughput收集器处理应用程序线程的批量任务能最大程度地利用CPU的处理能力,通常能获得更好的性能。
2.如果批量任务并没有使用机器上所有可用的CPU资源,那么切换到Concurrent收集器往往能取得更好的性能。
2.GC算法和吞吐量测试
存在空闲CPU周期时,CMS收集器的性能更好。
可用的CPU周期无法支撑后台的CMS收集线程运行,所以CMS收集器发生了并发模式失效(Concurrent Mode Failure)。发生这种失效意味着JVM不得不退化到单线程的Full GC模式,所以那段时间内平均CPU的使用率骤降。
3.GC算法及响应时间测试
快速小结
1.衡量标准是响应时间或吞吐量,在Throughput收集器和Concurrent收集器之间做选择的依据主要是有多少空闲CPU资源能用于运行后台的并发线程。
2.通常情况下,Throughput收集器的平均响应时间比Concurrent收集器要差,但是在90%响应时间或99%响应时间这几项指标上,Throughput收集器比Concurrent收集器要好一些。
3.使用Throughput收集器或超负荷地进行大量Full GC时,切换到Concurrent收集器通常能获得更低的响应时间。
CMS收集器和G1收集器之间的抉择
一般情况下,堆空间小于4GB时,CMS收集器的性能比G1收集器好。
使用大型堆或巨型堆时,由于G1收集器可以分割工作,通常它比CMS收集器表现更好。
快速小结
1.选择Concurrent收集器时,如果堆较小,推荐使用CMS收集器
G1的设计使得它能够在不同的分区(Region)处理堆,因此它的扩展性更好,比CMS更易于处理超大堆的情况。
GC调优基础
1.调整堆的大小
调整堆大小的首要原则就是永远不要将堆的容量设置得比机器的物理内存还大。如果机器上运行着多个JVM实例,则这个原则适用于所有堆的总和
堆的大小由2个参数值控制:分别是初始值(通过-Xms N设置)和最大值(通过-Xmx N设置)
快速小结
1.JVM会根据其运行的机器,尝试估算合适的最大、最小堆的大小
2.除非应用程序需要比默认值更大的堆,否则在进行调优时,尽量考虑通过调整GC算法的性能目标,而非微调堆的大小来改善程序性能。
2.代空间的调整
所有用于调整代空间的命令行标志调整的都是新生代空间;新生代空间剩下的所有空间都被老年代占用。
多个标志都能用于新生代空间的调整
-XX:NewRatio=N 设置新生代与老年代的空间占用比率(默认值为2)
-XX:NewSize=N 设置新生代空间的初始大小
-XX:MaxNewSize=N 设置新生代空间的最大大小
-Xmn N 将NewSize和MaxNewSize设置为同一个值的快捷方法
Initial Young Gen Size=Initial Heap Size/(1+NewRatio)
快速小结
1.整个堆范围内,不同代的大小划分是由新生代所占用的空间控制的。
2.新生代的大小会随着整个堆大小的增大而增长,但这也是随着整个堆的空间比率波动变化的(依据新生代的初始值和最大值)。
3.永久代和元空间的调整
JVM载入类的时候,需要记录这些类的元数据,这部分数据被保存在一个单独的对空间中。
Java7里,被称为永久代(Permgen或Permanent Generation)
Java8中,被称为原空间(Metaspace)
永久代和元空间内保存的信息只对编译器或者JVM的运行时有用,这部分信息被称为“类的元数据”
使用元空间替换掉永久代的优势之一是我们不再需要对其进行调整
对于永久代而言,可以通过-XX:PermSize=N -XX:MaxPermSize=N调整大小
对于元空间,可以通过-XX:MetaspaceSize=N 和-XX:MaxMetaspaceSize=N调整大小
如果程序在启动时发生了大量的Full GC(因为需要载入数量巨大的类),通常都是由于永久代或元空间发生了大小调整,因此这种情况下为了改善启动速度,可以增大初始值。
使用jmap和-permstat参数(适用于Java7)或-clstats参数(适用于Java8)可以输出类加载相关的信息。
4.控制并发
除Serial收集器外的几乎所有收集器使用的算法都基于多线程。启动的线程数由-XX:ParallelGCThreads=N参数控制
ParallelGCThreads=8+((N-8)*5/8) N代表CPU的数目
快速小结
1.几乎所有的垃圾收集算法中基本的垃圾回收线程数都依据机器上的CPU数目计算得出
2.多个JVM运行于同一台2物理机上时,根据公式计算出的线程数可能过高,必须进行优化(减少)
5.自适应调整
使用-XX:-UseAdaptiveSizePolicy标志可以在全局范围内关闭自适应调整功能(默认情况下,这个标志是开启的)
如果你想了解应用程序运行时JVM的空间是如何调整的,可以设置-XX:+PrintAdaptiveSizePolicy标志。开启该标志后,一旦发生垃圾回收,GC的日志中会包含垃圾回收时不同代进行空间调整的细节信息
快速小结
1.JVM在堆的内部如何调整新生代及老年代的百分比是由自适应调整机制控制的
2.通常情况下,我们应该开启自适应调整,因为垃圾回收算法依赖于调整后的代的大小来达到它停顿时间的性能目标。
3.对于已经精细调优过的堆,关闭自适应调整能获得一定的性能提升。
垃圾回收工具
观察垃圾回收对应用程序的性能影响最好的方法就是尽量熟悉垃圾回收的日志,垃圾回收日志中包含了程序运行过程中每一次垃圾回收操作。
多种方法都能开启GC的日志功能
使用-verbose:gc或-XX:+PrintGC这两个标志中的任意一个能创建基本的GC日志(这两个日志实际上互为别名,默认情况下GC日志功能是关闭的)
使用-XX:+PrintGCDetails标志会创建更详细的GC日志
使用-XX:+PrintGCTimestamps或者-XX:+PrintGCDateStamps便于我们更精确地判断几次GC操作之间的时间。
使用-Xloggc:filename标志也能修改输出GC日志到某个文件。除非显式地使用-XX:+PrintGCDetails标志,否则使用-Xloggc会自动开启基本日志模式
通过-XX:+UseGCLogfileRotation -XX:NumberOfGCLogfiles=N -XX:GCLogfileSize=N标志可以控制日志文件的循环
GC Histogram(http://java.net/projects/gchisto)能够读入GC日志,根据日志文件中的数据生成对应的图表和表格。
jstat提供了9个选项,提供堆的各种数据;使用jstat -options选项能够列出所有的选项。最常用的一项是-gcutil,它能够输出消耗在GC上的时间,以及每个GC区域使用的百分比。其他的选择能够以KB为代为输出各GC空间的大小。
快速小结
1.GC日志是分析GC相关问题的重要线索;我们应该开启GC日志标志(即便是在生产服务器上)
2.使用PrintDetails标志能获得更详尽的GC日志信息
3.使用工具能很有效地帮助我们解析和理解GC日志的内容,尤其是在对GC中的数据归纳汇总时,它们非常有帮助
4.使用jstat能够动态地观察运行程序的垃圾回收操作。
垃圾收集算法
理解Throughput收集器
通常新生代的垃圾回收发生在Eden空间快用尽时。新生代的垃圾收集会把Eden空间中的所有对象挪走:一部分对象会被移动到Survivor空间,其他的会被移动到老年代。
老年代垃圾收集会回收新生代中的所有对象(包括Survivor空间中的对象)。只有哪些活跃引用的对象,或者已经经过压缩整理的对象(它们占据了老年代的开始部分)会在老年代中继续保持,其余的对象都会被回收
快速小结
1.Throughput收集器会进行两种操作,分别是Minor GC和Full GC
2.通过GC日志中的时间输出,我们可以迅速地判断出Throughput收集器的GC操作对应用程序的总体性能影响
堆大小的自适应调整和静态调整
Throughput收集器的自动调优几乎都是围绕停顿时间进行的,寻求堆的总体大小、新生代的大小以及老年代大小之间的平衡。
-XX:MaxGCPauseMillis=N 标志用于设定应用可承受的最大停顿时间。这个标志设定的值同时影响Minor GC和Full GC
-XX:GCTimeRatio=N 可以设置你希望应用程序在垃圾回收上花费多少时间(与应用程序的运行时间相比较)。
快速小结
1.采用动态调整是进行堆调优极好的入手点。对很多的应用程序而言,采用动态调整就已经足够,动态调整的配置能够有效地减少JVM的内存使用
2.静态地设置堆的大小也可能获得最优的性能。设置合理的性能目标,让JVM根据设置确定堆的大小是学习这种调优很好的入门课程。
理解CMS收集器
CMS收集器有3中基本的操作
1.会对新生代的对象进行回收(所有的应用线程都会被暂停)
2.会启动一个并发的线程对老年代空间的垃圾进行回收
如果有必要,CMS会发起Full GC
JVM会依据堆的使用情况启动并发回收。当堆的占用达到某个程度时,JVM会启动后台线程扫描堆,回收不用的对象。
如果使用CMS回收器,老年代空间不会压缩整理
并发回收
并发回收由“初始标记”阶段开始,这个阶段会暂停所有的应用程序线程
下一个阶段是“标记阶段”,这个阶段中应用程序可以持续运行,不会被中断。
然后是“预清理”阶段,这个阶段也是与应用程序线程的运行并发进行的
接下来是“重新标记”阶段,这个阶段涵盖了多个操作
使用可中断预清理阶段是由于标记阶段不是并发的,所有的应用线程进入标记阶段后都会被暂停
使用可中断预清理阶段的目的就是希望尽量缩短停顿的长度,避免连续的停顿。
接下来是另一个并发阶段——清除(sweep)阶段
接下来是并发重置(concurrent reset)阶段
并发回收可能出现的消息
1.并发模式失效(concurrent mode failure)
新生代发生垃圾回收,同时老年代又没有足够的空间容纳晋升的对象时,CMS垃圾回收就会退化成Full GC
2.promotion failed
老年代有足够的空间可以容纳晋升的对象,但是由于空闲空间的碎片化,导致晋升失败
快速小结
1.CMS垃圾回收有多个操作,但是期望的操作是Minor GC和并发回收(concurrent cycle)
2.CMS收集过程中的并发模式失败以及晋升失败的代价都非常昂贵
3.默认情况下CMS不会对永久代进行垃圾回收
针对并发模式失效的调优
CMS收集器使用两个配置 MaxGCPauseMllis=N和GCTimeRadio=N来确定使用多大的堆和多大的代空间
CMS收集器与其他垃圾收集方法一个显著的不同是除非发生Full GC,否则CMS的新生代大小不会调整
1.给后台线程更多的运行机会
-XX:CMSInitiatingOccupancyFraction=N和-XX:+UseCMSInitiatingOccupancyOnly 同时使用能帮助CMS更容易进行决策
2.调整CMS后台线程
每个CMS后台进程都会100%地占用机器上的一颗CPU。如果应用程序发生并发模式失效,同时又有额外的CPU周期可用,可以设置-XX:ConcGCThreads=N标志,增加后台线程的数目。默认情况下,ConGCThreads的值是根据ParallelGCThreads标志的值计算得到的: ConcGCThreads=(3+ParallelGCThreads)/4
快速小结
1.避免发生并发模式失效是提升CMS收集器处理能力、获得高性能的关键
2.避免并发模式失效(如果有可能的话)最简单的方法是增大堆的容量
3.否则,我们能进行的下一个步骤就是通过调整CMSInitiatingOccupancyFraction参数,尽早启动并发后台线程的运行
4.另外,调整后台线程的数目对解决这个问题也有帮助
CMS收集器的永久代调优
默认情况下,Java7中的CMS垃圾收集线程不会处理永久代中的垃圾,如果永久代空间用尽,CMS会发起一次Full GC来回收其中的垃圾对象。除此之外,还可以开启-XX:+CMSPerGenSweepingEnabled标志(默认情况下,该标志的值为false),开启后,永久代中的垃圾使用与老年代同样的方式进行垃圾收集。
使用-XX:CMSInitiatingPermOccupancyFraction=N参数可以指定CMS收集器在永久代空间占用比达到设定值时启动永久代垃圾回收线程,这个参数默认值为80%
为了真正释放不再被引用的类,还需要设置-XX:+CMSClassUnloadingEnable标志,否则,即使启用了永久代垃圾回收也只能释放少量的无效对象,累的元数据并不会释放。
Java8中,CMS收集器默认就会收集元空间中不再载入的类。如果由于某些原因,你希望关闭这一功能,可以通过-XX:-CMSClassUnloadingEnable标志进行关闭(默认情况下这个标志是开启的,即该值为true)
增量式CMS垃圾收集
增量式的CMS垃圾收集在Java8中已经不推荐使用
如果系统确实只配备了极其有限的CPU,作为替代方案,可以考虑使用G1收集器——因为G1收集器的后台线程在垃圾收集的过程中也会周期性地暂停,客观上减少了与应用线程竞争CPU资源的情况。
指定-XX:+CMSIncrementalMode标示可以开启增量式CMS垃圾收集。通过改变标志-XX:CMSIncrementalSafetyFactor=N -XX:CMSIncrementalDutyCycleMin=N和-XX:CMSIncrementalPacing可以控制垃圾收集后台线程为应用程序线程让出多少CPU周期
增量式CMS垃圾收集依据责任周期(duty cycle)原则进行工作 ,这个原则决定了CMS垃圾收集器的后台线程在释放CPU周期给应用线程之前,每隔多长时间扫描一次堆
责任周期的时间长度是以新生代相邻两次垃圾收集之间的时间长度计算得出的;默认情况下,增量式CMS垃圾收集持续的时间是该时长的20%左右
快速小结
1.应用在CPU资源受限的机器上运行,同时又要求较小的停顿,这时使用增量式CMS收集器是一个不错的选择
2.通过责任周期可以调整增量式CMS收集器;增加责任周期的运行时间可以避免CMS收集器发生并发模式失效
理解G1垃圾收集器
G1垃圾收集器是一种工作在堆内不同分区上的并发收集器。分区(region)既可以归属于老年代,也可以归属于新生代(默认情况下,一个堆被划分为2048个分区);同一个代的分区不需要保持连续
这种只专注于垃圾最多分区的方式就是G1垃圾收集器名称的由来,即首先收集垃圾最多的分区
新生代进行垃圾回收时,整个新生代空间要么被回收,要么被晋升
G1垃圾收集器的收集活动主要包括4种操作
新生代垃圾收集
后台收集,并发周期
并发周期的第一阶段是初始—标记(initial-mark)阶段。这个阶段会暂停所有应用线程——部分源于初始—标记阶段也会进行新生代垃圾收集
接下来,G1收集器会扫描根分区(root region)
根分区扫描完成后,G1收集器就进入到并发标记阶段
紧接在标记阶段之后的是重新标记(remarking)阶段和正常的清理阶段
紧接着是一个额外的并发清理阶段
混合式垃圾收集
混合式垃圾回收周期会持续运行直到(几乎)所有标记的分区都被回收,这之后G1收集器会恢复常规的新生代垃圾回收周期
必要时的Full GC
并发模式失效
发生这种失败意味着堆的大小应该增加了,或者G1收集器的后台处理应该更早开始,或者需要调整后期,让它运行的更快(譬如,增加后台处理的线程数)
晋升失败
这种失败通常意味着混合式收集需要更迅速地完成垃圾收集;每次新生代垃圾收集需要处理更多老年代的分区
疏散失败
进行新生代垃圾收集时,Survivor空间和老年代中没有足够的空间容纳所有的幸存对象
解决这个问题最简单的方式是增加堆的大小
巨型对象分配失败
快速小结
1.G1垃圾收集器包括多个周期(以及并发周期内的阶段)。调优良好的JVM运行G1收集器时应该只经历新生代周期、混合式周期和并发GC周期。
2.G1的并发阶段会产生少量的停顿
3.恰当的时候,我们需要对G1进行调优,才能避免Full GC周期的发生
G1垃圾收集器调优
G1垃圾收集器调优的主要目标是避免发生并发模式失败或者疏散失败,一旦发生这些失败就会导致Full GC
避免发生Full GC
通过增加总的堆空间大小或者调整老年代、新生代之间的比例来增加老年代空间的大小
增加后台线程的数目(假设我们有足够的CPU资源运行这些线程)
以更高的频率进行G1的后台垃圾收集活动
在混合式来及回收周期中完成更多的垃圾收集工作
G1收集器最主要的调优只通过一个标志进行:-XX:MaxGCPauseMillis=N(默认值为200)
如果G1收集器发生时空停顿的时长超过该值,G1收集器就会尝试各种方式进行弥补——譬如调整新生代与老年代的比例,调整堆得大小,更早地启用后台处理,改变晋升阈值,或者是在混合式垃圾收集周期中处理更多或者更少的老年代分区(这是最重要的方式)
调整G1垃圾收集的后台线程数
对于应用线程暂停运行的周期,可以使用ParallelGCThreads标志设置运行的线程数
对于并发运行阶段可以使用ConcGCThreads标志设置运行的线程数
调整G1垃圾收集器运行的频率
G1垃圾收集周期通常在堆得占用达到参数-XX:InitatingHeapOccupancyPercent=N设定的比率时启动,默认情况下该参数的值为45
调整G1垃圾收集器的混合式垃圾收集周期
混合式垃圾收集要处理的工作量取决于三个因素
第一个因素是有多少分区被发现大部分是垃圾对象
第二个因素是G1垃圾收集回收分区时的最大混合式GC周期数,通过参数-XX:G1MixedGCCountTatget=N可以进行调节,默认值为8;减少该参数值可以帮助解决晋升失败的问题(代价是混合式GC周期的停顿时间会更长);另一方面,如果混合式GC的停顿时间过长,可以增大这个参数的值,减少每次混合式GC周期的工作量
第三个影响因素是GC停顿可忍受的最大时长(通过MaxGCPauseMillis参数确定);增大MaxGCPauseMillis能在每次混合式GC中收集更多的老年代分区,而这反过来又能帮助G1收集器在更早的时候启动并发周期
快速小结
1.作为G1收集器调优的第一步,首先应该设定一个合理的停顿时间作为目标。
2.如果使用这个设置后,还是频繁发生Full GC,并且堆的大小没有扩大的可能,这是就需要针对特定的失败采用特定的方法进行调优
a.通过InitiatingHeapOccpancyPercent标志可以调整G1收集器,更频繁地启动后台垃圾收集线程
b.如果有充足的CPU资源,可以考虑调整ConcGCThreads标志,增加垃圾收集线程数
c.减少G1MixedGCCountTarget参数可以避免晋升失败
高级调优
晋升及Survivor空间
两种情况下对象会被移动到老年代
Survivor空间的大小实在太小。新生代垃圾收集时,如果目标Survivor空间被填满,Eden空间剩下的活跃对象会直接进入老年代。
对象在Survivor空间中经历的GC周期数有个上限,超过这个上限的对象也会被移动到老年代。这个上限值被称为晋升阈值(Tenuring Threshold)
Survivor空间的初始大小由-XX:InitialSruvivorRatio=N标志决定
survivor_space_size=new_size/(initial_surivor_ratio+2)
默认为8
Survivor空间的上限由-XX:MinSurvivorRatio=N设置
maximum_survivor_space_size=new_size/(min_survivor_ratio+2)
默认为3
可以使用SurvivorRatio参数将Survivor空间的大小设置为固定值,同时关闭UseAdaptiveSizePolicy标志
通过标志-XX:TargetSurvivorRatio=N可以设置Survivor空间中垃圾回收之后的空闲比率
通过-XX:InitialTenuringThreshold=N标志可以设置初始的晋升阈值
最大晋升阈值由-XX:MaxTenuringThreshold=N标志设定
可以使用-XX:+AlwaysTenure标志(默认为false)使得对象直接晋升到老年代,不会再存放于Survivor空间
-XX:+NeverTenure(默认值也是false),开启后只要Survivor空间有容量,就不会有对象被晋升到老年代
使用-XX:+PrintTenuringDistribution标志可以在GC日志中增加晋升的统计信息
快速小结
1.设计Survivor空间的初衷是为了让对象(尤其是已经分配的对象)在新生代停留更多的GC周期。这个设计增大了对象晋升到老年代之前被回收释放的几率
2.如果Survivor空间过小,对象会直接晋升到老年代,从而触发更多的老年代GC。
3.解决这个问题的最好方法是增大堆的大小(或者至少增加新生代),让JVM来处理Survivor空间的回收
4.有的情况下,我们需要避免对象晋升到老年代,调整晋升阈值或者Survivor空间的大小可以避免对象晋升到老年代
分配大对象
线程本地分配缓存区(Thread Local Allocation Buffer,TLAB)
TLAB
默认情况下TLAB就是开启的,JVM管理着它们的大小以及如何使用
默认情况下,TLAB的大小由三个因素决定:应用程序的线程数、Eden空间的大小以及线程的分配率
使用-XX:-UseTLAB可以关闭TLAB
对于开源版本的JVM(不附带JFR),要监控TLAB的分配情况,最好的途径就是在命令行中添加-XX:+PrintTLAB标志。
调整TLAB的大小
使用-XX:TLABSize=N标志可以显式地制定TLAB的大小(默认为0)
使用-XX:-ResizeTLAB标志可以避免每次GC时都调整TLAB的大小
TLAB空间调整生效时,其容量的最小值可以使用-XX:MinTLABSize=N参数设置(默认为2KB)
快速小结
对需要分配大量大型对象的应用,TLAB空间的调整就变得必不可少
巨型对象(Humongous Objects)
对TLAB空间中无法分配的对象,JVM会尽量尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中分配空间
G1分区的大小
G1收集器将堆划分成了一定数量的分区,每个分区的大小都是固定的
分区的大小最小是1MB,最大不能超过32MB
G1分区的大小可以通过-XX:G1HeapRegionSize=N标志设置
使用G1收集器分配巨型对象
增大G1分区的大小,让其能够在一个分区内分配应用需要的所有对象能够提升G1收集器的效率
快速小结
1.G1分区的大小是2的幂,最小值为1MB
2.如果堆的初始大小跟最大值相差很大,这种堆会有大量的G1分区,在这种情况下,应该增大G1分区的大小。
3.如果要分配的对象超过了G1收集器分区容量的一般,对于这种应用程序,我们应该增大G1分区的容量,让G1分区能更好地适配这些对象。遵循这个原则,应用程序分配对象的大小至少应是512KB(因为G1分区的最小值为1MB)
AggressiveHeap标志
使用AggressiveHeap标志能方便地设置各种命令参数。这个标志只适用于64位的JVM
PLAB的大小
PLAB的全称是晋升本地分配缓冲(Promotion-Local Allocation Buffer),是垃圾回收清理代数据时基于线程分配的分区
编译策略
JVM发布时配备了多种JIT编译算法
关闭Full GC之前的新生代垃圾收集
将ScavengeBeforeFullGC标志设置为false意味着Full GC发生时,JVM不会对Full GC之前的新生代垃圾进行收集
将GC线程绑定到特定的CPU
快速小结
1.AggressiveHeap是个历史悠久的调优标志,设计初衷是为了在强大的机器上运行单一JVM时调整堆的各种参数。
2.这个标志设定的值并没有随着JVM技术的发展同步调整,因此它的有效性从长远来看是值得质疑的
全盘掌控堆空间的大小
堆的默认大小依据机器的内存配置确定,不过也可以通过参数-XX:MaxRAM=N设置
堆的最大容量是MaxRAM值的四分之一
如果机器的物理内存比MaxRAM的值小,默认堆的大小就是物理内存的1/4。但是相反的规则并不适用,即使机器配置了数百GB的内存,JVM能使用的最大堆容量也不会超过默认值32GB,及128GB的1/4.
最大堆的计算实际采用下面的公式 Default Xmx=MaxRAM/MaxRAMFraction
默认最大堆的大小也可以通过-XX:MaxRAMFraction=N标志值进行调整,MaxRAMFraction的默认值为4
-XX:ErgoHeapSizeLimit=N 默认值为0,否则,如果该标志设置的值比MaxRAM/MaxRAMFraction还小,就是用该参数设置的值
-XX:MinRAMFraction=N参数的默认值为2
堆的初始大小计算采用:Default Xms=MaxRAM/InitialRAMFraction
InitialRAMFraction默认值为64
指定的InitialRAMFraction小于-XX:OldSize=N的参数设定(该参数默认为4MB),堆的初始大小等于新生代和老年代大小之和
快速小结
1.大多数机器上堆的初始空间和最大空间默认值计算是比较直观的
2.达到堆大小的临界情况时,需要考虑的因素更多,计算也更加复杂
小结
堆内存的最佳实践
堆分析
堆直方图
堆直方图可使用命令 %jcmd jvm进程id GC.class_histogram 该命令的输出中仅包含活跃对象
在堆直方图中,Klass相关的对象往往会接近顶端,它们是加载类得到的元数据对象。
%jmap -histo process_id 该命令的输出中包含被回收的对象
%jmap -histo:live process_id 会在看到直方图之前强制执行一次Full GC
堆转储
用命令行生成堆转储
%jcmd process_id GC.heap_dump /path/to/head_dump.hprof
%jmap -dump:live,file=/path/to/heap_dump.hprof process_id
在jmap中包含live选项,这会在堆被转储之前强制执行一次Full GC;jcmd默认就会这么做,如果因为某些原因,你希望包含其他对象(即死对象),可以在jcmd命令的最后加上-all
打开堆转储文件
jhat
最原始的分析工具。它会读取堆转储文件,并运行一个小型的HTTP服务器,该服务器允许你通过一系列网页链接查看堆转储信息
jvisualvm
jvisualvm的监视(Monitor)选项卡可以从一个运行中的程序获得堆转储文件,可以打开之前生成的堆转储文件。
mat
开源的EclipseLink内存分析工具(EclipseLink Memory Analyzer Tool,mat)可以加载一个或多个堆转储文件并执行分析。它可以生成报告,向我们建议可能存在问题的地方;也可以用于浏览堆,并对堆实行类SQL的查询
一个对象的保留内存,是指回收该对象可以释放出的内存量
一个对象的浅(shallow)大小,是指该对象本身的大小。如果该对象包含一个指向另一个对象的引用,4字节或8字节的引用会计算在内,但是目标对象的大小不会包含进来。深(deep)对象则包含那些对象的大小,深大小与保留大小的区别在于那些存在共享的对象。
GC根是一些系统对象,其中保存着一些(通过一个较长的由其他对象组成的链条)指向问题中对象的静态和全局引用
快速小结
1.了解哪些对象正在消耗内存,是了解要优化代码中哪些对象的第一步
2.对于识别由创建了太多某一特定类型对象所引发的内存问题,直方图这一方法快速且方便
3.堆转储分析是追踪内存使用最强大的技术,不过要利用好,则需要一些耐心和努力
内存溢出错误
在下列情况下,JVM会抛出内存溢出错误(OutOfMemoryError)
JVM没有原生内存可用
永久代(在Java7和更早版本中)或元空间(在Java8中)内存不足
如果你正在编写的应用会创建并丢弃大量类加载器,一定要非常谨慎,确保类加载器本身能正确丢弃
Java堆本身内存不足——对于给定的堆空间而言,应用中活跃对象太多
如果应用存在内存泄漏,可以间隔几分钟,获得连续的一些堆转储文件,然后加以比较,mat内置了这一功能:如果打开了两个堆转储文件,mat有一个选项用来计算两个堆中的直方图之间的差别
JVM执行GC耗时太多
自动堆转储
-XX:+HeapDumpOnOutOfMemoryError
该标志默认为false,打开该标志,JVM会在抛出OutOfMemoryError时创建堆转储
-XX:HeapDumpPath=
该标志制定了堆转储将被写入的位置;默认会在应用的当前工作目录下生成java_pid.hprof文件,这里的路径可以指定目录(使用默认文件名),也可以指定要生成的实际文件的名字
-XX:+HeapDumpAfterFullGC
这会在运行一次Full GC之后生成一个堆转储文件
-XX:+HeapDumpBeforeFullGC
这会在运行一次Full GC之前生成一个堆转储文件
快速小结
1.有多种原因会导致抛出OutOfMemoryError,因此不要假设堆空间就是问题所在
2.对于永久代和普通的堆,内存泄漏时出现OutOfMemoryError时最常见的原因;堆分析工具可以帮助我们找到泄漏的根源
减少内存使用
减少对象大小
两种方式
1.减少实例变量的个数
2.减少实例变量的大小
对象对齐与对象大小
为了使对象大小是8字节的整数倍(对齐),总是会有填充操作。
JVM也会填充字节数不规则的对象,这样不管底层架构最适合什么样的地址边界,对象的数组都能优雅地适应
尽早清理
通过将变量的值设为null,实现尽早清理,从而使问题中的对象可以更快地被垃圾回收器回收
如果一个长期存活的类会缓存以及丢弃对象引用,那一定要仔细处理,以避免过时引用。否则,显式地将一个对象引用设置为null在性能方面基本没什么好处
延迟初始化
延迟初始化运行时性能
检查要进行延迟初始化的变量是不是已经被初始化了,未必总会有性能损失
快速小结
只有当常用的代码路径不会初始化某个变量时,才去考虑延迟初始化该变量
一般不会在线程安全的代码上引入延迟初始化,否则会加重现有的同步成本
对于使用了线程安全对象的代码,如果要采用延迟初始化,应该使用双重检查锁。
不可变对象和标准化对象
像这类不可变对象的单一化表示,就被称为对象的标准化(canonical)版本
要标准化某个对象,创建一个Map来保存该对象的标准化版本。为防止内存泄漏,务必保证用弱引用处理Map中的对象。
快速小结
1.不可变对象为标准化(canonicalization)这种特殊的生命周期管理提供了可能性。
2.通过标准化去掉不可变对象的冗余副本,可以极大减少应用消耗的堆内存
字符串的保留
String类提供了自己的标准化方法:intern()方法
保留字符串的表是保存在原生内存中的,它是一个大小固定的Hashtable
大小固定的Hashtable
从概念上讲,一个Hashtable包含一个数组,它会保存一些条目(数组中的每个元素叫作一个桶)。当要将一个对象保存到Hashtable中时,可以用该对象的哈希值堆桶的数目取余,以此确定对象在数组中的存储位置。这种情况下,两个哈希值不同的对象很有可能被映射到同一个桶中,每个桶实际就是一个链表,其中按顺序存储了映射到该桶的条目。当两个对象映射到一个桶时,这就叫“冲突”
从Java7中开始,这个表的大小可以在JVM启动时使用-XX:StringTableSize=N(如前面所介绍的,默认值为1009或60013)。如果某个应用会保留大量字符串,就应该增加这个值。如果这个值是个素数,字符串保留表的效率最高。
如果想看看字符串表的执行过程,可以使用-XX:PrintStringTableStatistics参数(这个标志要求JDK7u6或更新版本,默认为false)运行应用
某个应用中已经分配的保留字符串个数(及其总大小),可以使用如下的jmap命令获得
%jmap -heap process_id
快速小结
1.如果应用中有大量字符串是一样的,那通过保留实现字符串重用收效很大。
2.要保留很多字符串的应用可能需要调整字符串保留表的大小
对象生命周期管理
对象重用
对象重用通常有两种实现方式:对象池和线程局部变量
JDK提供了一些常见的对象池:线程池和软引用。软引用本质上是一大池可重用对象。同时Java EE依赖对象池来连接数据库和其他资源,而EJB的整个生命周期都是围绕对象池的概念构建的。
对象池
线程池的大小可能很难正确地设置,他们讲对象管理的负担又抛给程序员了;程序员不能简单地将对象丢出作用域,而必须记得将其返还到对象池中。
线程局部变量
生命周期管理
线程局部变量要比池中管理对象更容易,成本更低
线程局部对象在线程内总是可用的,不需要显式地归还
基数性(Cardinality)
线程局部变量通常会伴生线程数与保存的可重用对象数之间的一一对应关系。
同步
线程局部变量不需要同步,因为它们只能用于一个线程之内;而且线程局部的get()方法相当快
快速小结
1.对象重用通常是一种通用操作,我们并不鼓励使用它,但是这种技术可能适合初始化成本高昂,而且数量比较少的一组对象。
2.在使用对象池还是使用线程局部变量这两种技术之间,应该有所取舍。一般而言,假设线程和可重用对象直接存在一一对应关系,则线程局部变量更容易使用
弱引用、软引用、其他引用
术语说明
引用(Reference)
引用(或者说对象引用)可以是任何类型的引用:强引用、弱引用、软引用等。指向一个对象的普通引用实例变量就是一个强引用。
非确定引用(Indefinite reference)
本书使用这个术语来区分强引用和其他特殊引用(比如软引用或弱引用)。一个非确定引用其实是一个对象实例(比如,SoftReference类的一个实例)
所引对象
非确定引用的工作方式是,在非确定引用类的实例内,嵌入另一个引用(几乎总是嵌入一个强引用)。被封装的对象称作“所引对象”
与对象池或线程局部变量相比,非确定引用的优势在于,它们最终会被垃圾回收器回收。
非确定引用的缺点是堆垃圾收集器的效率会有轻微影响。
非确定引用和其他任何对象一样:它们也消耗内存,而且其他变量也是通过强引用引用它们
垃圾收集器要回收非确定引用,至少需要两个GC周期
GC日志与引用处理
当运行一个使用了大量非确定引用的对象时,可以考虑添加-XX:+PrintReferenceGC标志(默认为false)。这样就能看懂处理这些引用花了多少时间
1.软引用
如果问题中的对象以后有很大机会重用,可以使用软引用,但是如果该对象近期一直没有使用到(计算时也会考虑堆还有多少内存可用),垃圾收集器会回收它。
软引用本质上是一个比较大的、最近最久未用(LRU)的对象池。获得较好性能的关键是确保它们会被及时清理
如果对象的数目不是特别大,软引用就会工作得很好。否则,就要考虑用更传统的、固定大小的对象池来实现一个LRU缓存。
2.弱引用
当问题中的所引对象会同时被几个线程使用时,应该考虑弱引用。否则,弱引用很可能会被垃圾收集器回收;只有弱引用的对象在每个GC周期都可以回收。
当强引用被移除时,弱引用会立即释放
引用对象就和其他Java对象一样:在年轻代中创建,最终会被提升到老年代。如果弱引用本身仍在年轻代中,而弱引用的所引对象被释放了,则弱引用可以快速释放(下一次Minor GC时)
非确定引用集合
JDK提供了两个保存非确定引用的集合类:WeakHashMap和WeakIdentityMap
3.终结器(Finalizer)和最终引用(Final Reference)
每个Java类都有一个从Object类继承而来的finalize()方法;在对象可以被来及收集器回收时,可以用这个方法来清理数据。
终结器实际上是非确定引用的一种特殊情况:JVM使用了一个私有的引用类(java.lang.ref.finalize,它又是java.lang.ref.finalReference的子类)来记录finalize()方法的对象。当一个具有finalize()方法的对象被分配时,JVM会分配两个对象:一个是该对象本身,另一个是一个以该对象为所引对象的finalizer引用
通常,如果使用终结器是不可避免的,那么一定要确保尽量减少该对象访问的内存。
对于使用终结器,还有一种替代方案,至少可以避免部分问题。特别是,这种方案支持在正常的GC操作期间释放所引对象。这是通过使用另一种非确定引用实现的,而非隐式地使用finalizer引用。有时推荐使用另一种非确定引用类型:PhantomReference(虚引用)类
终结器队列
终结器队列是一个引用队列,用于当索引对象可以被GC回收时处理Finalizer引用
可以通过如下命令让JVM处理终结器队列: %jcmd process_id GC.run_finalization
要监控Finalizer队列的命令如下: %jmap -finalizerinfo process_id
快速小结
1.非确定引用(包括软引用、弱引用、虚引用和最终引用)会改变Java对象正常的生命周期,与池或对象局部变量相比,它可以以对GC更友好的方式实现对象重用。
2.当应用对某个对象感兴趣,而且该对象在应用中的其他地方有强引用时,才应该使用弱引用
3.软引用保存可能长期存在的对象,提供了一个简单的、对GC友好的LRU缓存
4.非确定引用自身会消耗内存,而且会长时间抓住其他对象的内存,应该谨慎使用