美文网首页JVM 原理和实践
《Java 虚拟机原理》7.3 精选 —— GC 基础篇

《Java 虚拟机原理》7.3 精选 —— GC 基础篇

作者: 熊本极客 | 来源:发表于2021-03-06 22:52 被阅读0次

1.GC 是什么?GC 的作用是什么?

背景
程序员在内存处理方面出现问题,例如忘记、错误回收内存,导致程序或系统的不稳定甚至崩溃。

GC (GabageCollection)是指垃圾回收。Java 提供 GC 功能,可以自动监控内存区域是否超过作用域,实现自动回收内存的目的。

说明
Java 语言没有提供显式方法来分配和释放内存。

2.简述 Java 垃圾回收机制及其优点

Java 垃圾回收机制实现了自动回收内存的功能。JVM 有垃圾回收线程(守护线程),其优先级较低,在正常情况下是不会执行的。当 JVM 空闲heap 内存Metaspace 内存不足时,JVM 会执行垃圾回收线程。

Java 垃圾回收的步骤
判断对象 A 是否存活。采用引用计数法可达性分析,例如后者从 GC Roots 开始搜索,当没有任何的 GC Roots 与对象 A 相连时,则对象 A 是不可达对象,可以判定为可回收对象。(GC Roots 是堆外指向堆内的引用
触发 GC 的时机。Minor GC 发生在 Heap 新生代,例如 Eden、FromSuv、ToSuv 满了都会触发 Minor GC。Full GC 发生在 Heap 老年代和 Metaspace,例如调用System.gc、Heap 老年代空间不足、Metaspace 空间不足等。
进行 GC。GC 算法是内存回收的理论方法,GC 垃圾收集器则是具体实现。GC 算法有标记清除法、复制算法、标记压缩算法

JVM 是采用分代垃圾回收机制,本文通过某对象的完整 GC 过程,理解 Java 垃圾回收机制,可参考《Java 虚拟机原理》5.1 GC垃圾收集及案例分析 — GC案例分析

Java 垃圾回收机制的优点

① 不需要考虑内存管理
② 有效地防止内存泄漏
③ 有效地利用内存
④ Java 中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。

Java 垃圾回收机制的缺点

① 垃圾回收机制的目标是在 JVM Heap 和 Metaspace 内存中,回收无用对象的内存空间,因此无法回收数据库连接、Socket、I/O 等物理资源
② 垃圾回收机制具有不可预知性,通过 System.gc()、对象引用设置为 null 等手段促进垃圾回收,但不能精确控制垃圾回收机制的运行;
③ 垃圾回收机制的潜在缺点是它的开销会影响性能

3.如何判断一个对象是否存活?

(1)判断对象存活的方法

引用计数法

每个对象都有一个引用的计数值,当该对象新增一个引用时,该计数值加 1;反之,当该对象减少一个引用时,该计数值减 1。如果计数值为 0,则回收该对象。引用计数法的缺点是不能处理对象的相互循环引用

image.png
可达性分析

从 GC Roots 开始搜索,当没有任何的 GC Roots 与对象 A 相连时,则对象 A 是不可达对象,可以判定为可回收对象。(GC Roots 是堆外指向堆内的引用)

image.png

GC Roots
堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)下列几种,Java 方法栈桢中的局部变量已加载类的静态变量JNI handles已启动且未停止的 Java 线程等。因此,GC Roots 是 GC 对象的引用。

(2)强引用、软引用、弱引用、虚引用的区别

强引用 StrongReference

例如,new 出来的对象,JVM 即使出现 OutOfMemory 错误,也不会垃圾回收该对象。

Object obj = new Object();
软引用 SoftReference

非必须引用,内存溢出之前回收

Object obj = new Object();  //强引用
ReferenceQueue<Object> referenceQueuee = new ReferenceQueue<>(); //引用队列
// 软引用和引用队列联合使用,如果软引用所引用的对象被垃圾回收器回收,JVM就会把这个软引用加入到引用队列中
SoftReference softReference = new SoftReference(str, referenceQueuee); 

