IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 垃圾回收概述(垃圾回收算法) -> 正文阅读

[Java知识库]垃圾回收概述(垃圾回收算法)

垃圾回收概述

img

  1. Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要程序员手动的收集。
  2. 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
  3. 关于垃圾收集有三个经典问题:
    • 哪些内存需要回收?
    • 什么时候回收?
    • 如何回收?
  4. 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。

大厂面试题

蚂蚁金服

  1. 你知道哪几种垃圾回收器,各自的优缺点,重点讲一下CMS和G1?
  2. JVM GC算法有哪些,目前的JDK版本采用什么回收算法?
  3. G1回收器讲下回收过程GC是什么?为什么要有GC?
  4. GC的两种判定方法?CMS收集器与G1收集器的特点

百度

  1. 说一下GC算法,分代回收说下
  2. 垃圾收集策略和算法

天猫

  1. JVM GC原理,JVM怎么回收内存
  2. CMS特点,垃圾回收算法有哪些?各自的优缺点,他们共同的缺点是什么?

滴滴

  1. Java的垃圾回收器都有哪些,说下G1的应用场景,平时你是如何搭配使用垃圾回收器的

阿里

  1. 讲一讲垃圾回收算法。
  2. 什么情况下触发垃圾回收?
  3. 如何选择合适的垃圾收集算法?
  4. JVM有哪三种垃圾回收器?

字节跳动

  1. 常见的垃圾回收器算法有哪些,各有什么优劣?
  2. System.gc()和Runtime.gc()会做什么事情?
  3. Java GC机制?GC Roots有哪些?
  4. Java对象的回收方式,回收算法。
  5. CMS和G1了解么,CMS解决什么问题,说一下回收的过程。
  6. CMS回收停顿了几次,为什么要停顿两次?

什么是垃圾

  1. 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
  2. 外文:An object is considered garbage when it can no longer be reached from any pointer in the running program.
  3. 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出

十几年前磁盘碎片整理的日子

img

为什么需要GC?

想要学习GC,首先需要理解为什么需要GC?

  1. 对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
  2. 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象
  3. 随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。

早期垃圾回收

  • 在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。比如以下代码:
 MibBridge *pBridge= new cmBaseGroupBridge();
 //如果注册失败,使用Delete释放该对象所占内存区域
 if(pBridge->Register(kDestroy)!=NO ERROR)
     delete pBridge;
  • 这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃

  • 有了垃圾回收机制后,上述代码极有可能变成这样

 MibBridge *pBridge=new cmBaseGroupBridge(); 
 pBridge->Register(kDestroy);
  • 现在,除了Java以外,C#、Python、Ruby等语言都使用了自动垃圾回收的思想,也是未来发展趋势,可以说这种自动化的内存分配和来及回收方式已经成为了现代开发语言必备的标准。

垃圾回收机制 *

官网介绍:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html

自动内存管理的优点

  1. 自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
  2. 没有垃圾回收器,java也会和cpp一样,各种悬垂指针,野指针,泄露问题让你头疼不已。
  3. 自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发

关于自动管理的担忧

  1. 对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力
  2. 此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见OutofMemoryError时,快速地根据错误异常日志定位问题和解决问题。
  3. 当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节

应该关心那些地方的回收?

img

  1. 垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收,
  2. 其中,Java堆是垃圾收集器的工作重点
  3. 从次数上讲:
    1. 频繁收集Young区
    2. 较少收集Old区
    3. 基本不收集Perm区(元空间)

垃圾回收相关的算法(面试高频)

标记阶段:引用计数算法

标记阶段的目的

垃圾标记阶段:主要是为了判断对象是否存活

  1. 在堆里存放着几乎所有的Java对象实例,在GC执行回收之前,首先 需要区分内存或中那些是存活对象,那些是已经死亡的对象。 只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以成为 垃圾标记阶段.
  2. 那么在JVM究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
  3. 判断对象存活一般有两种方式: 引用计数算法和可达性分析算法。

引用计数算法

  1. 引用计数算法(Reference Counting)比较简单,对每个对象保存一个整形的引用计数器属性。用于记录对象被引用的情况。
  2. 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加一;当引用失效时,引用计数器就减1.只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
  3. 优点 : 实现简单,垃圾对象便于辨识,判断效率高,回收没有延迟性
  4. 缺点:
    1. 它需要单独的字段存储计数器,这样的做法增强了 存储空间的开销
    2. 每次复制都需要更新计数器,伴随着加法和减法操作,这增加了 时间开销
    3. 引用计数器有一个严重的问题,即 无法处理循环引用的情况。 这是一条致命缺陷,导致在Java的垃圾回收器中没有这类算法。

img

当p的指针断开的时候,内部的引用形成一个循环,计数器都还算1,无法被回收,这就是循环引用,从而造成内存泄漏

证明:java使用的不是引用计数器

/**
 * -XX:+PrintGCDetails
 * 证明:java使用的不是引用计数算法
 */
public class RefCountGC {
    //这个成员属性唯一的作用就是占用一点内存
    private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB

    Object reference = null;

    public static void main(String[] args) {
        RefCountGC obj1 = new RefCountGC();
        RefCountGC obj2 = new RefCountGC();

        obj1.reference = obj2;
        obj2.reference = obj1;

        obj1 = null;
        obj2 = null;
        //显式的执行垃圾回收行为
        //这里发生GC,obj1和obj2能否被回收?
        System.gc();

    }
}

