Java 垃圾回收

2024-06-03  本文已影响0人  虎鲸1225

在 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 classesJava platform libraries)。

Java 语言的一个重要特点就是与平台无关,即得益于 JVM 的引入,Java在不同平台上运行时不需要重新编译,秩序生成在 JVM 上运行的目标代码,就可以多平台运行。

image.png

JVM 定义了程序执行期间是用的数据区域,如程序计数器、堆、栈、方法区、运行时常量池等。这些数据区域中的一些在JVM启动时创建,JVM退出时销毁。

1.2 Java 对象生命周期

Java 中,对象的生命周期包括:

image.png
public 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.png

Java 虚拟机使用该算法来判断对象是否可被回收,在 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 在这个阶段辨别内存是否被使用,被引用的对象这里展示为蓝色,没有被引用的为橙色。这个过程效率不高

image.png

Step2: 标准清理 (Normal Deletion)
清理时,直接把可以回收的对象内存释放,留下被引用的对象和指向可用空间的指针。内存碎片会导致没有足够连续的空间给大对象分配内存

image.png

Step3: 压缩清理 (Deletion with Compacting)

通过把被引用的对象都移动到一起,后续为新对象分配内存则会更加容易和快速。

image.png

3.2 为什么要进行世代垃圾回收?

如上所述,标记和压缩 JVM 中所有对象是相当低效的,随着分配的对象越来越多,对象表越来越到,导致垃圾回收时间越来越长。同时,又有大量实证表明,大多数对象生命都是短暂的。

下面是数据示例图,Y周显示分配的字节数存活时间,X是随着时间推移分配的字节数。

即随着时间推移,分配的对象越来越少对。

image.png

因此,从对象分配的行为中,为了提高JVM 性能,堆被分为了更小的部分(或成为代),分别是:新生代(Young Gneration)、老年代 (Old Generation) 和 永久代(Permanent Generation).

image.png

新生代:新对象首先被分配到这里,当年轻代内存用尽,会触发一次 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 EventMinor GCStop 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 区域

image.png

2. 当 Eden 区域占满,触发一次 minor GC

image.png

3. 被引用对象移动到第一块 Survivor 区域(S0),没有被引用的对象则被删除

image.png

4. 在下一次 Minor GC,类似上一步没有被引用的对象被释放,Eden 和 引用对象则会被一起移动到第二块Survivor 区域(S1),移动后,EdenS0 被清空,可以看到现在 S1 区域有不同age的对象。

image.png

5. 下次 Minor GC,同样的流程重复。但是这次 S0S1 互换。引用的对象会被移动到 S0,幸存的对象 age 继续增加,EdenS1 都被清空。

image.png

6. 经过几次 Minor GC 之后,对象age达到阈值(这里为8),他们就会被移动到老年代

image.png

7.随着Minor GC 不停地进行,幸存的对象持续promote到老年代

image.png

8. 随着老年代 的对象不断增多,最终会触发 Major GC 来释放更多的内存空间。

image.png
上一篇下一篇

猜你喜欢

热点阅读