JVM(十四)垃圾回收的一些额外点

发布时间 2023-07-12 16:26:28作者: Tod4

JVM(十四)垃圾回收的一些额外点


1 System.gc()的理解

  • 在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存
  • System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用
  • JVM可以通过System.gc()的调用来决定JVM的GC行为,但是一般情况下垃圾回收都是自动进行的,无需手动触发

​ 如下代码,控制器有时会打印有时则不会打印相关内容,但是打开System.runFinalization()的注释之后,每次就一定会打印该内容。

public class SystemGCTest {
    public static void main(String[] args) {
        new SystemGCTest();
        System.gc();
		// System.runFinalization();
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用了finalize()");
    }
}
  • 调用System.runFinalization()实际上等效于调用Runtime.getRuntime().runFinalization()
  • 该方法会强制调用已经失去引用的对象的finalize方法
手动gc理解不可达对象的回收行为
public class LocalVarGC {
    // youngGC不会回收,最终放入老年代
    public void localVarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        System.gc();
    }

    // youngGC就会回收
    public void localVarGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();
    }

    // youngGC不会回收,最终放入老年代
    public void localVarGC3() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();
    }

    // youngGC就会回收
    public void localVarGC4() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int value = 10;
        System.gc();
    }

    // localVarGC1()的youngGC不会回收,最终放入老年代,随后第二次YGC则会回收
    public void localVarGC5() {
        localVarGC1();
        System.gc();
    }
    
    public static void main(String[] args) {
        LocalVarGC localVarGC = new LocalVarGC();
        localVarGC.localVarGC5();
    }
}

​ 对于localVarGC4()和localVarGC5(),首先localVarGC4()会被回收而localVarGC3()不会,可以通过jclasslib看一下两个方法栈帧的局部变量表:

image-20230711151259103 image-20230711151110323 image-20230711151125044

​ 可以看到两个局部变量表的大小都是2(localVarGC3()的第一个局部变量由于在代码块里面,所以超出作用域不显示在局部变量表里面,但是并没有删除这个局部变量,而是在出现新的局部变量的时候覆盖掉这个slot插槽),这也是localVarGC4()的value显示在位置1的原因,因此根据可达性分析localVarGC3()的引用变量指向的堆中的对象不会被回收,而localVarGC4()会。

​ localVarGC5()就很好理解了,localVarGC1()的调用结束,意味着整个localVarGC1()栈帧出栈,其中的局部变量表将从GC Roots中移除,根据可达性分析算法,自然会回收掉堆中相应的对象。

2 内存溢出与内存泄露

2.1 内存溢出
  • 内存溢出(Out Of Memory)指的是没有空闲的内存空间,并且垃圾回收器也无法提供更多的内存的情况

  • 主要的原因有两个:

    • Java虚拟机设置的堆内存不够
    • 代码中创建了大量的大对象,并且长时间不能被垃圾收集器收集

    老版本的jdk永久代在堆中,因此经常出现java.lang.OutOfMemoryError:PermGen space,而随着元数据区的引入,直接内存不足的时候,就会出现java.lang.OutOfMemoryError:MetaSpace

  • 通常在OOM之前,都会触发一次垃圾收集器的垃圾清除,如尝试回收软引用指向的对象,仍然不足则报OOM

  • 当一个超大的对象的大小超过堆的最大值的时候,就不会触发垃圾收集器而是直接报OOM

2.2 内存泄露
  • 内存泄露是指对象已经不会被应用程序用到了,但是垃圾回收器又不能回收它们的情况
  • 内存泄露不会立刻引起程序崩溃,而是在耗尽内存的时候会出现内存溢出
  • 例如:
    • 单例模式,单例的生命周期和应用程序是一样长的,而当当前单例程序中,如果持有对外部对象的引用的话,那么这个外部对象就不能被回收了,从而导致内存泄露
    • 一些提供关闭的资源未关闭导致垃圾泄露,如数据库连接、网络连接、IO等