img

  • 如果不小心直接把obj1.referenceobj2.reference置为null。则在Java堆中的两块内存依然保持着互相引用,无法被回收

没有进行GC时

把下面的几行代码注释掉,让它来不及

        System.gc();//把这行代码注释掉
Heap
 PSYoungGen      total 38400K, used 14234K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)
  eden space 33280K, 42% used [0x00000000d5f80000,0x00000000d6d66be8,0x00000000d8000000)
  from space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)
  to   space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)
 ParOldGen       total 87552K, used 0K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)
  object space 87552K, 0% used [0x0000000081e00000,0x0000000081e00000,0x0000000087380000)
 Metaspace       used 3496K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

Process finished with exit code 0

进行GC

打开那行代码的注释

[GC (System.gc()) [PSYoungGen: 13569K->808K(38400K)] 13569K->816K(125952K), 0.0012717 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 808K->0K(38400K)] [ParOldGen: 8K->670K(87552K)] 816K->670K(125952K), [Metaspace: 3491K->3491K(1056768K)], 0.0051769 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 38400K, used 333K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)
  eden space 33280K, 1% used [0x00000000d5f80000,0x00000000d5fd34a8,0x00000000d8000000)
  from space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)
  to   space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)
 ParOldGen       total 87552K, used 670K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)
  object space 87552K, 0% used [0x0000000081e00000,0x0000000081ea7990,0x0000000087380000)
 Metaspace       used 3498K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

Process finished with exit code 0

1、从打印日志就可以明显看出来,已经进行了GC

2、如果使用引用计数算法,那么这两个对象将会无法回收。而现在两个对象被回收了,说明Java使用的不是引用计数算法来进行标记的。

小结

  1. 引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。
  2. 具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。
  3. Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
  4. Python如何解决循环引用?
    • 手动解除:很好理解,就是在合适的时机,解除引用关系。
    • 使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。

标记阶段:可达性分析算法

可达性分析算法:也可以成为根搜素算法、追踪性垃圾收集

  1. 相较于引用技术算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地 解决引用计数算法中的循环引用的问题,防止内存泄漏的发生。
  2. 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作 追踪性垃圾收集(Tracing Garbage Collection)

可达性分析实现思路

  • 所谓"GCRoots"根集合就是一组必须活跃的引用
  • 其基本思路如下:
  1. 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式 搜素被根对象集合所连接的目标对象是否可达。
  2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为 引用链(Reference Chain)
  3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
  4. 在可达性分析算法中,只有能够被根对象结合直接或者间接连接的对象才是存活对象。

img

GC Roots可以是哪些元素?

  1. 虚拟机栈中引用的对象
    • 比如:各个线程被调用的方法中使用到的参数、局部变量等。
  2. 本地方法栈内JNI(通常说的本地方法)引用的对象
  3. 方法区中类静态属性引用的对象
    • 比如:Java类的引用类型静态变量
  4. 方法区中常量池引用的对象
    • 比如:字符串常量池(StringTable)的引用
  5. 所有被同步锁synchronize持有的对象
  6. Java虚拟机内部的引用
    • 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutofMemoryError),系统类加载器。
  7. 反应java虚拟机内部情况的JMXBean、jVMTI中注册的回调、本地代码缓存等。

6

  1. 总结一句话就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,都可以作为GC Roots进行可达性分析
  2. 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分带收集和局部回收(PartialGC)
    • 如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。

小技巧

由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root.

注意

  1. 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
  2. 这点也导致GC进行时必须“Stop The World”的一个重要原因。即使号称(几乎)不会发生挺短的CMS收集器中, 枚举根节点也是必须要停顿的。

对象的finalization机制

finalize()方法机制

对象销毁钱的回调函数:finalize()

  1. Java语言提供了对象终止(finalization)机制来允许开发人员提供 对象被销毁之前的自定义处理逻辑,
  2. 当垃圾回收器发现没有引用指向一个对象,即: 垃圾回收此对象之前,都会先调用这个对象的finalize()方法。
  3. finalize()方法允许在子类中被重写, ***用于在垃圾被回收时进行资源释放。***通常在这个方法中进行一些资源释放和清理的工作,比如关闭文集、套接字和数据库连接等。

Object类中finalize()源码

