Garbage Collector(GC)自动管理应用程序的动态内存分配请求
垃圾收集器通过以下操作执行自动动态内存管理:
- 从操作系统申请内存并将内存返回给操作系统
- 在应用程序请求时将内存分发给应用程序
- 确定应用程序仍在使用该内存的哪些部分
- 回收未使用的内存以供应用程序重用
Java HotSpot垃圾收集器采用各种技术来改进这些操作的效率:
- 将分代清理(generational scavenging)与年龄(aging)结合使用,将精力集中在堆中最可能包含大量可回收内存的区域
- 使用多个线程并行操作,或者在后台与应用程序并发执行需要长运行时间的操作
- 尝试通过整理(compacting, 压实)存活的对象来恢复更大的连续内存
虚拟机内程序计数器、虚拟机栈、本地方法栈3个运行时数据区是线程私有的内存区域,内存分配回收都比较确定,随着方法和线程的结束,回收内存,而堆和方法区是线程共享的存储区域,内存的分配和回收是动态的,是GC处理的核心
Java 11 GC Tuning Guide 文档:
Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide, Release 11
Java 9 和11中GC,堆大小,及时编译的默认设置:
- 使用Garbage-First (G1) collector (Java 7 和 8 默认是 Parallel GC)
- GC 线程的最大值受堆大小和可获得的CPU资源限定
- 初始堆大小为物理内存的 1/64
- 堆的最大值默认是物理内存的 1/4
- 分层编译Tiered compiler, 同时使用 C1 and C2编译器
基于行为的调优
在行为上,GC主要有两个不同的目标,一个是控制GC的最长停顿时间,使得响应更加及时,另一个方面是控制吞吐量,使得GC时间占比较小,Java HotSpot VM GC 可以配置为优先满足这两个目标之一,如果满足首选目标,GC会尝试最大化另一个目标,因为堆内存大小的限制,以及其他的配置的影响,也存在GC不能满足目标的可能
最大暂停时间 Maximum Pause-Time
暂停时间是垃圾收集器停止应用并回收不再使用的内存空间的持续时间,-XX:MaxGCPauseMillis=<n>
用来限制暂停的最长时间,不同GC的默认值不同,调整得过小可能导致垃圾收集更频繁地发生,从而降低应用程序的整体吞吐量
GC会记录平均停顿时间和相对平均值的偏差,如果平均值加上暂停时间的偏差大于目标值,则目标停顿时间未满足
吞吐量 Throughput
吞吐量是根据GC所花费和在垃圾收集之外花费的时间application time来衡量的,由-XX:GCTimeRatio=nnn
指定。垃圾收集时间与应用时间的比率为1/ (1+nnn)
,例如,-XX:GCTimeRatio=19
设置垃圾收集为总时间的1/20或5%。
垃圾收集所花费的时间是所有垃圾收集引起的暂停的总时间。如果未满足吞吐量目标,则GC可能会增加堆的大小,以便在两次GC停顿时间之间在应用时间更长
内存占用
如果已满足吞吐量和最大暂停时间的设定,则垃圾收集器会减小堆大小,直到无法满足其中的吞吐量目标。可以使用-Xms=<nnn>
和-Xmx=<mmm>
分别设置GC可以使用的最小和最大堆大小
优化策略
- 除非知道需要的堆空间大于默认最大堆大小(物理内存的四分之一),否则不要为堆设定最大值
- 应用程序行为的改变会导致堆增长或缩小。例如,如果应用以更高的速率分配空间,则堆空间需要增长来保持相同的吞吐量
- 如果堆增长到其最大大小并且未满足吞吐量目标,则最大堆大小对于吞吐量目标而言太小。可以将最大堆大小设置为接近物理内存的值
- 如果可以满足吞吐量目标,但暂停时间过长,设定最大暂停时间,吞吐量可能目标无法满足,因此需要选择一个可接受的折中值
判断对象是否存活
GC回收堆内存空间之前首先要判断对象是否存活
引用计数算法 Reference Counting
每个对象都有一个引用计数器,有引用的地方就加1,引用失效的时候就减1,为0就可以回收
问题:很难解决对象之间的相互循环引用的问题可达性分析算法 Reachability Analysis
如果程序中的任何其他存活对象的引用无法再访问它,对象被认为是垃圾,此时VM可以重用它的内存,通过一系列称为GC Roots
的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链Reference Chain
,当一个对象到GC Roots没有任何引用链相连时,说明对象不可用
GC Roots对象的种类
- 虚拟机栈中引用的对象 Local variables
- 方法区类静态属性或常量引用的对象 Static variables
- 本地方法栈中JNI引用的对象
引用
一个对象的引用,根据引用强度的强弱,分为四种,GC时根据不同的情况,决定是否回收对应类型对象的内存空间
强引用 Strong Reference
默认的引用类型,只要存在强引用,GC就不会回收引用的对象,默认创建都是强引用类型软引用 Soft Reference
当虚拟机内部不足,抛出OutOfMemoryError
之前,GC会清除这些对象的内存,通常用于实现对内存敏感的缓存,内存不足会自动清除
SoftReference<User>softReference=new SoftReference<User>(new User());
-
弱引用 Weak Reference
非必须的对象,当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象
WeakReference<User> weakBuilder = new WeakReference<User>(new User());
-
虚引用 Phantom Reference
无法通过虚引用获取一个对象的实例,get()
方法返回始终为NULL
唯一目的就是在这个对象回收时收到一个系统通知,例如在NIO的DirectByteBuffer
内进行native内存释放的Cleaner
类
package jdk.internal.ref;
public class Cleaner
extends PhantomReference<Object>
-
ReferenceQueue
在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中,内部事先由GC组成一个PendingList,然后通过在Reference
类初始化时,静态块内部创建的ReferenceHandler
线程将其取出加入到引用队列中
引用对象的状态由两个属性表征:
最开始处于Active
状态,当Reference对应的对象可以被垃圾收集器回收时,GC会将该Reference的状态转换为Pending
,经过ReferenceHandler处理后转变为Inactive
clear/enqueue/GC
[active/unregistered] ------
| |
| GC |
| |--> [inactive/unregistered]
v |
[pending/unregistered] ------
ReferenceHandler
如果Reference对象创建时指定了ReferenceQueue
则对应状态为Registered
,GC将其加入到PendingList,ReferenceHandler将其读取出来,加入引用队列后状态转变为Enqueued
,用户线程调用ReferenceQueue.remove()
函数阻塞式获取队列中的对象,来获知某个引用要被回收,这相当于一种通知机制,从队列中取出后状态转变为Dequeued
内部具体实现上,当加入ReferenceQueue
之后,引用对象中的队列信息ReferenceQueue<? super T> queue
会被自动覆盖为NULL,下次GC需要回收时就无法再次加入引用队列,内存空间会被直接回收
clear
[active/registered] -------> [inactive/registered]
| |
| | enqueue [2]
| GC enqueue [2] |
| -----------------|
| |
v |
[pending/registered] --- v
| | ReferenceHandler
| enqueue [2] |---> [inactive/enqueued]
v | |
[pending/enqueued] --- |
| | poll/remove
| poll/remove |
| |
v ReferenceHandler v
[pending/dequeued] ------> [inactive/dequeued]
Reference
时创建时,附加一个引用队列参数
Reference(T referent, ReferenceQueue<? super T> queue)
FinalReference
实现Object的finalize()
方法的类,在创建对象的时,JVM会实例化一个对应的FinalReference
Finalizer
Finalizer是FinalReference的子类,该类被final修饰,不可再被继承,JVM实际操作的是Finalizer类,当满足实例化FinalReference的条件时,JVM会调用Finalizer.register(Object finalizee)
进行注册,对应的引用队列是Finalizer
类内部的静态变量private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
,静态块中的初始化的FinalizerThread
线程从引用队列中取出元素,执行其finalize()
方法
方法区的回收
Permanent generation(方法区)存储描述类和方法的元数据,需要回收两个部分的内存:废弃常量和无用的类
其中无用的类需要同时满足以下三个条件:
- 类的所有实例已经被回收
- 加载该类的ClassLoader 已经被回收
- 该类的
java.lang.Class
对象没有再任何地方被引用,后续不会通过反射的方式访问该类
垃圾收集算法基础
性能考虑
GC主要关注两点,吞吐量和延迟
- 吞吐量是一段长时间周期内不考虑的GC的时间占总时间的百分比,吞吐量包括分配内存所花费的时间(但通常不需要调优内存分配速度)
- 延迟是应用程序的响应能力,GC暂停会影响应用程序的响应能力。
标记 - 清除 Mark-Sweep
首先对所有需要回收的对象进行标记,后来统一回收
这是最基础的收集算法,后续的收集算法都是基于这种思路,并对其不足进行改进而得到的
不足:
- 效率不高
- 空间问题,产生大量不连续的碎片
标记 - 整理 Mark-Compact
让所有存活对象向一端移动,然后直接清理掉端边界以外的内存
分代收集 Generational Collection
理论上,最直接的垃圾收集算法每次运行时遍历每个可到达的对象,任何剩余的对象都被认为是垃圾。这种方法所花费的时间与活动对象的数量成正比,这对于维护大量实时数据的大型应用程序来说是不可行的
Java HotSpot VM包含许多不同的垃圾收集算法,这些算法都使用称为分代收集 generational collection 的技术。分代收集利用几条对大多数应用程序的经验观测属性来最小化回收未使用(垃圾)对象所需的工作。其中最重要属性是是弱分代假说 weak generational hypothesis,它表明大多数对象都只能存活很短的时间
堆区根据对象存活周期的不同将内存分为几块,例如新生代和老年代,区域填满时采用不同的收集算法
最开始,JVM会保留整个Java堆的地址空间,但是只有在需要的时候才分配物理内存
Young Generation
对象填满时,触发 minor collection 回收该区域,时间复杂度与活动对象的数量成比例,通常每次会有部分幸存对象移动到Old Generation
,当它填满时触发 major collection 进行回收,清理整个堆区域,通常所需的时间更长,因为涉及更多的对象
复制算法 Copying
新生代代由Eden
和两个Survivor
空间组成,大多数对象最初都是在Eden
中分配的,在任何时候必然由一个Survivor
空间是空的,在GC期间充当Eden
和另一个Survivor
空间中存活对象的目的地;GC之后,Eden
和源Survivor
空间都是空的。在下一次GC中,两个Survivor
空间的用途互换了。最近被填充的一个作为源Survivor
,将其中的存活对象复制到另一个Survivor
空间。 对象以这种方式在两个Survivor
之间复制,直到它们被复制了一定次数或者没有足够的空间,此时这些对象将被复制到老年代中(这个过程叫做aging)
新生代和老年代的晋升
每个对象在新生代有个年龄值,如果经历一次minor gc,对应的年龄就会增加,达到阈值(-XX:MaxTenuringThreshold=n
设定,n的最大值为15,parallel collector 默认15,CMS 默认为6),即可晋升,移动到老年代
如果Survivor空间中相同年龄所有对象大小之和大于Survivor空间的一半,年龄大于或者等于该年龄的对象可以直接进入年老代,无需达到阈值的要求
术语:
full GC:清理整个Java 堆区
Stop the World Event : JVM上所有的应用都停止,直到gc操作完成,无论哪种gc算法,无论是新生代还是老年代都会产生STW
影响垃圾收集性能的因素
两个最重要的因素是总可用内存和新生代在堆中的比例
堆空间总量 Total Heap
这是影响垃圾收集性能的最重要因素,因为GC在对应分代的内存填满时发生
在初始化虚拟机时,保留堆的整个空间,-Xmx
选项指定保留空间的大小,如果-Xms
参数的值小于-Xmx
参数的值,则不会将所有保留的空间立即交给虚拟机,未提交的空间在图中标记为virtual
,随后堆的不同分区,可以根据需要向虚拟空间增长
新生代比例
新生代越大,发生的minor collection
就越少,但是,堆大小本身是有限的,新生代比例较大意味着老年代比例较小,这将增加major collection
的频率,最佳比例取决于程序内对象的生命周期分布,设定-XX:NewSize=size
,-XX:MaxNewSize=size
,-XX:NewRatio=ratio
其中如果survivor
空间太小,那么复制收集算法会将数据溢出到老年代,如果太大,浪费空间,设定-XX:SurvivorRatio=ratio
垃圾收集器
JDK 9 到 目前(JDK 11),Java HotSpot VM包括三种不同类型的收集器,每种收集器具有不同的性能特征
Serial Collector (-XX:+UseSerialGC)
串行收集器使用单个线程来执行所有垃圾收集工作,这使得它相对有效,因为线程之间没有通信开销,它最适合单处理器机器,因为它无法利用多处理器硬件,也适用于多处理器上的小型应用程序(内存大小为100MB左右)
使用选项-XX:+UseSerialGC
显式启用串行收集器,使用分代收集的原理,新生代使用复制算法,老年代使用标记整理算法
Parallel Collector (-XX:+UseParallelGC)
并行收集器也称为吞吐量收集器throughput collector
,它是类似于串行收集器的分代收集器。串行和并行收集器之间的主要区别在于并行收集器具有多个线程,用于加速垃圾收集。
并行收集器适用于运行在多处理器或多线程硬件上的具有中到大型数据集的应用程序,使用-XX:+UseParallelGC
选项启用,新生代使用parallel scavenge garbage collector
,老年代parallel garbage collector
,如果启动-XX:+UseParallelGC
选项,会默认启动-XX:+UseParallelOldGC
设定,反之亦然
可以使用-XX:ParallelGCThreads=<N>
来控制垃圾收集器线程的数量。因为多个垃圾收集器线程参与minor collection
,在收集期间从新生代晋升(promotion
)到老年代可能产生一些碎片。minor collection
中的每个垃圾收集线程都会保留老年代的一部分用于晋升,将可用空间划分为“晋升缓冲区”会导致碎片效应,减少垃圾收集器线程的数量并增加老年代的大小可以减少这种碎片效应
并行收集器目标的优先级:当同时设定由最大暂停时间目标,吞吐量目标和最小内存目标时,首先满足最大暂停时间目标,之才是吞吐量目标,最后是内存目标
如果超过98%的总时间花在垃圾收集上并且不到2%的堆空间被回收,则抛出OutOfMemoryError
,防止应用程序长时间运行,但由于堆太小进度很慢。也可以通过-XX:-UseGCOverheadLimit
来禁用此功能
ParNew (-XX:+UseParNewGC)/ CMS (-XX:+UseConcMarkSweepGC)
ParNew GC在新生代中使用多线程进行垃圾收集,与用于清理老年代的CMS算法配合使用
当设定-XX:+UseConcMarkSweepGC
时自动使用,在JDK 8中-XX:+UseParNewGC
必须和-XX:+UseConcMarkSweepGC
配合使用,选项组合-XX:+UseConcMarkSweepGC -XX:-UseParNewGC
已经弃用,从JDK 9开始,弃用-XX:+UseParNewGC
选项
Concurrent Collectors
Concurrent Mark Sweep (CMS)和 Garbage-First (G1) 垃圾收集器是两个主要的并发收集器,并发收集器在执行一些昂贵的工作时与应用程序并发运行
- G1垃圾收集器:服务器式收集器,适用于具有大量内存的多处理器计算机。 它有很高的概率满足垃圾收集停顿时间的要求,同时实现高吞吐量,很多参数可以自适应调节,使用简单高效。Java 9 开始默认使用G1,可以使用
-XX:+UseG1GC
显式启用G1 - CMS收集器:适用于需要较短垃圾收集暂停且可以与GC共享处理器资源的应用程序,使用选项
-XX:++UseConcMarkSweepGC
启用CMS收集器,从JDK 9开始,CMS收集器已经被弃用
并发收集器交换处理器资源(否则可供应用程序使用)以缩短主要收集暂停时间。
最明显的开销是在GC的并发部分会使用一个或多个处理器,在N处理器系统上,并发部分使用可用处理器的 K/N,其中1 <= K <= ceiling{N/4}
。除了在并发阶段使用处理器之外,还会产生额外的开销以实现并发。因此,虽然并发收集器的垃圾收集暂停通常要短得多,但应用程序吞吐量也往往略低于其他收集器
在多核的计算机上,应用程序线程在并发部分期间依然可以获得处理器,因此并发垃圾收集器线程不会暂停应用程序,使得暂停时间缩短,但应用程序可用的处理器资源也会减少,并且应该会有一些减速,特别是如果应用程序最大限度地使用所有核。随着N的增加,由于并发垃圾收集导致的处理器资源的减少变得更小,并发收集的益处也会增加
Z Garbage Collector
Z垃圾收集器 (ZGC) 是一个可扩展的低延迟垃圾收集器。 ZGC并发式的执行所有昂贵的工作,不需要停止执行应用线程
ZGC适用于需要低延迟(小于10毫秒暂停)和/或使用非常大的堆(TB量级)的应用,使用-XX:+UseZGC
启用,从JDK 11开始,ZGC作为实验性功能提供
选择合适的GC
如有必要,先调整堆大小来提高性能。如果性能仍不符合要求
- 如果应用程序具有较小的数据集(最大约100 MB)或者在单个处理器上运行且没有暂停时间要求,则选择串行收集器
-XX:+UseSerialGC
- 如果应用程序的极限性能是第一优先级考虑并且没有暂停时间要求,或者可以接收一秒或更长的暂停时间,那么可以让VM选择收集器或选择并行收集器
-XX:+UseParallelGC
- 如果响应时间比总吞吐量更重要,并且垃圾收集暂停必须保证短于一秒左右,那么选择并发收集器
-XX:+UseG1GC
或-XX:+UseConcMarkSweepGC.
- 如果响应时间是高优先级,和/或使用的是非常大的堆内存,选择完全并发的收集器
-XX:+UseZGC
此外性能还取决于堆的大小,应用程序内的实时数据量以及处理器的数量和速度,如果推荐的收集器未达到所需性能,则首先尝试调整堆和分代的大小。如果性能仍然不足,那么尝试使用不同的收集器:使用并发收集器来减少暂停时间,使用并行收集器来提高多核处理器的总吞吐量
参数配置
选项
-X
:非标准选项(不保证所有的 JVM 实现都支持)
-XX
:不稳定、不建议随便使用
- 布尔选项 打开
-XX:+<option>
,关闭-XX:-<option>
- 数值选项 设定方式
-XX:<option>=<number>
,数字可以包含 'm' 'M' , 'k' 'K' f, 'g' 'G' - 字符串选项 设定
-XX:<option>=<string>
查询某个参数的默认值,例如MaxHeapSize
的值
> java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
uintx MaxHeapSize := 4173332480 {product}
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
选项 | 描述 |
---|---|
堆栈空间相关 | |
-Xms -Xmx | 堆的最小值和最大值,对于服务器的部署,通常设定为相同的值,避免堆大小调整产生的暂停 |
-Xss | 设定线程调用栈的大小,默认是1M,默认操作系统ulimit -s 限定最大为8192KB |
-Xmn | 新生代的大小,直接设定为固定值,可以通过-XX:NewSize 和-XX:MaxNewSize 分别设定初始大小和最大值 |
-XX:MaxHeapFreeRatio | GC后堆空间的最大空闲比例(默认70%,达到会进行收缩),调小会减小内存占用 |
-XX:MinHeapFreeRatio | GC后堆空间的最小空闲比(默认40%,不足堆空间会进行扩张),调小会减少内存占用 |
-XX:-ShrinkHeapInSteps | 默认情况下开启,逐步减小堆空间到最大空闲比,关闭会立即减小,可能导致性能下降 |
GC类型 | |
-XX:+UseG1GC | 使用G1收集器 |
-XX:+UseSerialGC | 使用Serial GC |
-XX:+UseParallelGC | 使用 Parallel Scavenge GC |
-XX:+UseParallelOldGC | 使用 Parallel Old GC |
-XX:+UseParNewGC | deprecated,JDK 9 |
-XX:+UseConcMarkSweepGC | deprecated,JDK 9 |
GC性能设定 | |
-XX:MaxGCPauseMillis=n | 设定最长停顿时间,毫秒 |
-XX:GCTimeRatio=n | 吞吐量设定,1 / (1 + n) ,G1默认为12,GC时间占比8% |
-XX:ParallelGCThreads=n | 设置STW工作线程的值,逻辑处理器的数量不超过8时,n的默认值与逻辑处理器的数量相同,如果有超过8个逻辑处理器,n默认值为逻辑处理器数量的5/8 |
新生代相关 | |
-XX:NewRatio=n | young : old = 1 : n ,默认n为2 |
-XX:NewSize | 新生代的初始大小,推荐为堆大小的25%到50% |
-XX:MaxNewSize | 新生代的最大值 |
-XX:SurvivorRatio=n | eden/survivor区域的比例,默认为8,即每个survivor占新生代的1/10,默认-XX:+UseAdaptiveSizePolicy 是开启的,JVM会动态调整survivor的大小比例,最小为3 |
-XX:MaxTenuringThreshold=n | 对象晋升老年代的年龄阈值,parallel gc默认15,CMS默认为6 |
G1相关设置 | |
-XX:G1HeapRegionSize | 堆区域的大小,默认基于初始和最大堆大小得到,在1到32 MB之间变化,并且必须是2的幂,堆包含大约2048个堆区域 |
-XX:InitialHeapOccupancyPercent | 默认整个堆占比45%时触发并发标记 |
-XX:ConcGCThreads | 并发标记的线程数目,ParallelGCThreads的 1/4 |
-XX:+G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=45 | 用于自适应控制启动并发标记的堆占用百分比,对于前几个收集周期G1将使用老年代的45%的占用率作为默认阈值 |
-XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 | 新生代的大小,在这两个值之间变化,百分比表示占当前使用的Java堆的比例 |
-XX:G1HeapWastePercent=5 | 区域集合中允许未回收空间百分比,如果收集集合候选中的可回收空间低于该阈值,则G1将停止空间回收阶段 |
-XX:G1MixedGCCountTarget=8 | 空间回收阶段的进行收集的次数 |
-XX:G1MixedGCLiveThresholdPercent=85 | 老年代区域的存活对象大于该比例,不会在空间回收阶段进行垃圾收集 |
CMS设置 | |
-XX:CMSInitiatingOccupancyFraction | 设置启动CMS的老年代占用率,默认值为-1,使用-XX:CMSTriggerRatio 的值 |
-XX:CMSTriggerRatio=percent | 内存占用达到-XX:MinHeapFreeRatio 的percent%时启动CMS,默认值是80% |
其他 | |
-XX:MetaspaceSize | 设置触发gc的元数据空间的大小 |
-Xlog:gc* | 发生垃圾收集时,打印日志,等同于PrintGCDetails
|
-Xlog:gc | 使用的是JVM Unified Logging Framework输出log信息,等同于PrintGC , -Xlog 使用样式:-Xlog:tag1[+tag2...][*][=level][:[output][:[decorators][:output-options [,...]]]]
|
-XX:MaxDirectMemorySize | java.nio包使用的内存数量 |