JVM垃圾回收篇(重要概念解析)

Posted ProChick

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM垃圾回收篇(重要概念解析)相关的知识,希望对你有一定的参考价值。

1.System.gc方法

  • 基本概述

    • 默认情况下,通过调用 System.gc() 或者 Runtime.getRuntime().gc() 方法会显式触发 Full GC的回收,然后对堆空间、方法区进行回收,尝试释放被丢弃对象所占用的内存
    • 然而System.gc()方法无法保证对垃圾收集器的调用时间,也就是说方法执行后不能确保垃圾回收过程及时执行
    • 开发者可以在某些场景下通过 System.gc()方法手动调用JVM的GC行为,但是一般情况下,垃圾回收应该是由JVM自动进行的
  • 代码测试

    测试调用 System.gc() 垃圾回收器不会立即执行

    public class SystemGCTest {
        public static void main(String[] args) {
            new SystemGCTest();
            
            // 尝试多次运行,观察是否出现finalize里面的输出语句
            System.gc();
            
            // 如果调用此方法,则意味着强制执行finalize()方法
            // System.runFinalization(); 
        }
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("SystemGCTest重写了finalize()");
        }
    }
    

    测试不同对象的回收行为

    // 不会回收,因为对象引用存在
    public void localvarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        System.gc();
    }
    
    // 会回收,因为对象引用置空了
    public void localvarGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();
    }
    
    // 不会回收,因为虽然在代码块外buffer变量已经无效了,但对象引用依旧存在
    public void localvarGC3() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();
    }
    
    // 会回收,因为在局部变量表中,变量buffer的位置被变量num替代,对象失去引用
    public void localvarGC4() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int num = 10;
        System.gc();
    }
    
    // 会回收,因为方法执行结束后,栈帧出栈,对象失去引用
    public void localvarGC5() {
        localvarGC1();
        System.gc();
    }
    

2.内存溢出与泄露

  • 内存溢出概述

    • 内存溢出是引发程序崩溃的罪魁祸首之一
    • 内存溢出代表当前没有空闲内存,并且通过垃圾收集器进行回收后也无法提供更多内存
    • 内存溢出之前,通常伴随有垃圾收集器的触发,然后尽其所能去清理出空间。但是也不是在任何情况下垃圾收集器都会被触发的,当我们去分配一个比整个堆空间还要大的对象时,JVM完全可以判断出垃圾收集并不能解决这个问题,所以就会直接拋出OOM异常
    • 由于GC一直在发展,一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现O0M的情况
  • 内存溢出原因

    • 堆内存不足

      很有可能就是堆的大小设置不合理造成的,比如:我们要处理比较大的数据量,但是没有显式指定JVM堆空间的大小或者指定的数值偏小

    • 存在大对象

      代码中创建了大量的大对象,并且长时间不能被垃圾收集器回收。对于老版本的JDK来说,因为永久代的大小是有限的,并且JVM对永久代垃圾回收非常不频繁,所以当我们不断添加大类型对象的时候,永久代很容易出现内存溢出。对于新版本的JDK来说,虽然随着元数据区的引入,方法区内存已经不再那么窘迫,但是如果直接内存不足,也会导致内存溢出

  • 内存泄漏概述

    • 也称作“存储渗漏”,严格意义上来说,当出现对象不会再被程序用到了,但是GC又不能回收它们的情况,这才叫内存泄漏

    • 由于不规范的代码书写,出现了大量对象的生命周期很长,从而导致OOM,这种情况也可以叫做宽泛意义上的“内存泄漏”

    • 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步耗尽,最终导致程序崩溃

  • 内存泄漏举例

    • 在单例模式中,单例对象的生命周期和应用程序是一样长的,所以如果该对象又持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则有可能会导致内存泄漏的产生
    • 一些提供close方法的资源用完后要及时关闭,比如:数据库连接,网络连接、IO连接等,这些都必须手动调用close方法,否则有可能会导致内存泄漏的产生

3.Stop The World

  • Stop The World,简称STW,指的是在垃圾回收事件的发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,这个时间段称为STW
  • 可达性分析算法中枚举根节点的过程也会导致所有Java执行线程停顿
  • STW是JVM在后台自动发起和自动完成的,它是在用户不可见的情况下,把用户正常的工作线程全部停掉的
  • 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉程序像是卡顿一样, 所以我们需要减少STW的发生
  • STW的发生和采用哪款垃圾回收器无关,因为所有的垃圾回收器都会发生这个事件,只能说垃圾回收器越来越优秀,回收效率越来越高,这在一定程度上缩短了暂停时间而已