// 等待被重写
protected void finalize() throws Throwable { }
  1. 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
    • 在finalize()时可能会导致对象复活。
    • finali()方法的执行时间是没有保障的,它完全有GC线程决定,极端情况下,若不发生GC,则finalize()没有执行机会。
    • 一个糟糕的finalize()会严重影响GC的性能。比如finalize是个死循环。
  2. 从功能上说,finalize(0方法与c++的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所有finalize()方法在 本质不同于C++的中的析构函数。
  3. finalize()方法对应一个finalize线程,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收。

生存还是死亡?

由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。

  1. 如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂停处于“缓刑”阶段。 一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它立即进行回收就是不合理的。为此,定义虚拟机的对象可能的三种状态。如下:
    • 可触及的:从根节点开始,可以到达这个对象。
    • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
    • 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及的状态。不可触及的对象不可能被复活, 因为finalize()只会被调用一个。
  2. 以上3中状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。

具体过程

判断一个对象objA是否可回收,至少要经历两次标记过程:

  1. 如果对象objA到GC Roots没有引用链,则进行第一次标记。
  2. 进行筛选,判断此对象是否有必要执行finalize()方法
    • 如果对象objA没有重写finalize()方法,或者finalize()方法 已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定不可触及的。
    • 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的FInalizer线程触发其finalize()方法执行。
    • finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上任何一个对象建立了联系,那么在第二次标记时,objA会移除“即将回收”集合。之后,对象会再次出现没有引用存在的情况,在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及状态,也就是说,一个对象的finalize()方法只会被调用一次。

通过 JVisual VM 查看 Finalizer 线程

img

代码演示:

我们重写 CanReliveObj 类的 finalize()方法,在调用其 finalize()方法时,将 obj 指向当前类对象 this

/**
 * 测试Object类中finalize()方法,即对象的finalization机制。
 *
 */
public class CanReliveObj {
    public static CanReliveObj obj;//类变量,属于 GC Root


    //此方法只能被调用一次
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前类重写的finalize()方法");
        obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
    }


    public static void main(String[] args) {
        try {
            obj = new CanReliveObj();
            // 对象第一次成功拯救自己
            obj = null;
            System.gc();//调用垃圾回收器
            System.out.println("第1次 gc");
            // 因为Finalizer线程优先级很低,暂停2秒,以等待它
            Thread.sleep(2000);
            if (obj == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
            System.out.println("第2次 gc");
            // 下面这段代码与上面的完全相同,但是这次自救却失败了
            obj = null;
            System.gc();
            // 因为Finalizer线程优先级很低,暂停2秒,以等待它
            Thread.sleep(2000);
            if (obj == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1次 gc调用当前类重写的finalize()方法obj is still alive第2次 gcobj is dead

MAT与JProfiler的GC Roots溯源(番外)

MAT介绍

  1. MAT是Memory Analyzer的简称,它是一款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。
  2. MAT是基于Eclipse开发的,是一款免费的性能分析工具。
  3. 大家可以在http://www.eclipse.org/mat/下载并使用MAT

1、虽然Jvisualvm很强大,但是在内存分析方面,还是MAT更好用一些

2、此小节主要是为了实时分析GC Roots是哪些东西,中间需要用到一个dump的文件

获取dump文件的方式

方式一:命令行使用jmap

img

方式二:使用JvisaulVM

  1. 捕获的heap dump文件是一个临时文件,关闭JVisualVM后自动删除,若要保留,需要将其另存为文件。可通过以下方法捕获heap dump:
  2. 操作步骤下面演示

捕捉 dump 示例

使用JVisualVM捕捉 heap dump

代表:

  • numList和birth在第一次抓捕内存快照的时候,为GC Roots
  • 之后numList和birth置为null,对应的引用对象被回收,唉第二次抓捕内存快照的时候,就不再是GC Roots
public class GCRootsTest {    public static void main(String[] args) {        List<Object> numList = new ArrayList<>();        Date birth = new Date();        for (int i = 0; i < 100; i++) {            numList.add(String.valueOf(i));            try {                Thread.sleep(10);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        System.out.println("数据添加完毕,请操作:");        new Scanner(System.in).next();        numList = null;        birth = null;        System.out.println("numList、birth已置空,请操作:");        new Scanner(System.in).next();        System.out.println("结束");    }}

如何捕捉堆内存快照

1、先执行第一步,然后停下来,去生成此步骤dump文件

img

2、 点击【堆 Dump】

img

3、右键 --> 另存为即可

img

4、输入命令,继续执行程序

img

img

5、我们接着捕获第二张堆内存快照

img

使用 MAT 查看堆内存快照

1、打开 MAT ,选择File --> Open File,打开刚刚的两个dump文件,我们先打开第一个dump文件

点击Open Heap Dump也行

img

2、选择Java Basics --> GC Roots

img

3、第一次捕捉堆内存快照时,GC Roots 中包含我们定义的两个局部变量,类型分别为 ArrayList 和 Date,Total:21

img

4、打开第二个dump文件,第二次捕获内存快照时,由于两个局部变量引用的对象被释放,所以这两个局部变量不再作为 GC Roots ,从 Total Entries = 19 也可以看出(少了两个 GC Roots)

img

JProfiler GC Roots 溯源

1、在实际开发中,我们很少会查看所有的GC Roots。一般都是查看某一个或几个对象的GC Root是哪个,这个过程叫GC Roots 溯源

2、下面我们使用使用 JProfiler 进行 GC Roots 溯源演示

依然用下面这个代码

img

img

img

img

img

4、选择Incoming References,表示追寻 GC Roots 的源头

点击Show Paths To GC Roots,在弹出界面中选择默认设置即可

imgimgimg

JProfiler 分析 OOM

这里是简单的讲一下,后面篇章会详解

/** * -Xms8m -Xmx8m  * -XX:+HeapDumpOnOutOfMemoryError  这个参数的意思是当程序出现OOM的时候就会在当前工程目录生成一个dump文件 */public class HeapOOM {    byte[] buffer = new byte[1 * 1024 * 1024];//1MB    public static void main(String[] args) {        ArrayList<HeapOOM> list = new ArrayList<>();        int count = 0;        try{            while(true){                list.add(new HeapOOM());                count++;            }        }catch (Throwable e){            System.out.println("count = " + count);            e.printStackTrace();        }    }}

程序输出日志

com.atguigu.java.HeapOOMjava.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid14608.hprof ...java.lang.OutOfMemoryError: Java heap space    at com.atguigu.java.HeapOOM.<init>(HeapOOM.java:12)    at com.atguigu.java.HeapOOM.main(HeapOOM.java:20)Heap dump file created [7797849 bytes in 0.010 secs]count = 6

打开这个dump文件

1、看这个超大对象

img

2、揪出 main() 线程中出问题的代码

img

垃圾收集算法 *

垃圾清除阶段

  • 当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的内存空间为新对象分配内存。目前在JVM中比较常见的垃圾收集算法是
    1. 标记-清除算法(Mark-Sweep)
    2. 标志-复制算法(Copying)
    3. 标志-压缩算法(MArk-Compact)

标记-清除算法 *

背景

最早出现也是最基础的垃圾收集算法,在1960年由Lisp之父John MCCarty所提出。

执行过程

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也称STW),然后进行两项工作,第一项是标记,第二项是清除

  1. 标记:Collector从引用根节点开始遍历,标记所有被引用对象,一般是在对象的Header(头信息)中记录是可达对象。
    • 注意:标***记的是被引用的对象,也就是可达对象,并非标记的是即将清除的垃圾对象***
  2. 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在Header中没有标记可达对象,则将其回收。

img

标记-清除算法的缺点

  1. 标记清除算法的效率不算高
  2. 在进行GC的时候,需要停止整个应用程序,用户体验较差
  3. 在这种方式清除出来的空闲内存是不连续的,会产生碎片,需要维护一个空闲列表

注意:何为清除?

这里所谓的清除并不是真的置空,而是把需要清除的对象保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放(也就是覆盖原来的地址)。

关于空闲列表是在对象分配内存的时候提过:

  1. 如果内存规整
    • 采用指针碰撞的方式进行内存分配
  2. 如果内存不规整
    • 虚拟机需要维护一个空闲列表
    • 采用空闲列表分配内存

清除阶段:复制算法*

背景

  1. 为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。

核心思想

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清楚正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收(比如幸存者0/幸存者1 [from to])

img

新生代里面就用到了复制算法,Eden区和S0区存活对象整体复制到S1区

复制算法的优缺点

优点

  1. 没有标记和清除过程,实现简单,运行高效。
  2. 复制过去以后保证空间的连续性,不会出现“碎片”问题。

缺点

  1. 此算法的缺点也是很明显的,就是需要两倍的内存空间。
  2. 对于G1这种分拆成大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小

复制算法的应用场景

  1. 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,效率较高
  2. 老年代大量的对象存活,那么复制的对象将会有很多,效率会很低
  3. 在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高,缩影现在的商业虚拟机都是用这种收集算法回收新生代。

img

清除阶段:标记-压缩算法 *

标记-压缩(或标记-整理、Mark-Compact)算法

背景

  1. 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是老年代,更常见的是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也很高。因此,基于老年代垃圾回收的特性,需要用到其他的算法。
  2. 标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行玩内存回收后还会产生内存碎片,所以JVM的设计者需要在基础之上进行改进。 标记-算法由此诞生。
  3. 1970年前后,G.L.Steele、C.J.Chene和D.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。

执行过程

  1. 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象

  2. 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。

    img

标记-压缩算法与标记=清除算法的比较

  1. 标记-压缩算法的最终效果等同于标记-清除算法那执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩算法。
  2. 二者的本质差异在于标记-清除算法是一种 非移动式的回收算法, 标记-压缩是 移动式的。 是否移动回收后的存活对象是一项优缺点并存的风险决策。
  3. 可以看到,标记得到存活对象会被整理,按照内存地址依次排序,而未被标记的内存会被清理掉。这样一来,当我们需要给新对象分配内存时,JVM只需要迟持有一个内存的起始地址即可,这样比维护一个空闲列表显然少了许多开销。

标记-压缩算法的优缺点

优点

  1. 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只要迟永一个内存的起始地址即可。
  2. 消除了复制算法当中,内存减半的高额代价。

缺点

  1. 从效率上来说,标记-整理算法要地域复制算法
  2. 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址(因为HotSpot虚拟机采用的不是句柄池的方式,而是直接指针)
  3. 移动过程中,需要全程暂停用户应用程序。即:STW

垃圾回收算法小结

对比三种清除阶段的算法

  1. 从效率来说,复制算法是当之无愧的老大,但是却浪费了太多内存
  2. 而为了尽量兼顾上面提到的三个指标,标记-整理算法相对于更平滑一些,但是效率上不尽人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
标记清除标记整理复制
速率中等最慢最快
空间开销少(但会堆积碎片)少(不堆积碎片)通常需要活对象的2倍空间(不堆积碎片)
移动对象

分代收集算法

为什么要使用分代收集算法

  1. 前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都有自己独特的优势和特点。分代收集算法应运而生。
  2. 分代收集算法,是基于这样的一个事实:***不同对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。***一般Java是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高回收的效率。
  3. 在Java程序运行的过程中,会产生大量的对象,其中有些对象是于业务信息相关:
    • 比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此声明周期比较长。
    • 但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

目前几乎所有的GC都采用分代收集算法执行垃圾回收的

在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻和老年代各自的特点。

  1. 年轻代(Young Gen)
    • 年轻代的特点:区域相对老年代较小,对象生命周期短、存活率低、回收频繁。
    • 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过Hotspot中的两个survior的设计得到缓解。
  2. 老年代(Tenured Gen)
    • 老年代特点:区域较大、对象生命周期长、存活率高,回收不及老年代频繁。
    • 这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般都由标记-清除或者是标记-整理的混合实现。
      • Mark阶段的开销与存活对象的数量成正比。(标记)
      • Sweep阶段的开销与管理区域的大小成正相关。(清理)
      • Compact阶段的开销与存活对象的数据成正比。(整理)
  3. 以Hotspot中的CMS回收器为例,CMS是基于Mark-Sweep(标记-清除)实现的,对于对象的回收效率很高。对于碎片问题,CMS采用基于Mark-Compact(标记-整理)的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),采用Serial Old执行Full GC以达到老年代内存的管理。
  4. 分代的想被现有的虚拟机广泛使用,几乎所有的垃圾回收器都区分新生代和老年代

增量收集算法和分区算法*

增量收集算法

上述现有的算法,在垃圾回收过程中,应用软件将处于Stop The World的状态。在STW状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或系统的稳定性。为了解决这个问题,对于实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生。

增量收集算法的基本思想

  1. 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾回收线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直至垃圾收集完成。
  2. 总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过 对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

增量收集算法的缺点

使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文的转换消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分区算法

主要针对G1收集器来说

  1. 一般来说,在相同条件下,堆空间越大,一次GC时所需要的的时间越长,有关GC产生的停顿也变长了。为了更好地控制GC产生的停顿时,将一大块内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
  2. 分代算法将对象的生性周期长短划分两个部分,分区算法将整个堆空间划分为连续的不同的小区间。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

img

写在最后

注意,这些只是基本的算法思路,实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是复合算法,并且并行和并发兼备。

垃圾回收相关概念

System.gc()的理解

  1. 在默认情况下,通过System.gc()或Runtime.getRuntime().gc()的调用,就会显示触发Full Gc,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
  2. 然而System.gc()调用附带一个免责声明,无法保证对垃圾回收器的调用(不能确保立即生效)
  3. JVM实现着可以通过System.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,***无须手动触发,否则就太过于麻烦了。***在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()
public class SystemGCTest {
    public static void main(String[] args) {
        new SystemGCTest();
        System.gc();//提醒jvm的垃圾回收器执行gc,但是不确定是否马上执行gc
        //与Runtime.getRuntime().gc();的作用一样。

//        System.runFinalization();//强制调用使用引用的对象的finalize()方法
    }
    //如果发生了GC,这个finalize()一定会被调用
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest 重写了finalize()");
    }
}

输出结果不确定:有时候会调用 finalize() 方法,有时候并不会调用

SystemGCTest 重写了finalize()
或
空

手动GC理解不可达对象的垃圾回收行为

//加上参数:  -XX:+PrintGCDetails
public class LocalVarGC {
    public void localvarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024];//10MB
        System.gc();
    }

    public void localvarGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();
    }

    public void localvarGC3() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();
    }

    public void localvarGC4() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int value = 10;
        System.gc();
    }

    public void localvarGC5() {
        localvarGC1();
        System.gc();
    }

    public static void main(String[] args) {
        LocalVarGC local = new LocalVarGC();
        //通过在main方法调用这几个方法进行测试
        local.localvarGC1();
    }
}