注意:一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,同时维护系统的运行安全,防止 OutOfMemory 等问题。

弱引用 WeakReference

第二次垃圾回收时回收

@Test
public void testWeakReference() throws InterruptedException {
    ReferenceQueue<Object> referenceQueuee = new ReferenceQueue<>();
    Object weakObject = new Object();
    //弱引用
    WeakReference weakReference = new WeakReference(weakObject, referenceQueuee);
    System.out.println("WeakReference:" + weakReference.get());
    System.out.println("referenceQueuee:" + referenceQueuee.poll());

    weakObject = null;
    System.gc();
    Thread.sleep(2000);
    System.out.println("WeakReference:" + weakReference.get());
    System.out.println("referenceQueuee:" + referenceQueuee.poll());
}

// 测试结果
WeakReference:java.lang.Object@694f9431
referenceQueuee:null
WeakReference:null
referenceQueuee:java.lang.ref.WeakReference@f2a0b8e
虚引用 PhantomReference

虚引用是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动

4.简述 Java 中的内存泄漏

内存泄露是指一个不再被程序使用的对象或变量一直占据内存

在 Java 垃圾回收机制中,如果对象不再被引用(GC Roots 的可达性分析),则垃圾收集器会从内存中清除该对象。

(1)长生命周期的对象持有短生命周期对象的引用

由于短生命周期对象已经不再需要,而长生命周期对象持有它的引用而导致不能被回收。通俗地说,程序创建了一个对象,之后一直不再该对象,但这个对象却一直被引用,导致无法被垃圾收集器回收。
static 字段引起的内存泄露
在 Java 中,静态字段与整个程序的生命周期一致。如果静态对象是不断创建、增大,可能导致 OutOfMemory。

静态集合类:HashMap、LinkedList 等,容器内的对象在程序结束前都不能被回收。
单例模式:如果单例对象 A 持有外部对象 B 的引用,那么对象 B 将不能被 JVM 正常回收,导致内存泄露。

引用了外部类的内部类
一个外部类实例对象 B 的方法返回了一个内部类 B.Inner 的实例对象。如果 B.Inner 被长期引用,即使对象 B 不再被使用。因为 B.Inner 持有对象 B 的引用,所以对象 B 将不会被垃圾回收,导致内存泄露。解决方法:采用静态内部类

为什么非静态内部类持有外部类引用,静态内部类不持有外部引用?
static 调用 static,非 static 调用非 static。即 static --> 针对 class, 非static -> 针对 对象。

(2)长生命周期的对象本身不释放

数据库连接、网络连接和 IO 连接

(3)改变哈希值

不正确地重写 equals() 和 hashCode(),在 HashMap、HashSet 等种集合中,常常用到 equal() 和 hashCode() 来比较对象,如果重写不合理,将会成为潜在的内存泄露问题。

排除内存泄露的样例
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

解答:当进行大量的 pop 操作时,引用未置空使得 GC 是不会回收数组的对象。

image.png

解决方法:

public Object pop() {
    if (size == 0)
    throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}
image.png

5.简述 JVM 的一次完整 GC 流程

(1)Java 对象的创建及初始化

image.png

类加载检查。JVM 遇到 new 指令,首先去检查该类是否在常量池中有符合引用,并且检查该类是否被加载、解析和初始化。如果没有,则执行类加载流程。
内存分配。在类加载检查后,可知对象所需要的内存大小。JVM 采用“指针碰撞”或者“空闲列表”,为对象进行内存分配。

image.png

初始化零值。JVM 为分配到的内存空间都初始化为零值(不包括对象头),保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,例如,Object 对象的零值为 null,int 的零值为 0 等。
设置对象头。JVM 为对象设置对象头,包括该对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等。
执行 init 方法。init 方法即程序中对象的构造函数显示的内容。

(2)GC 过程

可参考《Java 虚拟机原理》5.1 GC垃圾收集及案例分析 — GC案例分析

相关文章

网友评论

    本文标题:《Java 虚拟机原理》7.3 精选 —— GC 基础篇

    本文链接:https://www.haomeiwen.com/subject/cbrvtltx.html