3 Stop The World

  • Stop The World简称STW,指的是在GC时间发生过程中,会使整个应用程序暂停
    • 具体来说,是GC过程的垃圾标记阶段的根可达性分析算法(GC Roots)导致的Java执行线程的停顿,这是由于:
      • 分析工作必须在一个能够保证一致性的快照中进行
      • 如果出现分析过程中对象的引用关系还在不断变化,则分析结果的准确性将无法保证
  • 被STW中端的应用程序将在完成GC后恢复,频繁GC将影响系统的停歇,因此要减少STW的发生
  • STW跟采用哪种垃圾回收器无关,所有的GC都存在
    • 哪怕是G1也不能避免STW,只是尽可能缩短暂停时间
  • STW是JVM在后台自动发起、完成的,是在用户不可见的情况下把用户正常工作线程全部停掉
  • 尽量不要使用System.gc(),会导致STW

4 垃圾回收的并发与并行

​ 立即回收器的并行与并发,指的是:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
  • 串行(Serial):如果内存不足,则程序暂停启动JVM垃圾收集器单线程执行进行垃圾回收,回收完成后再启动程序的线程
image-20230711163249497
  • 并发(Concurrent):指用户线程垃圾收集线程并发工作(不一定是并行的,也有可能是交替执行),垃圾回收线程在执行的时候不会暂停用户程序的执行
    • 用户程序在执行,而垃圾回收器运行在另外的CPU核心
    • 如CMS、G1

5 安全点与安全区域

  • 安全点(safe point):程序执行过程中并非所有地方都能停顿下来GC,只有在特定位置才能停顿进行GC,这些位置称作安全点(safe point)

    safe point的选择很重要,如果太少可能导致GC等待的时间太长,太多则可能导致性能问题。

    通常情况下会根据“是否具有让程序长时间执行的特征”为标准进行选择,比如选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等

  • 怎么在GC发生之前,检查所有的线程跑到最近的安全点停顿下来呢?

    • 抢断式中断:首先中断所有的线程,如果还有线程不在安全点,则恢复该线程运行到安全点再中断(目前基本没有虚拟机使用了)
    • 主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候,会主动轮询这个标志,如果中断标志为真,则将自己主动挂起

    该机制保证了程序执行的时候,能够在不太长的时间内就进入GC的Safe Point

  • 安全区域(Safe Region):安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域内任何地方进行GC都是安全的,这是为了防止不执行的程序无法响应JVM中断请求的情况(如现成处于Sleep或者Blocked状态)

  • 总得来说,实际运行的时候

    • 当线程运行到Safe Region的时候,就会标识已经进入Safe Region,在此期间发生GC,垃圾收集器就会忽略标识为Safe Region的线程
    • 当线程离开Safe Region,就会检查JVM是否完成GC,完成则继续执行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止

6 Java引用

​ Java中的引用分为了强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种引用,四种引用的强度依次减弱

6.1 强引用:不回收
  • 强引用是最常见的普通对象引用,也是默认的引用类型

  • 一般使用一个new操作符创建一个对象并将其赋值给一个变量的时候,这个变量就成为了指向这个对象的强引用

  • 强引用的对象是可触及的,因此垃圾回收器永远不会回收强引用指向的对象

  • 一个普通对象如果没有其他的引用关系,并且超过了引用的作用域或者显式地将其强引用赋值为null,就可以被当做垃圾收集了

  • 强引用是造成垃圾泄露的主要原因,因为即使发生OOM也不会回收强引用指向的对象

    image-20230711190545858

​ 如下测试表明被强引用的对象不会被垃圾回收

    public static void main(String[] args) throws InterruptedException {
        StringBuffer str = new StringBuffer("Hello");
        StringBuffer str1 = str;

        str = null;
        System.gc();

        Thread.sleep(1000);

        System.out.println(str1);
    }