JVM参数:

-Xms256m -Xmx256m -XX:+PrintGCDetails -XX:PretenureSizeThreshold=15m

内存溢出与内存泄漏

内存溢出

  1. 内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
  2. 由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况
  3. 大多情况下,GC会进行各种年龄代的垃圾,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
  4. Javadoc中对OOm的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。

OOM原因分析

首先说没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有二:

  1. Java虚拟机的堆内存设置不够。

    • 比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数-Xms 、-Xmx来调整。
  2. 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

    • 对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见。尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:PermGen space"。
    • 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息则变成了:“java.lang.OutofMemoryError:Metaspace"。直接内存不足,也会导致OOM。
  3. 这里面隐含着一层意思是,在抛出OutofMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。

    • 例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。
    • 在java.nio.Bits.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。
  4. 当然,也不是在任何情况下垃圾收集器都会被触发的

    • 比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutofMemoryError。

内存泄漏

  1. 也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
  2. 但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。
  3. 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutofMemory异常,导致程序崩溃。
  4. 注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。

官方的例子

左边的图:Java使用可达性分析算法,最上面的数据不可达,就是需要被回收的对象。

右边的图:后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开(图示中的Forgotten Reference Memory Leak),从而导致没有办法被回收。

img

常见例子

  1. 单例模式
    • 单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
  2. 一些提供close()的资源未关闭导致内存泄漏
    • 数据库连接 dataSourse.getConnection(),网络连接socket和io连接必须手动close,否则是不能被回收的。

