温馨提示:本系列博文(含示例代码)已经同步到 GitHub,地址为「java-skills」,欢迎感兴趣的童鞋
Star
、Fork
,纠错。
垃圾收集
在 Java 对象被回收之前,首先需要判断该对象是否已经过期或者死亡?常见的判断一个对象是否过期的算法主要有两种,分别为:
- 引用计数法算法:在对象创建时,为其设置一个默认值为 0 的引用计数器,每当有一个地方引用该对象时,引用计数器的值就加 1;当引用失效时,引用计数器的值就减 1。当 GC 发生时,所有引用计数器的值为 0 的对象,都可以被当做垃圾进行回收。
- 可达性分析算法:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,或者说是不可达的。这些不可达的对象,即可被当做垃圾进行回收。
在 Java 语言中,可作为 GC Roots 的对象包括以下几种,分别为:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
此外,在主流的 Java 虚拟机中大都采用可达性分析算法来判断对象是过期,其中最主要的原因就是引用技术算法无法解决对象之间相互循环引用的问题。例如,在出现下面的情况时,引用计数算法就束手无策了:
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 该成员属性的唯一意义就是占点内存,以便能在 GC 日志中看清楚是否被回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
/**
* 假设在这行发生 GC,objA 和 objB 是否能被回收?
*/
System.gc();
}
}
实际上,无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判断对象是否存活都与“引用”有关。在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用(也称为幽灵引用或者幻象引用)4 种,这 4 种引用强度依次逐渐减弱。
- 强引用:它在程序代码中普遍存在,类似
Object obj = new Object()
这类的引用,只要强引用还在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用:它用来描述一些还有用但并非必须的对象,对于软引用关联的对象,在系统将要发生内存溢出的异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常,即在发生内存溢出的异常时,软引用关联的对象都已经被回收掉了,说的更直白些,程序只会保留强引用关联的对象。
- 弱引用:它也是用来描述非必须的对象,但是它的强度比弱引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
- 虚引用:它是最弱的引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。
在虚拟机完成对对象是否存活的判断之后,将正式执行垃圾收集操作。常见的垃圾收集算法,包括:
- 标记-清除算法,先标记 Java 堆中可回收的对象,然后直接进行回收操作;
- 复制算法,常见的是将 Java 堆划分为一个 Eden 空间和两个 Survivor 空间,默认比例为 8:1:1,每次使用一个 Eden 空间和一个 Survivor 空间;
- 标记-整理算法,与“标记-清除算法”的唯一区别就是在回收操作完成后,会将零散的空间碎片进行整理;
- 分代收集算法,将 Java 堆分为新生代和老年代,新生代采用“复制算法”,而老年代则采用“标记-清除算法”或者“标记-整理算法”。
在 HotSpot 为例,它将 Java 堆划分为新生代和老年代,不同的“年代区域”采用不同的垃圾收集器,例如新生代采用 Serial、ParNew 或者 Parallel Scavenge 收集器;老年代采用 Serial Old、Parallel Old 或者 CMS 收集器;G1 收集器则同时使用于新生代与老年代,即仅使用一个 G1 收集器即可完成对整个 Java 堆的回收工作。其中,Serial 是一个单线程收集器,ParNew 是 Serial 的一个多线程版本收集器;Parallel Scavenge 也是一个多线程且采用复制算法的新生代收集器,与 CMS 等收集器关注点是尽可能地缩短垃圾收集时用户线程的停顿时间不同,Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量;Serial Old 和 Parallel Old 则是 Serial 和 Parallel Scavenge 的老年代版本。
内存分配策略
对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过 JIT 编译后被拆散为标量类型并间接地在栈上分配),对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲(Thread Local Allocation Buffer,缩写为 TLAB),将按线程优先在 TLAB 上分配。少数情况下,也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。常见的内存分配策略为:
- 对象优先在 Eden 分配:大多数情况下,对象在新生代 Eden 区中分配,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC,即发生在新生代的垃圾收集。
- 大对象直接进入老年代:所谓大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。虚拟机提供了一个
-XX:PretenureSizeThreshold
参数,令大于这个设置值的对象直接在老年代分配。 - 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄计数器,如果对象在 Eden 出生并经过过第一次 Minor GC 后仍然存活,并且能被 Survivor 区容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1,对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(默认为 15 岁),将将会被晋升到老年代中。对象晋升老年代的年龄阙值,可以通过参数
-XX:MaxTenuringThreshold
设置。 - 动态对象年龄判定:虚拟机并不是永远地要求对象的年龄必须达到了
MaxTenuringThreshold
才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代。 - 空间分配担保:在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看
HandlePromotionFailure
设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者HandlePromotionFailure
设置不允许冒险,这这时也要改为进行一次 Full GC(指发生在老年代的 GC,会发生 STW)。实际上,在 JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则就进行 Full GC。
———— ☆☆☆ —— 返回 -> 那些年,关于 Java 的那些事儿 <- 目录 —— ☆☆☆ ————