JVM常见的几种GC算法
什么是垃圾回收(GC)
GC::Garbage Collection(垃圾收集)。JVM会自动对内存进行管理和垃圾清扫,这种行为称之为垃圾回收。
常见的垃圾回收算法有哪些
常见的垃圾回收算法有三种,它们各有优缺点:
- 标记-复制:将内存划分为大小相等的两块,当一块用完后,将还存活的对象copy到另一块上
- 优点:实现简单,效率高,不产生内存碎片
- 缺点:内存空间利用率低
- 标记-清理:先标记需要回收的对象,完成标记后,清除对象。
- 优点:效率高,空间利用率高
- 缺点:内存空间碎片化严重
- 标记-整理:先标记需要会后的对象,完成标记后,清除对象,将存活的对象都想一端移动。(标记-清除+整理)
- 优点:不会产生内存碎片
- 缺点:效率低
对象存活分析
常见的对象存活分析有两种算法:引用计数法,可达性分析算法。
引用计数法
引用计数法的逻辑比较简单,对象维护一个counter计数器,如果有一个引用与之相连,则counter++。如果一个与之相连的引用失效了,则counter–。如果一个对象的counter为0,则表明这个对象已经被废弃了,可以被GC。
对于循环引用的两个对象A,B,引用计数法永远无法A,B对象。
可达性分析算法
所谓的可达性分析算法,就是通过一组GC Roots集合,或者说tracing GC的“根集合”,就是一组必须活跃的引用 作为起点,通过引用关系遍历对象图,能被遍历到的对象就判定为存活的,其余的对象判定为死亡。
常见的可以作为GC Roots引用的有:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象。
强/软/弱/虚引用
从JDK1.2开始,对象的引用被分为四个级别,从而使JVM可以更好地控制对象的生命周期。这四个引用级别就是:强引用、软引用、弱引用和虚引用。
强引用(Strong Reference)
强引用可以直接访问目标对象。如果一个对象具有强引用,那垃圾回收器绝不会回收它。JVM宁愿抛出OOM异常也不会回收这一类对象。
显示地将对象引用置为null
,或者超出对象引用的使用范围(比如方法出栈,方法内部的强引用,是保存在Java栈中的)。
java.util.ArrayList#clear
方法的含义,其实就是保留了容器,但是将里面的元素全部置为null,方便gc。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
软引用(SoftReference)
对于软引用对象,如果JVM内存空间充足,JVM就不会GC它;如果内存空间不足,就会酌情回收这些对象。软引用的这一特性很适合用来做内存缓存。
SoftReference<String> str = new SoftReference<String>("Hello World");
System.out.println(str.get());
弱引用(WeakReference)
弱引用和软引用区别在于,弱引用的对象拥有更短暂的生命周期,当JVM在GC时扫描到只具有弱引用的对象时,无论当前内存空间是否足够,都会回收它。常见的使用WeakReference
的有:ThreadLocal
,WeakHashMap
。
在ThreadLocal
中,内部使用了ThreadLocalMap
来保存key-vlaue,这个map使用的entry定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
key是ThreadLocal本身,使用的WeakReference。value对应具体要保存的值,是强引用。
一般来说,ThreadLocal是静态变量,其内存模型大致如下:
ThreadLocal
实例通常是类中的private static
字段,它们希望将状态与某一个线程>(例如,用户 ID 或事务 ID)相关联。 关于ThreadLocal,ThreadLocal并不是用来解决多线程共享变量的问题,它们希望将状态与某一个线程相关联。
ThreadLocalMap
维护 ThreadLocal
变量与具体value的映射,由于key是WeakReference
的,所以Entry
中的key可以被回收,是null的。这样一来,就无法访问对应的value了。但是只要当前的线程没有退出,就存在一条Thread Ref->Thread->ThreadLocalMap->Entry->value的强引用链,所以value无法被GC。只有当线程退出,Current Thread Ref
不在栈中,value的强引用断开,value才会被正确GC。
所以关于ThreadLocal
的内存泄漏,其实是对于ThreadLocal
的使用不当。使用ThreadLocal时,避免内存泄漏,可以注意两点:
- static
- 手动的remove
WeakHashMap
的设计和ThreadLocal
类似,不过,WeakHashMap
会在读和写操作之前,调用java.util.WeakHashMap#expungeStaleEntries
方法,遍历整个ReferenceQueue
来除去value上的强引用(e.value = null
).
虚引用(PhantomReference)
虚引用并不会影响对象的生命周期,如果一个对象仅持有虚引用,那么和没有任何引用一样,随时可以被GC。
虚引用对象,在回收器确定其指示对象可另外回收之后,被加入队列。虚引用最常见的用法是以某种可能比使用 Java 终结机制更灵活的方式来指派 pre-mortem 清除动作。
java.lang.ref.PhantomReference#get
方法及其注释。
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
引用队列(ReferenceQueue)
引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中。
有了上述的四种引用类型后,还需要一个引用队列来配合使用。在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中。(性质更类似与监听器?)
ReferenceQueue其实就是一个简单的队列,主要提供了enqueue
和poll
两个操作。
Reference
是引用对象的抽象基类。Reference
有四种状态:
- Active:新建实例的状态,当GC检查到reference的可达性变更了,GC会”通知”当前引用实例改变其状态为”pending”或者”inactive”。
- Pending:当前的引用在pending-Reference列表中,等待
ReferenceHandler
线程处理 - Enqueued:当前的引用实例已经被添加到
ReferenceQueue
中,但是尚未移除。 - Inactive:当前的引用实例已被从
ReferenceQueue
中移除或者并没有注册过Queue的直接被回收。
Guava中的内存缓存-LoadingCache
Guava中的内存缓存LoadingCache
就使用了Reference的特性。
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.maximumSize(5)
.expireAfterWrite(5, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
//.softValues()
.build(new CacheLoader<Object, Object>() {
@Override
public Object load(Object key) {
System.out.println("load key " + key);
return "hello " + key + "!";
}
});
为什么抛弃了softKeys()可以参考Why is softKeys() deprecated in Guava 10?
JVM中的分代收集
Java应用中,大部分的对象都是’朝生暮死’。不同阶段适合使用不同的GC算法进行GC回收。分代收集就是基于这种思想。
以hotspot为例,JVM将堆分为 新生代(Young),老年代(Old),永久代(Permanent),并从JDK1.7开始,去掉永久代,将常量池等以前存放在永久代的内容从永久代中移出。并在JDK1.8中完全去掉了永久代,取而代之的是元空间(Metaspace),元空间使用的是本地内存,Metaspace的大小仅受限于native memory的剩余大小。
对传统的、基本的GC实现来说,在收集的工程中,无可避免的要stop-the-world
,现代的各种收集器其实都是在致力于低停顿高并发的进行GC。
Safepoint
如果要触发GC,那么JVM中的所有Java线程必须达到GC SafePoint。
程序执行时并非在所有的地方都能停顿下来开始 GC,只有在到达安全点时才能暂停。 Safepoint 的选定既不能太少以至于让 GC 等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。 所以安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行。 “长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生 Safepoint。
https://www.sczyh30.com/posts/Java/jvm-gc-safepoint-condition/
http://xiao-feng.blogspot.com/2008/01/gc-safe-point-and-safe-region.html
新生代垃圾收集器
SerialNew(串行)收集器
最早最基本的收集器,单线程收集器,在进行垃圾回收的时候,需要进行STW(Stop The World)
ParNew(并行)收集器
SerialNew的多线程版本,在单核cpu场景下,效率远远低于SerialNew。目前只有它能与cms配合工作。
Parallel Scavenge(并行)收集器 并行的多线程收集器,与ParNew类似,也采用的复制算法。但是和ParNew不同的是,Parallel Scavenge关注的是更高的吞吐量。
老年代垃圾收集器
SerialOld收集器
Serial收集器的老年代版本,单线程收集器,采用标记-整理算法。
Parallel Old收集器
parallel scavenger的老年代版本,多线程收集器,采用标记整理算法
CMS收集器
Concurrent Mark Swap,以获取最短回收停顿时间为目标。基于标记-清除算法实现。 主要流程分为:
- 初始标记(STW):从GCRoots开始,只扫描能之间关联GCRoots的对象,速度很快
- 并发标记:和用户线程并发执行,在初始标记的基础上向下追溯。
- 并发预清理(可关闭):查找在并发标记过程中新进入老年代的对象,减少下一阶段”重新标记”的工作
- 重新标记(STW):扫描CMS堆中的剩余对象,STW
- 并发清理 :清理垃圾对象,和用户线程并发执行
- 并发重置:重置CMS收集器的数据结构,等待下一次GC。 CMS算法的缺点:
- 基于标记-清扫算法,不会整理,压缩空间,容易产生内纯碎片,需要定期重启。
- 对cpu资源敏感,并发阶段,用户线程和gc线程并行,总吞吐量会降低。
- 无法处理浮动垃圾,在cms并发清理时,用户线程新产生的垃圾,只能留待下次gc时处理。
混合的垃圾收集器-G1
拓展:ZGC
JVM调优相关日志参数
- -XX:+PrintGC 输出简要GC日志
- -XX:+PrintGCDetails 输出详细GC日志
- -XX:+PrintGCTimeStamps 输出GC的时间戳(JVM启动到当前时间的总时长)
- -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式)
- -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- -XX:+PrintReferenceGC 打印年轻代各种类型引用的数量以及时长
- -XX:+PrintTenuringDistribution 打印幸存区中对象的年龄分布,并输出当前的动态年龄