Stop the World

  1. Stop-the-World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
  2. 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿,为什么需要停顿所有 Java 执行线程呢?
    • 分析工作必须在一个能确保一致性的快照中进行
    • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
    • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
  3. 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
  4. STW事件和采用哪款GC无关,所有的GC都有这个事件。
  5. 哪怕是G1也不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
  6. STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
  7. 开发中不要用System.gc() ,这会导致Stop-the-World的发生。

代码感受

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();//会触发full gc,进而会出现STW事件
                     
                    }
                }
            } 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();
    }
}

关闭工作线程 w ,观察输出:当前时间间隔与上次时间间隔基本是每隔1秒打印一次

0.1
1.1
2.2
3.2
4.3
5.3
6.3
7.3

Process finished with exit code -1

开启工作线程 w ,观察打印输出:当前时间间隔与上次时间间隔相差 1.3s ,可以明显感受到 Stop the World 的存在

0.1
1.4
2.7
3.8
4.12
5.13

Process finished with exit code -1

垃圾回收的并行与并发

并行的概念

  1. 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行
  2. 并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换。由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行

img

并发的概念

  1. 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)
  2. 其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行
  3. 适合科学计算,后台处理等弱交互场景

img

并发与并行的对比

  1. 并发,指的是多个事情,在同一时间段内同时发生了。
  2. 并行,指的是多个事情,在同一时间点上(或者说同一时刻)同时发生了。
  3. 并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的。
  4. 只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

