深入理解Java虚拟机-垃圾收集器与内存分配策略

时间:2022-12-27 21:08:12

深入理解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的对象作为起始点,向下搜索,不可达的对象就可以回收,上面引用计数算法中举的例子,虽然两个对象相互引用,但是由于不可达,于是还是会被回收的。

深入理解Java虚拟机-垃圾收集器与内存分配策略

灰色表示还存活的对象,白色表示不可达对象,可以回收。

可以作为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