《深入理解java虚拟机》读书笔记(二)垃圾收集器与内存分配策略
1.判断对象是否可回收
1.1 引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器的值就减一。当对象的计数器为零时不能再被使用,可以被回收。
引用计数器算法的优点是效率高,在计数器归0时即可回收对象;缺点是无法处理循环引用中的对象:
class A {
private B b;
public A(B b) {
this.b = b;
}
public void setB(B b) {
this.b = b;
}
}
class B {
private A a;
public B(A a) {
this.a = a;
}
public void setA(A a) {
this.a = a;
}
}
public static void main(String[] args) {
A a = new A(null);
B b = new B(a);
a.setB(b);
// a 和 b 对象将永远不会被回收
}
#generated by ChatGPT
1.2 可达性分析算法
通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为『引用链』,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象不可能再被引用。
Java中可作为GC Roots的对象包括:
- 当前活跃线程的栈桢里指向堆中对象的引用,即当前所有正在被调用方法的引用类型参数、局部变量等;
- 类的引用类型静态变量,这里指的是引用类型,像 int 等基本数据类型的静态变量肯定不能作为 GC Roots;
- 当前所有已被加载的Java类和类加载器;
- JNI 句柄,包括 JNI Local Handles 和 JNI Global Handles;
- 在方法区中常量引用的对象,譬如字符串常量池 ( String Table ) 里的引用;
- 所有被同步锁 ( synchronized关键字 ) 持有的对象引用
- ……
这些 GC Roots 大体可以分为三大类: - 活动线程相关的各种引用。
- 类的静态变量的引用。
- JNI 引用。
常用的 MAT 和 VisualVM 都提供 GC Roots 的查找功能
1.3 引用
引用的传统定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用。
- 强引用
指在程序代码之中普遍存在的引用赋值,即类似"Object obj = new Object()"这种引用关系。垃圾收集器不会回收掉存在强引用关系的对象。 - 软引用
用于描述一些还有用但非必须的对象,在系统将要发生内存溢出异常前会被列进回收范围之中进行二次回收。JDK1.2版之后提供了SoftReference类来实现软引用。 - 弱引用
用于描述非必须对象,强度比软引用更弱,只能生存到下一次垃圾收集发生为止。JDK1.2版提供了WeakReference类来实现。 - 虚引用
完全不会对对象生存时间构成影响,也无法用于对象访问,唯一作用是在对象被回收时收到一个系统通知。JDK1.2提供了PhantomReference类来实现。
1.4 对象的死亡
在可达性分析中被判定为不可达的对象真正死亡需要经历至少两次标记过程:1)对象没有与GC Roots相连接的引用链,被第一次标记;2)筛选该对象是否有必要执行finalize()方法,如果对象没有复写finalize()方法或该方法已被虚拟机调用过,则进行第二次标记,被回收。
当 Java 应用程序需要分配内存时,如果发现内存不足,垃圾收集器就会自动触发回收过程。回收的对象的内存将返回给系统,以便程序使用。
Java对象被回收的常见case:
- 对象被赋值null
- 对象在方法中被创建但未被使用
- 对象在循环体内被创建,在循环结束后不再被应用
1.5 回收方法区
方法区垃圾回收主要包含两部分内容:废弃的常量和不再使用的类型。
判定常量是否废弃比较简单,即系统中没有任何一个字符串引用常量池中的该常量;判定一个类型是否属于“不再被使用的类”的条件比较苛刻:
- 该类所有的实例都已经被回收,Java堆中不存在该类及其任何派生子类的实例;
- 加载该类的类加载器已经被回收,只有经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
HotSpot虚拟机提供了-Xnoclassgc进行类型回收控制、-verbose:class、-XX:+TraceClassLoading、-XX:+TraceClassUnLoading等参数查看类的加载和卸载信息。
2.垃圾收集算法
从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类,这两类也常被称作直接垃圾收集和间接垃圾收集,后者为主流收集算法。
2.1分代收集理论
根据强分代假说和弱分代假说,主流垃圾收集器的一致原则是将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储,对于年轻的区域,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,以较低代价回收到大量的空间;对于老年区域,虚拟机可以以较低的频率来回收这个区域,同时兼顾了垃圾收集的时间开销和内存空间的有效利用。
- 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。CMS收集器独有。
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。G1收集器独有。
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
2.2 主要的垃圾收集算法
2.2.1 标记-清除算法
是最基础的垃圾回收算法。首先标记出所有需要回收的对象,标记完成后统一回收掉所有被标记的对象,或者反过来标记存活的对象。主要缺点是:1)执行效率不稳定,随对象数量增长执行效率降低;2)内存空间碎片化不连续,导致空间效率利用率下降。
2.2.2 标记-复制算法
将内存按容量划为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将仍存活的对象复制到另一块上面,然后将已使用的内存空间一次清理掉。优点是每次复制时只需要移动堆顶指针,按顺序分配,避免了空间碎片化的问题,实现简单,效率较高;缺点是浪费了大量的内存空间。
1989年Appel提出了优化策略,即将新生代分为一块较大的Eden空间和两块较小的Survivor空间(8:1:1),每次分配内存只使用Eden和其中一块Survivor,垃圾搜集时将存活对象一次性复制到另外一块Survivor空间上,然后对Eden和那块Survivor进行清理。这种优化是基于弱分代假说“朝生夕灭”特点而实现的,即大量对象熬不过第一轮收集。
2.2.3 标记-整理算法
针对老年代对象的存亡特征(存活率较高),标记-复制算法造成的空间浪费比较大,因此提出标记-整理算法,让所有存活对象都向内存空间一端移动,直接清理掉边界以外的内存。缺点是移动存活对象并更新所有引用这些对象的地址代价较高,但相比标记-清除算法,减少了内存分配和访问方面的工作,总体上有效率的提升。