垃圾回收的并行与执行

  1. 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
    • 如ParNew、Parallel Scavenge、Parallel Old
  2. 串行(Serial)
    • 相较于并行的概念,单线程执行。
    • 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收(单线程)

img

并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:

  1. 并发(Concurrent):指

    用户线程与垃圾收集线程同时执行

    (但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。

    • 比如用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
  2. 典型垃圾回收器:CMS、G1

img

HotSpot的算法实现细节

根节点枚举

  1. 固定可作为GC Roots的节点主要是在全局性的引用(例如常量或静态属性)与执行上下文(例如栈帧中的本地变量表)中,尽管目标明确,但查找过程要做到高效并非一件容易的事情,现在Java应用越做越庞大,光是方法区的大小就常有数百上M,里面的类、常量更是恒河沙数,若是要逐个检查以这里为起源的引用肯定要消耗不少时间。
  2. 迄今为止,***所有的收集器在根节点枚举这一步骤时都是要必须暂停用户线程的,***因此毫无疑问根节点枚举与之前的整理内存碎片一样面临相似的“STOP The World”。现在可达性分析算法耗时 最长的查找引用链的过程已经可以做到与用户现场一起并发,但根节点枚举始终还是必须在在一个能保障一致性的快照中才得以进行 这里“一致性”意思是整个枚举期间执行子系统看起来就像被冻结到某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况,若这点不能满足的话,分析结果准确性也就无法保证。这是导致垃圾收集过程必选停顿所有用户线程的其中一个重要原因,即使号称停顿时间可控,或者(几乎)不会发生停顿的CMS、C1、ZGC等收集器,枚举根节点时也是必须要停顿的。
  3. 由于目前主流Java虚拟机使用的都是 准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有 执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。在HotSpot的解决方案里,是使用一组 ***OopMap的数据结构***来达到这个目的。一旦类加载动作完成的时候,HotSpot就会吧对象内什么偏移量是什么类型的数据类型计算出来,在即时编译过程中,也会在特定的位置记录下栈李和寄存器哪些位置是引用。这样收集器在扫描的时就可以直接得知这些信息了, 并不需要正在一个不漏地从方法区等 GC Roots开始寻找。
  4. Exact Vm因它使用 准确式内存管理。准确式内存管理是指虚拟机可以知道内存某个位置的数据具体什么类型。譬如内存中有一个32bit的整数123456,虚拟机将有能力分辨出它到底是指向了123456的内存地址的引用类型还是一个数值为123456的整数,准确分辨出哪些内存是引用类型,这也是垃圾收集时准确判断堆上的数据是否还可能被使用的前提。

常考面试:在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举

安全点与安全区域

安全点(SafePoiont)

  1. 程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”
  2. 安全点的选择很重要,***如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。***大部分指令的执行时间都非常短暂,通常会根据“***是否具有让程序长时间执行的特性”***为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  1. 抢先式中断:(目前没有虚拟机采用)首先中断所有的线程。如果还有线程不在安全点上,就恢复线程,让线程跑到安全点。
  2. 主动式中断:设置一个中断标志,各个线程运行到安全点的时候都会主动轮询这个标志,如果标志为真,则将自己进行的中断挂起。

安全区域

  1. 安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的安全点。但是,程序“不执行”的时候呢?
  2. 例如线程处于Sleep状态或者Blocked状态,这时候线程都无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要 ***安全区域(Safe Region)***来解决。
  3. ***安全区域是指在一段代码片段中,对象的引用关系不会发生改变,在这个区域的任何位置开始GC都是安全的。***我们也可以把安全区域看作是被拓展的安全点。

安全区域的执行流程

  1. 当线程运行到安全区域的代码时,首先标识已经进入了安全区域,如果这段时间内发生GC,JVM会忽略标识为安全区域状态的线程
  2. 当线程即将离开安全区域时,会检查JVM是否已经完成了根节点枚举(即GC Roots的枚举),如果完成了,则继续运行,否则线程必须等待直到收到了可以安全离开安全区域的信号位置;

记忆与卡表

前言

什么是跨代引用?

  1. 一般的垃圾回收算法至少会划分出两个年代:年轻代和老年代。但是单纯的分代理论在垃圾回收的时候存在一个巨大的缺陷:为了找到年轻代中的存活对象,却不得不比那里整个老年代,反过来也是一样的。

    img

  2. 如果我们从年期待开始遍历,那么可以断定N,S,P,Q都是存活对象。但是,V却不会被认为是存活对象,其占据的内存会被回收了。这就是一个惊天的大漏洞!因为U本身是老年代对象,而且有外部引用指向它,也就是说U是存活对象,而U指向了V,也就是说V也应该是存活对象才是!而这都是因为我们只遍历年轻代对象!

  3. 所以,为了解决这种跨代引用的问题,最笨的办法就是遍历老年代的对象,找出这些跨代引用来。这种方案存在极大的性能浪费。因为从两个分代假说里面,其实隐含了一个推论:跨代引用是极少的。也就是为了找出那么一点点跨代引用,我们却得遍历整个老年代!从上图来说,很显然的是,我们根本不必遍历R。

  4. 因此,为了避免这种遍历老年代的性能开销,通常的分代垃圾回收器会引入一种称为记忆集的技术。简单来说,记忆集就是用来记录跨代引用的表。

记忆表和卡表

  1. 解决对象跨代引用所带来的问题,垃圾收集器在新生代中建 立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题,因此我们有必要进一步 理清记忆集的原理和实现方式,以便在后续章节里介绍几款最新的收集器相关知识时能更好地理解。
  2. 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。如果我们不考虑效和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构。

比如说我们有老年代(非收集区域)和年轻代(收集区域)的对象之间有一条引用链

3、这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾 收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针 就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为 粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范 围以外的)的记录精度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