6.2 软引用:内存不足即回收
  • 只要是被软引用关联的对象,在系统内存空间不够之前,会把这些对象列入垃圾回收范围内进行第二次回收,如果回收之后仍没有足够的内存,就会报内存溢出异常
  • 垃圾回收器在某个时刻决定回收软可达对象的时候,会清理软引用,并可选地把引用存到一个专业队列(Reference Queue)
  • 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用,如果还有空闲内存就可以暂时保留缓存,当内存不足的时候就清理掉,这样就能保证在使用缓存的时候不会耗尽内存
  • 类似弱引用,只不过Java虚拟机会尽量软引用的存活时间更长一点,迫不得已才去清除
    public static void main(String[] args) throws InterruptedException {
        //SoftReference<User> userSoftRef = new SoftReference<>(new User("lxg", 1));
        // 等同于下面的写法
        User user = new User("lxg", 1);
        SoftReference<User> userSoftRef = new SoftReference<>(user);
        user = null;

        System.out.println(userSoftRef.get());

        System.gc();
        System.out.println("After GC.");
        //Thread.sleep(1000);
        // 内存足够,不会回收软引用
        System.out.println(userSoftRef.get());

        try {
            byte[] bytes = new byte[1024 * 1024 * 7];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(userSoftRef.get());
        }

    }

​ 结果:

User{userName='lxg', id=1}
After GC.
User{userName='lxg', id=1}
null
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.hikaru.java.reference.SoftReferenceTest.main(SoftReferenceTest.java:54)

​ 注意!软引用是在内存紧张的时候进行回收,而不一定是在内存溢出的时候才进行

    public static void main(String[] args) throws InterruptedException {
        //SoftReference<User> userSoftRef = new SoftReference<>(new User("lxg", 1));
        User user = new User("lxg", 1);
        SoftReference<User> userSoftRef = new SoftReference<>(user);
        user = null;

        System.out.println(userSoftRef.get());

        System.gc();
        System.out.println("After GC.");
        //Thread.sleep(1000);
        System.out.println(userSoftRef.get());

        try {
            byte[] bytes = new byte[1024 * 7185 - 1024 * 626];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(userSoftRef.get());
        }

    }

​ 上面代码并没有出现内存溢出,但是软引用还是被回收了

