Java 垃圾回收
在 C 和 C++ 中,许多对象要求程序员声明他们后为其分配资源,然后才能安全地使用对象。使用完后,则需要程序员将这些资源释放到自由内存池。如果资源得不到释放,则认为代码泄露内存。然而,如果过早地释放,又可能发生数据丢失、Null指针等问题。
Java 和 C# 都有单独的管理应用程序管理对象的生存期并进行垃圾回收 (Garbage Collection - GC
) ,这样程序员可以不再关心内存释放问题,保证系统的性能和稳定性。
这里记录 Java 垃圾回收相关知识点,后面会再记录 C# 垃圾回收。
部分借鉴网络一些总结文章
1 Java 垃圾回收
Java 中,JVM
通过跟踪已经分配资源的引用来释放不再使用的堆存。只要 JVM
检测到对象不再被引用,垃圾回收器就会在适当的时候回收对象。
1.1 JVM
下载 Java 的时候,我们会得到 JRE (Java Runtime Environment)
,JRE 中包括了 JVM (Java Virtual Machine)
和 类库 (Java platform core classes
, Java platform libraries
)。
Java 语言的一个重要特点就是与平台无关,即得益于 JVM
的引入,Java在不同平台上运行时不需要重新编译,秩序生成在 JVM
上运行的目标代码,就可以多平台运行。
JVM
定义了程序执行期间是用的数据区域,如程序计数器、堆、栈、方法区、运行时常量池等。这些数据区域中的一些在JVM启动时创建,JVM退出时销毁。
1.2 Java 对象生命周期
Java 中,对象的生命周期包括:
image.pngpublic class ObjectLifecycle {
public static void main(String[] args) {
ObjectLifecycle obj = new ObjectLifecycle(); // 创建阶段
// 使用阶段
obj = null; // 不可达阶段
System.gc(); // 触发垃圾回收,进入回收阶段
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("垃圾回收前调用finalize方法");
}
}
2 如何判断一个对象可被回收
2.1 引用计数算法
给对象添加一个引用计数器,对象每增加一个引用,计数器加1,引用失效计数器减1,引用计数为0的对象可以被回收。
假如两个对象互相引用,那么引用计数器永远不会为0,导致对象无法回收,因此 JVM 不再使用引用计数算法:
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}
2.2 可达性分析算法
通过 GC Roots 作为起始点进行搜索,所有不可达的对象即为可被回收的对象。
image.pngJava 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
在虚拟机栈(栈帧中的本地变量表)中引用的对象:
Java public void method() { Object localVariable = new Object(); // localVariable是GC Roots }
在方法区中类静态属性引用的对象:
Java public class MyClass { private static Object staticObject = new Object(); // staticObject是GC Roots }
在方法区中常量引用的对象:
Java public class MyClass { private static final String CONSTANT_STRING = "constant"; // CONSTANT_STRING是GC Roots }
在本地方法栈中JNI(即通常所说的Native方法)引用的对象:
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
所有被同步锁(synchronized关键字)持有的对象:
Javapublic synchronized void synchronizedMethod() { // 当前对象(this)在执行同步方法时是GC Roots }
所有被同步锁(synchronized关键字)持有的对象:
3 垃圾回收过程
3.1 总体过程
Step1. 标记 (Marking)
GC 在这个阶段辨别内存是否被使用,被引用的对象这里展示为蓝色,没有被引用的为橙色。这个过程效率不高
Step2: 标准清理 (Normal Deletion)
清理时,直接把可以回收的对象内存释放,留下被引用的对象和指向可用空间的指针。内存碎片会导致没有足够连续的空间给大对象分配内存
Step3: 压缩清理 (Deletion with Compacting)
通过把被引用的对象都移动到一起,后续为新对象分配内存则会更加容易和快速。
image.png3.2 为什么要进行世代垃圾回收?
如上所述,标记和压缩 JVM
中所有对象是相当低效的,随着分配的对象越来越多,对象表越来越到,导致垃圾回收时间越来越长。同时,又有大量实证表明,大多数对象生命都是短暂的。
下面是数据示例图,Y周显示分配的字节数存活时间,X是随着时间推移分配的字节数。
即随着时间推移,分配的对象越来越少对。
image.png因此,从对象分配的行为中,为了提高JVM
性能,堆被分为了更小的部分(或成为代),分别是:新生代(Young Gneration)、老年代 (Old Generation) 和 永久代(Permanent Generation).
新生代:新对象首先被分配到这里,当年轻代内存用尽,会触发一次 minor garbage colletion
。如果对象的死亡率高,那么这次 minor garbage collection
则会有效的优化内存。新生代会被分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用一块 Eden 和一块 Survivor。回收时,会将 Eden 和 Survivor 中还存活得对象复制到另一块 Survivor空间中,最后清理 Eden 和使用过的那一块 Survivor。
这个过程会快速地完成,当另一块 Survivor不足以存放 Eden 和 Survivor 存活对象时,所有幸存对象 (surviving objects) 会被标记年龄并移动到 old generation
。
Minor GC - Stop the world Event:Minor GC
是 Stop the world
的行为,即所有的应用县城都要停止,直到整个操作完成。
老年代: 一般来说,新生代对象的age达到一定的阈值,就会被移动到老年代,而老年代的回收称为 major garbage collection
。
Major GC - Stop the world Event: Major GC
也是 Stop the world
的行为,通常会更慢一些,所以对于响应式应用程序,应该尽量减少这类GC,由于 Java 虚拟机提供了多种垃圾回收器,这个GC的长度受垃圾回收器所影响。
永久代: 这里面包括了 JVM
描述应用进程中是用的类和方法所需要的静态文件。永久代也被称为方法区,方法区的回收内容主要是 废弃常量和无用的类。
3.3 分代垃圾回收过程
上面我们理解了为什么要分代回收和回收的基本操作,下面展示具体回收过程:
1. 一个新的对象分配内存并记录在 Eden
区域
2. 当 Eden
区域占满,触发一次 minor GC
3. 被引用对象移动到第一块 Survivor 区域(S0)
,没有被引用的对象则被删除
4. 在下一次 Minor GC
,类似上一步没有被引用的对象被释放,Eden
和 引用对象则会被一起移动到第二块Survivor 区域(S1)
,移动后,Eden
和 S0
被清空,可以看到现在 S1
区域有不同age的对象。
5. 下次 Minor GC
,同样的流程重复。但是这次 S0
和 S1
互换。引用的对象会被移动到 S0
,幸存的对象 age 继续增加,Eden
和 S1
都被清空。
6. 经过几次 Minor GC
之后,对象age达到阈值(这里为8),他们就会被移动到老年代
7.随着Minor GC
不停地进行,幸存的对象持续promote到老年代
8. 随着老年代
的对象不断增多,最终会触发 Major GC
来释放更多的内存空间。