4、其中,第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,这也是 目前最常用的一种记忆集实现形式,一些资料中甚至直接把它和记忆集混为一谈。前面定义中提到记 忆集其实是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的 具体实现。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。 关于卡表与记忆集的关系,读者不妨按照Java语言中HashMap与Map的关系来类比理解。 卡表最简单的形式可以只是一个字节数组,而HotSpot虚拟机确实也是这样做的

引用的概述(面试常问)

  1. 我们希望能描述这一类对象:当内存空间害足够时,则能保留在内存中;如果内存空间在进行垃圾收集还是很紧张,则可以抛弃这些对象。
  2. 既偏门又非常高频的面试题:强引用,软引用,弱引用,虚引用有什么区别?具体使用场景是什么?
  3. 在JDK1.2版之后,Java对引用的概念进行扩充,将引用分为了:
    • 强引用(Strong Reference)
    • 软引用(Soft Reference)
    • 弱引用(Weak Reference)
    • 虚引用(Phanotom Reference)
  4. 这4中引用强逐渐递减。除强引用外,其中3中引用可以在java.lang.ref包中找到它们的身影。如下图,显示了这3中引用类型对应的类,开发人员可以在应用程序中使用它们。

img

Reference子类中只有终结器引用是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用

  1. 强引用:最传统“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似==Object obj = new Object()==这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。宁可抱OOM,也不会GC强引用
  2. 软引用:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回首之后还没有足够的内存,才会抛出内存溢出异常。
  3. 弱引用:被弱引用关联的对象只能生存到下一个垃圾回收之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉弱引用关联的对象。
  4. 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时会收到一个通知

强引用

  1. 在Java程序中,最常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们最常见的普通对象引用, 也是默认的引用类型。
  2. 当在Java语言中使用new操作符创建一个新的独享,并将其赋值给一个变量的时候,这个变量就称为指向该对象的一个强引用。
  3. ***只要强引用的对象是可触及的,垃圾收集器就永远不回回收掉被引用的对象。***只要强引用的对象可达的,jvm宁可报OOM,也不会回收强引用。
  4. 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,就是可以当做垃圾收集了,当然具体回收时机还是要看垃圾收集策略。
  5. 相对的,软引用、弱引用和虚已用的对象是软可触及、弱可触及和虚初级的,在一定条件下,都是可以被回收的。所以,强引用是造成Java内存泄漏的主要原因之一。

强引用代码举例

public class StrongReferenceTest {
    public static void main(String[] args) {
        StringBuffer str = new StringBuffer ("Hello,尚硅谷");
        StringBuffer str1 = str;

        str = null;
        System.gc();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(str1);
    }
}

输出

Hello,尚硅谷

局部变量str指向stringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是stringBuffer实例的强引用对应内存结构:

StringBuffer str = new StringBuffer(“hello,尚硅谷”);

img

总结

本例中的两个引用,都是强引用,强引用具备以下特点:

  1. 强引用可以直接访问目标对象。
  2. 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
  3. 强引用可能导致内存泄漏。

软引用

软引用(Soft Reference):内存不足即回收

  1. 软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。注意,这里的第一次回收是不可达的对象
  2. 软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
  3. 垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)。
  4. 类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。
  5. 一句话概括:当内存足够时,不会回收软引用可达的对象。内存不够时,会回收软引用的可达对象

在JDK1.2版之后提供了SoftReference类来实现软引用

Object obj = new Object();// 声明强引用SoftReference<Object> sf = new SoftReference<>(obj);obj = null; //销毁强引用

软引用代码举例

代码