User{userName='lxg', id=1}
[GC (System.gc()) [PSYoungGen: 1728K->488K(2560K)] 1728K->684K(9728K), 0.0012236 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 196K->614K(7168K)] 684K->614K(9728K), [Metaspace: 3207K->3207K(1056768K)], 0.0039028 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
After GC.
User{userName='lxg', id=1}
[GC (Allocation Failure) [PSYoungGen: 60K->160K(2560K)] 674K->774K(9728K), 0.0002132 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 160K->128K(2560K)] 774K->742K(9728K), 0.0002047 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 128K->0K(2560K)] [ParOldGen: 614K->610K(7168K)] 742K->610K(9728K), [Metaspace: 3215K->3215K(1056768K)], 0.0040388 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 610K->610K(9728K), 0.0002728 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 610K->592K(7168K)] 610K->592K(9728K), [Metaspace: 3215K->3215K(1056768K)], 0.0039398 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
null
Heap
 PSYoungGen      total 2560K, used 173K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 8% used [0x00000000ffd00000,0x00000000ffd2b418,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 7151K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 99% used [0x00000000ff600000,0x00000000ffcfbc68,0x00000000ffd00000)
 Metaspace       used 3288K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

6.3 弱引用:发现即回收
  • 只被弱引用关联的对象只能生存到下一次垃圾收集发生为止

  • 发生GC的时候,不管系统堆空间是否充足,都会回收掉只被弱引用关联的对象

    但是垃圾回收器的线程通常优先级较低,因此并能够一定很快地发现持有弱引用的对象,在这种情况下,弱引用可以存在较长的时间

  • 在构造弱引用的时候,可以指定一个引用队列,当弱引用被回收的时候,就会加入该指定的引用队列,通过这个队列可以追踪对象的回收情况

  • 弱引用也适合保存那些可有可无的缓存数据,当系统内存不足的时候缓存数据就会被回收,从而不会导致内存的溢出,而当内存充足的时候缓存数据又能够存在相当长的时间,从而起到加速系统的作用

  • jdk1.2后通过java.lang.ref.WeakReference来实现软引用

       Object obj = new Object();
       WeakReference<Object> objectWeakReference = new WeakReference<>(obj);
    
       WeakReference<Object> objectWeakReference1 = new WeakReference<>(new Object());
    
  • WeakHashMap的内部类继承了WeakReference,意味着使用WeakHashMap会在内存不足的时候进行数据的回收

public class WeakReferenceTest {
    public static void main(String[] args) {
        Object obj = new Object();
        WeakReference<Object> objectWeakReference = new WeakReference<>(obj);
        obj = null;
        System.out.println(objectWeakReference.get());
        System.gc();
        System.out.println(objectWeakReference.get());
    }
}
java.lang.Object@677327b6
null
6.4 虚引用:对象回收跟踪
  • 虚引用又称幽灵引用幻影引用,是所有引用对象中最弱的一个

  • 一个对象是否有虚引用,完全不会决定对象的生命周期;对象仅持有虚引用,那么跟不持有任何引用是一样的,随时都有可能被垃圾回收器回收

  • 虚引用不能被单独使用,当尝试通过虚引用获取被引用的对象的时候,总会得到null

  • 为对象设置虚引用关联的目的在于跟踪对象的垃圾回收过程,比如在这个对象被回收的时候能够收到一个系统通知

  • 虚引用必须和引用队列一起使用:虚引用在创建的时候必须提供一个引用队列作为参数,当垃圾回收器回收对象发现有虚引用指向的时候,就把这个虚引用加入引用队列,以通知应用程序该对象的回收情况

    由于虚引用可以跟踪对象的回收时间,因此可以将一些资源释放操作放置在虚引用中执行和记录

  • jdk1.2之后提供PhantomReference来实现虚引用

package com.hikaru.java.reference;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceTest {
    public static PhantomReferenceTest obj;
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前方法的finalize");
        obj = this;
    }

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while(true) {
                if(phantomQueue != null) {
                    PhantomReference<PhantomReferenceTest> reft = null;
                    try {
                        reft = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if(reft != null) {
                        System.out.println("虚引用指向的实例被回收了");
                    }
                }
            }
        });
        // 设置线程为守护线程
        thread.setDaemon(true);
        thread.start();

        phantomQueue = new ReferenceQueue<>();
        obj = new PhantomReferenceTest();

        PhantomReference<PhantomReferenceTest> phantomRef
                = new PhantomReference<>(obj, phantomQueue);
        // 无法通过虚引用获取对象
        System.out.println(phantomRef.get());

        // 去掉强引用
        obj = null;
        // finalize方法会复活对象
        System.gc();
        try {
            Thread.sleep(1000);
            if(obj == null) {
                System.out.println("obj是null");
            } else {
                System.out.println("obj存活");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        obj = null;
        // finalize只能被调用一次,因此第二次垃圾回收会回收掉obj
        System.gc();
        try {
            Thread.sleep(1000);
            if(obj == null) {
                System.out.println("obj是null");
            } else {
                System.out.println("obj存活");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
null
调用当前方法的finalize
obj存活
虚引用指向的实例被回收了
obj是null
6.5 终结器引用
  • 它用以实现对象的finalize()方法,无需手动编码,内部配合队列使用
  • 在GC的时候,终结器引用入队,由Finalizer线程通过终结器引用找到被引用的对象调用它的finalize()方法,然后再第二次GC的时候才能回收该对象