深入理解Java虚拟机-垃圾收集器与内存分配策略
三个问题
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
引用计数算法
当对象有一个引用时,计数器加1,当引用失效时,计数器减1,计数器为0则表示对象不可能再被使用。但是这个算法很难解决对象之间相互循环引用的问题。
举个栗子
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
运行一下,这个对象还是会被回收的,如果是计数算法,那么就不会被回收了。
可达性分析算法
主流的商用语言,包含java都是采用可达性分析算法的。从GC Roots的对象作为起始点,向下搜索,不可达的对象就可以回收,上面引用计数算法中举的例子,虽然两个对象相互引用,但是由于不可达,于是还是会被回收的。
灰色表示还存活的对象,白色表示不可达对象,可以回收。
可以作为GC Roots的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
引用
- 强引用就是指在程序代码之中普遍存在的,类似Object obj = new Object();只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用是用来描述一些还有用但并非必需的对象。如果内存溢出之前,会把这些对象列进回收范围之中进行第二次回收。
- 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,在下一次垃圾收集器发生之前被回收。
- 虚引用是最弱的一种引用。
对象死亡
宣布一个对象死亡至少要经历两次标记过程,如果可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。此时会被放置在F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。
回收方法区
无用类的三个条件
- 该类所有的实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用。无法再任何地方通过反射访问该类的方法。
垃圾收集算法
标记-清除算法
不足
- 标记和清除两个过程效率都不高
- 标记清除后会产生大量不连续的内存碎片
复制算法
解决效率问题
不足
内存缩小为原来的一半
分代收集算法
目前商业虚拟机的垃圾收集都是采用分代收集算法。一般是把java堆分为新生代和老年代,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,而老年代中因为对象存活率高、没有额外空间对她进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
HotSpot中,使用一组成为OopMap的数据结构直接得知那些地方存放着对象引用。在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,通常是先找Safepoint,方法调用、循环跳转、异常跳转等功能指令才会产生Saftpoint。
G1收集器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。
G1收集器步骤
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
内存分配与回收策略
对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按照线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中。
大多情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC.
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Major GC/ Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。
大对象直接进入老年代
典型的大对象是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息,短命大对象就是一个更坏的消息,应当避免。
长期存活的对象将进入老年代
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于或者等于该年龄的对象就直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。只要老年代连续空间大于新生代对象总大小,就会进行Minor GC,否则进行Full GC