public class SoftReferenceTest {    public static class User {        public User(int id, String name) {            this.id = id;            this.name = name;        }        public int id;        public String name;        @Override        public String toString() {            return "[id=" + id + ", name=" + name + "] ";        }    }    public static void main(String[] args) {        //创建对象,建立软引用//        SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));        //上面的一行代码,等价于如下的三行代码        User u1 = new User(1,"songhk");        SoftReference<User> userSoftRef = new SoftReference<User>(u1);        u1 = null;//取消强引用        //从软引用中重新获得强引用对象        System.out.println(userSoftRef.get());        System.out.println("---目前内存还不紧张---");        System.gc();        System.out.println("After GC:");//        //垃圾回收之后获得软引用中的对象        System.out.println(userSoftRef.get());//由于堆空间内存足够,所有不会回收软引用的可达对象。        System.out.println("---下面开始内存紧张了---");        try {            //让系统认为内存资源紧张、不够//            byte[] b = new byte[1024 * 1024 * 7];            byte[] b = new byte[1024 * 7168 - 635 * 1024];        } catch (Throwable e) {            e.printStackTrace();        } finally {            //再次从软引用中获取数据            System.out.println(userSoftRef.get());//在报OOM之前,垃圾回收器会回收软引用的可达对象。        }    }}

JVM参数

-Xms10m -Xmx10m

在 JVM 内存不足时,会清理软引用对象

输出结果:

[id=1, name=songhk] ---目前内存还不紧张---After GC:[id=1, name=songhk] ---下面开始内存紧张了---nulljava.lang.OutOfMemoryError: Java heap space    at com.atguigu.java1.SoftReferenceTest.main(SoftReferenceTest.java:48)Process finished with exit code 0

弱引用

  1. 弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象
  2. 但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
  3. 弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。
  4. 软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

在JDK1.2版之后提供了WeakReference类来实现弱引用

// 声明强引用Object obj = new Object();WeakReference<Object> sf = new WeakReference<>(obj);obj = null; //销毁强引用

弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。

面试题:你开发中使用过WeakHashMap吗?

弱引用代码举例

public class WeakReferenceTest {    public static class User {        public User(int id, String name) {            this.id = id;            this.name = name;        }        public int id;        public String name;        @Override        public String toString() {            return "[id=" + id + ", name=" + name + "] ";        }    }    public static void main(String[] args) {        //构造了弱引用        WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));        //从弱引用中重新获取对象        System.out.println(userWeakRef.get());        System.gc();        // 不管当前内存空间足够与否,都会回收它的内存        System.out.println("After GC:");        //重新尝试从弱引用中获取对象        System.out.println(userWeakRef.get());    }}

执行垃圾回收后,软引用对象必定被清除

[id=1, name=songhk] After GC:nullProcess finished with exit code 0

虚引用

虚引用(Phantom Reference):对象回收跟踪

  1. 也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个
  2. 一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
  3. 它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null 。即通过虚引用无法获取到我们的数据
  4. 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
  5. 虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
  6. 由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

在JDK1.2版之后提供了PhantomReference类来实现虚引用。

// 声明强引用Object obj = new Object();// 声明引用队列ReferenceQueue phantomQueue = new ReferenceQueue();// 声明虚引用(还需要传入引用队列)PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);obj = null; 

虚引用代码示例

public class PhantomReferenceTest {    public static PhantomReferenceTest obj;//当前类对象的声明    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用队列    public static class CheckRefQueue extends Thread {        @Override        public void run() {            while (true) {                if (phantomQueue != null) {                    PhantomReference<PhantomReferenceTest> objt = null;                    try {                        objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    if (objt != null) {                        System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");                    }                }            }        }    }    @Override    protected void finalize() throws Throwable { //finalize()方法只能被调用一次!        super.finalize();        System.out.println("调用当前类的finalize()方法");        obj = this;    }    public static void main(String[] args) {        Thread t = new CheckRefQueue();        t.setDaemon(true);//设置为守护线程:当程序中没有非守护线程时,守护线程也就执行结束。        t.start();        phantomQueue = new ReferenceQueue<PhantomReferenceTest>();        obj = new PhantomReferenceTest();        //构造了 PhantomReferenceTest 对象的虚引用,并指定了引用队列        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);        try {            //不可获取虚引用中的对象            System.out.println(phantomRef.get());            System.out.println("第 1 次 gc");            //将强引用去除            obj = null;            //第一次进行GC,由于对象可复活,GC无法回收该对象            System.gc();            Thread.sleep(1000);            if (obj == null) {                System.out.println("obj 是 null");            } else {                System.out.println("obj 可用");            }            System.out.println("第 2 次 gc");            obj = null;            System.gc(); //一旦将obj对象回收,就会将此虚引用存放到引用队列中。            Thread.sleep(1000);            if (obj == null) {                System.out.println("obj 是 null");            } else {                System.out.println("obj 可用");            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

第一次尝试获取虚引用的值,发现无法获取的,这是因为虚引用是无法直接获取对象的值,然后进行第一次GC,因为会调用finalize方法,将对象复活了,所以对象没有被回收

2、但是调用第二次GC操作的时候,因为finalize方法只能执行一次,所以就触发了GC操作,将对象回收了,同时将会触发第二个操作就是将待回收的对象存入到引用队列中。

输出结果:

null1 次 gc
调用当前类的finalize()方法
obj 可用
第 2 次 gc
追踪垃圾回收过程:PhantomReferenceTest实例被GC了
obj 是 null

Process finished with exit code 0
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-07-30 12:36:58  更:2021-07-30 12:37:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/29 2:15:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码