程序案例

public class StopTheWorldDemo {
    // 此线程不断的在进行垃圾回收
    public static class WorkThread extends Thread {
        List<byte[]> list = new ArrayList<byte[]>();

        public void run() {
            try {
                while (true) {
                    for(int i = 0;i < 1000;i++){
                        byte[] buffer = new byte[1024];
                        list.add(buffer);
                    }

                    if(list.size() > 10000){
                        list.clear();
                        System.gc();
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    // 此线程不断的打印时间
    public static class PrintThread extends Thread {
        public final long startTime = System.currentTimeMillis();

        public void run() {
            try {
                while (true) {
                    long t = System.currentTimeMillis() - startTime;
                   
                    System.out.println(t / 1000 + "." + t % 1000);
                    Thread.sleep(1000);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        WorkThread w = new WorkThread();
        PrintThread p = new PrintThread();
        w.start();
        p.start();
    }
}

从以下打印结果,我们发现打印的时间间隔明显要多于我们设定的1秒,说明在垃圾回收的过程中,发生了STW

4.垃圾回收的并行与并发

  • 系统并发

    • 在操作系统中,是指一个时间段中有几个程序都处于运行的状态,且这几个程序都是在同一个处理器上运行的
    • 并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片,然后在指定的时间片内来回进行程序的切换,由于CPU处理的速度非常快,只要时间间隔处理得当,那么给用户感觉是多个应用程序同时在进行
  • 系统并行

    • 在系统中存在多个CPU的时候( 或者一个CPU有多个处理核 ),一个CPU执行一个进程时,另一个CPU可以执行另一个进程,这两个进程互不抢占CPU资源,可以同时进行,我们称之为并行
    • 这种过程适合科学计算、后台处理等弱交互场景
  • 二者对比

    • 对于并发来讲,多个进程是在同一时间段内同时发生了,它们之间是互相抢占资源的
    • 对于并行来讲,多个进程是在同一时间点内同时发生了,它们之间是不互相抢占资源的
  • 垃圾回收

    • 串行:采用单线程执行,只允许一个垃圾收集线程在工作,此时用户线程将处于等待状态(STW)

    • 并行:指多个垃圾收集线程并行工作,此时用户线程将处于等待状态(STW)

    • 并发:指用户线程与垃圾收集线程在同一时间段内交替执行,降低了前面两种方式产生STW的时间

5.安全点与安全区域

  • 安全点

    • 在程序执行时,并非在所有地方都能停顿下来开始进行垃圾回收,只有在特定的位置才能停顿下来开始垃圾回收,那这些特定的位置我们称为“安全点 ( SafePoint ) ”
    • 安全点的选择很重要,如果设定的太少可能导致GC等待的时间很长,如果设定的太多可能导致运行时出现性能问题
    • 我们发现大部分指令的执行时间都非常短暂,所以通常会根据是否具有让程序长时间执行的特征为标准,选择那些执行时间较长的指令作为安全点, 比如:方法调用、循环跳转、异常跳转等

    如何判断在垃圾回收发生的时候,其它所有用户线程都到达了所谓的安全点呢?

    • 抢先式中断法:首先中断所有线程,如果发现还存在线程不在安全点,那就恢复该线程让它跑到安全点
    • 主动式中断法:设置一个中断标志,各个线程运行到安全点的时候主动查询这个标志。如果中断标志为真,则将该线程中断挂起,否则继续运行。这也是当下虚拟机所采用的的方式
  • 安全区域

    • 安全点保证了程序线程在正常执行时,线程能够响应JVM发出的信号中断请求。但是,当程序线程处于Sleep状态或Blocked状态时,该线程无法响应JVM的中断请求,同时JVM也不太可能等待线程被唤醒。所以对于这种情况,就需要设定安全区域来解决
    • 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始垃圾回收都是安全的
    • 当线程进入安全区域时,首先进行标识,如果这段时间内发生垃圾回收,则JVM会忽略这些已被标识的线程
    • 当线程离开安全区域时,首先检查JVM是否已经完成了垃圾回收工作,如果完成了,则允许线程离开,否则线程必须等待垃圾回收完成后离开

以上是关于JVM垃圾回收篇(重要概念解析)的主要内容,如果未能解决你的问题,请参考以下文章

JVM之内存与垃圾回收篇执行引擎

JVM学习笔记内存与垃圾回收篇

JVM学习笔记内存与垃圾回收篇

JVM垃圾回收2(垃圾收集算法)

JVM垃圾回收(下)

JVM垃圾回收篇(对象引用)