JVM - GC算法
判断对象需要被回收的算法
引用计数
引用计数,Reference Counting。主流的JVM并没有使用引用计数来管理内存。引用计数的方法即在对象中添加一个引用计数器,每当有个地方引用它的时候就加一;引用失效的时候,就减一。当计数器为0,那么该对象就应该被回收,不能再使用了。
问题: 循环引用的问题无法解决。
可达性分析
可达性分析,Reachability Analysis。通过“GC Roots”的根对象为起始节点集,根据引用关系向下搜索,搜索的路径叫做引用链(Reference Chain),如果某个对象和GC Roots间没有任何引用链相连,那么该对象不可达,需要被回收。
GC Roots根对象
总的来说,GC Roots根节点主要存在于全局性引用(如常量或类静态属性)以及执行上下文中(例如栈帧中的本地变量表)
- 虚拟机栈。栈帧本地变量表引用的对象,例如线程被调用的方法堆栈中使用到的参数,局部变量,临时变量等
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象,e.g. 字符串常量池(String Table)里的引用
- 本地方法栈中(JNI)即Native方法引用的对象
- JVM内部引用,如基本类型对应的Class对象,常驻异常对象,系统类加载器等
- 被
synchronized
锁持有的对象 - 反映Java虚拟机内部情况的JMXBean, JVMTI中注册的回调,本地代码缓存等(这些是啥不知道)
引用关系
可达性分析和引用计数都离不开“引用”,都是根据引用来确定对象是否被关联。JDK1.2后的引用,分为强引用,软引用,弱引用,虚引用。
- 强引用(Strong Reference),最为普遍,例如
Object obj = new Object()
, 只要强引用存在,对象就不会被回收。 - 软引用 (Soft Reference),描述一些还有用但是非必须的对象。在将要OOM异常前,会回收这部分对象,如果还是没有足够内存才会OOM
- 弱引用(Weak Reference),比软引用更弱。垃圾回收会回收这部分。
- 虚引用 (Phantom Reference),最弱。
ThreadLocal内存泄漏看引用
简单介绍下ThreadLocal的类示例如下:
class ThreadLocal<T> {
AtomicInteger nextHashCode;
set();//Thread.currentThread().threadLocals.set()
get();//Thread.currentThread().threadLocals.getEntry()
static class ThreadLocalMap {
Entry[] table;
set(); // i = key(ThreadLocal实例).threadLocalHashCode & (table.length - 1)
getEntry(); // table[i]
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
}
}
}
class Thread {
ThreadLocal.ThreadLocalMap threadLocals = null;
//...
}
再看一张经典的ThreadLocal
引用示意图,实线代表强引用,虚线代表弱引用,和上述的结构示例结合一起看就更清晰了:
- 使用弱引用的原因:
当我们把ThreadLocalRef
指向null
的时候,如果ThreadLocalMap里的key不是弱引用,那么该ThreadLocal实例永远不会被回收,但是我们其实是认为不再使用这个ThreadLocal的(希望它被回收)。 - 仍然存在内存泄漏的原因:
即便如此ThreadLocal实例被回收了,这个value由于是强引用,仍然没有被回收,但是已经是“不可达”了,(应用不会访问到它)。所以如果使用完ThreadLocal过后remove
一下,就不会出现内存泄漏了。 - 为什么value不设置为弱引用?
因为弱引用对于GC来说就是相当于没有引用,即如果不存在其他引用,那该实例一定会被回收。从ThreadLocal
的结构我们可以看出,ThreadLocal是没有强引用这个value的,所以当线程中ThreadLocal还存活着,但是又出现了GC时(前提是该value没有其他地方引用了),这个value被回收过后,我们再通过get()
方法拿到的就是不符合预期的null
了。 - 但其实也还好
如果线程对象会即时被回收,这个问题的影响也不大。
回收对象的算法
垃圾回收有两个分代假说和一个经验法则:
- 弱分代假说:绝大多数对象都是朝生夕灭
- 强分代假说:熬过越多次垃圾回收过程的对象就越难以消亡
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数
在我们通过上述算法知道了对象该不该被回收了,具体怎么来回收这部分对象呢(清理内存),有如下的方法:
标记-清除
主要缺点:内存碎片,无法找到连续的空间分配大对象。
标记-复制
主要缺点:内存可用空间缩小;如果大部分对象都是存活的,复制的开销较大
标记-整理
主要缺点:对象移动的开销。对象移动要STW(Stop the world,主要是停止用户线程)。
经典的垃圾收集器
Serial/Serial Old收集器
ParNew 收集器
Parrallel Scavenge 收集器
Parrallel Scavenge收集器又叫做“吞吐量优先”收集器和ParNew相似。吞吐量:用户线程时间占总时间(用户线程时间 + 垃圾回收线程时间)之比。
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法。注重吞吐量的场合可以优先考虑Parallel Scavenge收集器和Parallel Old收集器组合。
CMS
CMS(Concurrent Mark Sweep)收集器关注点是:尽量减少STW的时间。CMS基于“标记-清除”,分为四个步骤,分别是:1)初始标记 2)并发标记 3)重新标记 4)并发清除。耗时最长的就是并发标记和并发清除。
对于CMS来说的一个重要特点就是它不会做压缩,所以会有很多空间碎片,直到做一次Full GC才会压缩。如下图:
With CMS, old generation objects are deallocated in place. They are not moved around. The space is not compacted unless there is a full GC.
CMS收集步骤
- 优点
👍 并发收集,低停顿 - 不足
👎 降低总吞吐量,占用用户线程用来并发收集等
👎 无法处理“浮动垃圾”,在并发收集和并发清理阶段产生的垃圾(浮动垃圾)要等到下一次GC才能收集。
👎 空间碎片
G1
Garbage First收集器是一款面向服务端应用的垃圾收集器,目标是在延迟可控的情况下,尽可能提供高的吞吐量。G1在遵循分代收集理论的基础下,开创了收集器面向局部收集的设计思路和基于Region的内存布局,使得垃圾回收的范围时堆内存任何部分都能组成的回收集,而非一定是整个新生代或者整个老年代或者整个Java堆。
以停顿时间为目标的GC。通过建立数学模型,预测目标停顿时间可以回收的内存,并以此调整新生代大小。
G1将Java堆划分成多个大小相等的Region,每个Region根据需要都可以扮演新生代Eden,s0s1,或者老年代的角色,Region中还有一类特殊的Humongous区域专门用来存储大对象。G1优先处理回收价值最大的Region。G1的运作过程可以划分为四个步骤,1)初始标记 2)并发标记 3)最终标记 4)筛选回收
G1在回收过程中会做压缩(Compaction), 利用多核进行并行处理来减少暂停时间和提高吞吐量。G1使用一个pause prediction model来满足用户定义的暂停时间的要求,然后根据这个暂停时间选择需要回收的regions的数量
Based on data from previous collections, G1 does an estimate of how many regions can be collected within the user specified target time.
G1 has both concurrent (runs along with application threads, e.g., refinement, marking, cleanup) and parallel (multi-threaded, e.g., stop the world) phases. Full garbage collections are still single threaded, but if tuned properly your applications should avoid full GCs
和ParallelOldGC, CMS相比, G1会使得JVM进程大小增大一些,因为多了“审计”数据结构Remembered Sets
(RSets)和Collection Sets
(CSets)。
- RSets 每个region都有一个RSets,用于记录对象引用
- CSets 会被回收的region,在回收过程中,CSets里的存活对象都会被复制移动。
G1 Young 收集步骤
在JVM启动的时候Region大小就被选择好了。一般来说,JVM会分成2000个region,每个region大小可从1-32MB。
每个Region都被映射成Eden, Survior和old generation。除此之外,还有第四中region type叫做Humngous region
,用来存放比正常region一半大小还要大的大对象,它们是由连续的一组region组成的。
G1 Old 收集步骤
G1最佳实践
重要参数
-XX:+UseG1GC
: 使用G1收集器
-XX:MaxGCPauseMillis=200
: 为G1收集器设置目标回收暂停时间, 默认是200ms
-XX:InitiatingHeapOccupancyPercent=45
: 触发G1开始GC的已使用的堆占整个堆空间(Eden+S+Old)的比例。默认是45%。
最佳实践
- 不要设置young区大小(
-Xmn
), 设置该参数会限制G1的GC行为从而达不到暂停时间的目标,也不能做分区大小调整 - Response Time Metrics
Instead of using average response time (ART) as a metric to set the XX:MaxGCPauseMillis=
- Evacuation Failure
Evacuation Failure是指JVM在GC过程中用完了堆region, 而堆空间已经不能扩展了,达到了最大值。Evacuation Failure的问题是:GC要继续进行,一些不必要的拷贝会发生等,这些都非常的耗费资源。
避免Evacuation Failure的方法:1. 增大堆空间,增大-XX:G1ReservePercent=n
,默认是10;2. 让marking cycle(标记)更早启动. 3. 增大标记过程的线程数-XX:ConcGCThreads=n
G1 GC参数调整列表
参数 | 描述 |
---|---|
-XX:+UseG1GC | |
-XX:MaxGCPauseMillis=n | Sets a target for the maximum GC pause time. This is a soft goal, and the JVM will make its best effort to achieve it. |
-XX:InitiatingHeapOccupancyPercent=n | Percentage of the (entire) heap occupancy to start a concurrent GC cycle. It is used by GCs that trigger a concurrent GC cycle based on the occupancy of the entire heap, not just one of the generations (e.g., G1). A value of 0 denotes 'do constant GC cycles'. The default value is 45. |
-XX:NewRatio=n | Ratio of new/old generation sizes. The default value is 2. |
-XX:SurvivorRatio=n | Ratio of eden/survivor space size. The default value is 8. |
-XX:MaxTenuringThreshold=n | Maximum value for tenuring threshold. The default value is 15. |
-XX:ParallelGCThreads=n | Sets the number of threads used during parallel phases of the garbage collectors. The default value varies with the platform on which the JVM is running. |
-XX:ConcGCThreads=n | Number of threads concurrent garbage collectors will use. The default value varies with the platform on which the JVM is running. |
-XX:G1ReservePercent=n | Sets the amount of heap that is reserved as a false ceiling to reduce the possibility of promotion failure. The default value is 10. |
-XX:G1HeapRegionSize=n | With G1 the Java heap is subdivided into uniformly sized regions. This sets the size of the individual sub-divisions. The default value of this parameter is determined ergonomically based upon heap size. The minimum value is 1Mb and the maximum value is 32Mb. |
ZGC
ZGC的目标提供在吞吐量影响不大的前提下,实现垃圾收集停顿时间都限制在10ms以内的低延迟。
ZGC像G1一样也是采用基于Region的内存布局,但是不同点在于ZGC的Region的大小不是一样的,有三种规格,包括2MB,32MB和大型Region(容量不固定),它通过一种“染色指针技术”,通过以下四个阶段进行垃圾回收:1)并发标记 2)并发预备重分配 3)并发重分配 4) 并发重映射