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 {
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> {
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)并发清除。耗时最长的就是并发标记和并发清除。
- 优点
?? 并发收集,低停顿 - 不足
?? 降低总吞吐量,占用用户线程用来并发收集等
?? 无法处理“浮动垃圾”,在并发收集和并发清理阶段产生的垃圾(浮动垃圾)要等到下一次GC才能收集。
?? 空间碎片
G1
Garbage First收集器是一款面向服务端应用的垃圾收集器,目标是在延迟可控的情况下,尽可能提供高的吞吐量。G1在遵循分代收集理论的基础下,开创了收集器面向局部收集的设计思路和基于Region的内存布局,使得垃圾回收的范围时堆内存任何部分都能组成的回收集,而非一定是整个新生代或者整个老年代或者整个Java堆。
G1将Java堆划分成多个大小相等的Region,每个Region根据需要都可以扮演新生代Eden,s0s1,或者老年代的角色,Region中还有一类特殊的Humongous区域专门用来存储大对象。G1优先处理回收价值最大的Region。G1的运作过程可以划分为四个步骤,1)初始标记 2)并发标记 3)最终标记 4)筛选回收
ZGC
ZGC的目标提供在吞吐量影响不大的前提下,实现垃圾收集停顿时间都限制在10ms以内的低延迟。
ZGC像G1一样也是采用基于Region的内存布局,但是不同点在于ZGC的Region的大小不是一样的,有三种规格,包括2MB,32MB和大型Region(容量不固定),它通过一种“染色指针技术”,通过以下四个阶段进行垃圾回收:1)并发标记 2)并发预备重分配 3)并发重分配 4) 并发重映射