JVM-垃圾回收

深入理解JVM虚拟机读书笔记——垃圾回收算法
深入理解JVM虚拟机读书笔记——垃圾回收器

什么是垃圾

垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?如何回收?

垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
外文:An object is considered garbage when it can no longer be reached from any pointer in the runningprogram.

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。

面试题

蚂蚁金服:
你知道哪几种垃圾回收器,各自的优缺点,重点讲一下cms和g1一面:JVMGcC算法有哪些,目前的JDK版本采用什么回收算法一面:G1回收器讲下回收过程
Gc是什么?为什么要有Gc?
一面: GC的两种判定方法?CMS收集器与G1收集器的特点。百度:
说一下Gc算法,分代回收说下垃圾收集策略和算法
天猫:
一面:jvm Gc原理,JVM怎么回收内存
一面:CMS特点,垃圾回收算法有哪些?各自的优缺点,他们共同的缺点是什么?滴滴:
一面: java的垃圾回收器都有哪些,说下g1的应用场景,平时你是如何搭配使用垃圾回收器的

京东:
你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。垃圾回收算法的实现原理。
阿里:
讲一讲垃圾回收算法。什么情况下触发垃圾回收?如何选择合适的垃圾收集算法?JVM有哪三种垃圾回收器?字节跳动:
常见的垃圾回收器算法有哪些,各有什么优劣?system.gc ()和runtime.gc()会做什么事情?一面: Java Gc机制? GC Roots有哪些?
二面:Java对象的回收方式,回收算法。
CMS和c1了解么,CMS解决什么问题,说一下回收的过程。CMS回收停顿了几次,为什么要停顿两次。

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

对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完。因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。

除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。

随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有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等语言都使用了自动垃圾回收的思想,也是未来发展趋势。可以说,这种自动化的内存分配和垃圾回收的方式己经成为现代开发语言必备的标准。

Java自动内存管理介绍

自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
没有垃圾回收器,java也会和cpp一样,各种悬垂指针,野指针,泄露问题让你头疼不已。
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
oracle官网关于垃圾回收的介绍https : / / docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html

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

GC主要关注方法区和堆

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

垃圾回收相关算法概述

GC(Garbage Collector或者Garbage Collection)

标记阶段:识别哪些对象不再使用了 引用计数算法 可达性分析算法(根搜索算法)
清除阶段: 标记清除算法 复制算法 标记压缩算法

分代收集算法是针对具体落地的垃圾回收器

引用计数算法

在这里插入图片描述

引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:

它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

举例,如果我把p指针废弃了,本来这3个对象都应该被回收,但是他们的引用都是1,形成了循环引用,导致无法回收
在这里插入图片描述

内存泄漏:对象本身已经没有价值,但是又无法回收

举一个内存泄漏的例子

可达性分析算法与GC Roos

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

就比如买葡萄,你直接能提起来的哪些就是存活对象,有些葡萄掉了,提不起来,那些就是垃圾,你无法访问的
就比如一人得道,鸡犬升天

哪些结构可以作为GC roots?

在Java语言中,GC Roots包括以下几类元素:
GC Roots是一组必须活跃的引用,不是对象
虚拟机栈中引用的对象比如:各个线程被调用的方法中使用到的参数、局部变量等。
本地方法栈内JNI(通常说的本地方法)引用的对象
方法区中类静态属性引用的对象
比如: Java类的引用类型静态变量。方法区中常量引用的对象
比如:字符串常量池(string Table)里的引用
所有被同步锁synchronized持有的对象
Java虚拟机内部的引用
比如基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器
反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

在这里插入图片描述

在这里插入图片描述

对象的finalization机制

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

使用MAT查看GCRoots

MAT:Memory Analyzer Tool
在这里插入图片描述

获取dump文件
方式1:命令行使用jmap

首先使用jps查看当前程序中的进程
然后通过jmap生成bin文件
在这里插入图片描述

方式二:使用JVisualVM导出

捕获的heap dump文件是一个临时文件,关闭JVisualVM后自动删除,若要保留,需要将其另存为文件
可通过以下方法捕获heap dump:
在左侧“Application"(应用程序)子窗口中右击相应的应用程序,选择Heap Dump(堆Dump) 。
在Monitor(监视)子标签页中点击Heap Dump(堆Dump)按钮。

本地应用程序的Heap dumps作为应用程序标签页的一个子标签页打开。同时heap dump在左侧的Application(应用程序)栏中对应一个含有时间戳的节点右击这个节点选择save as(另存为)即可将heap dump保存到本地。

举例用MAT查看GCRoots

首先下载MAT

/**
 * Created by 此生辽阔 on 2021/8/8 11:45
 */
public class GCRoots {
    public static void main(String[] args) {
        //main方法对应的线程叫主线程,main线程的栈帧类里面有局部变量表,局部变量表里面就是当前线程的局部变量,比如args numList,birth
        //局部变量表天然的作为GCRoot,都是引用嘛,指向堆空间中的对象
        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("birth,numList已置空,请操作");
        new Scanner(System.in).next();
        System.out.println("结束");
    }
}

我们在上面程序的

 numList=null;
 birth=null;

之前和之后分别生成一个dump文件,numList和birth置空之后,堆空间中的实体就没有意义了,可以作为垃圾被回收

首先启动主程序.
然后启动JVisualVM

在这里插入图片描述

点击“堆 dump”生成dump文件

在这里插入图片描述

在这里插入图片描述

然后继续执行主程序,把birth和numList置空
在这里插入图片描述

重新生成一个dump文件

在这里插入图片描述

然后使用MAT打开dump文件

在这里插入图片描述
在这里插入图片描述

选择刚刚生成的dump文件

在这里插入图片描述

然后选择GCRoots

在这里插入图片描述
可以看到GCRoots有1702个
其中包括 System.class(系统类),比如系统类加载器,引导类加载器
JNI Global :本地方法中加载的相关结构
Thread:当前线程
Busy Monitor:锁对象

现在我们回顾一下之前说的GCRoots,虽然有点不一样,实际上是从不同的维度描述的

在这里插入图片描述

eclipse官网的GCRoots描述
在这里插入图片描述

现在我们来分析主线程中的GCRoots
在这里插入图片描述

然后我们分析第二个dump文件

在这里插入图片描述

可以看到birth和numList已经没有了

使用Jprofiler进行GCRoots溯源

当一个对象我们已经不想使用了,但是当我们使用可达性分析算法的时候发现这个对象直接或间接的还被GCRoot关联,就会出现内存泄漏

所以,我们一般还需要查看某一个对象的GCRoot

下面这条指令在OOM的时候会生成一个dump文件
在这里插入图片描述

标记清除算法原理及优缺点

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾收集算法是标记―清除算法( Mark-sweep )、复制算法( copying )、标记–压缩算法(Markcompact ) 。

背景:
标记–清除算法(Mark-Sweep )是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。
执行过程:
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
·标记:collector从引用根节点开始遍历,标记所有被引用的对象(不是标记垃圾)。一般是在对象的Header中记录为可达对象。
·清除: collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收

在这里插入图片描述

缺点
不算高(标记阶段递归遍历所有可达对象,清除阶段需要遍历堆空间中的所有对象,清除没有被标记为可达的对象)
在进行Gc的时候,需要停止整个应用程序,导致用户体验差
这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表(指向堆空间中的空闲内存),空闲列表也要占用内存空间,如果此时需要创建一个大对象,但是空闲列表里面的空间都不够,如果是新生代就可以考虑放在老年代,老年代还不够,就要执行GC,GC完还不够的话,就会OOM异常
在这里插入图片描述

注意︰何为清除?

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

比如硬盘被格式化了,只要你没有在往硬盘里面写数据,那么是可以恢复数据的,一旦你写过数据了,就会把原来的数据给抹掉,包括一些地址指针等等

复制算法的原理及优缺点

核心思想:
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

就是我们找到对象之后不用进行标记,而是直接把它复制到另一块内存,不是复制地址,而是复制整个对象实体
在这里插入图片描述

AB区交换使用,比如新生代的垃圾回收算法就是复制算法

优点:
·没有标记和清除过程,实现简单,运行高效I
·复制过去以后保证空间的连续性,不会出现“碎片”问题
缺点:
·此算法的缺点也是很明显的,就是需要两倍的内存空间。
·对于G1这种分拆成为大量region的Gc,复制而不是移动,意味着Gc需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
特别的:如果系统中的存活对象很多(效率较低),复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。

在这里插入图片描述

老年代中的对象大都是持久存活的,所以不用复制算法

标记压缩算法的原理和优缺点

也叫标记整理算法(Mark-Compact),可以理解为内存碎片的整理

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法

另一方面,老年代的空间比较大,使用赋值算法直接砍一半空间不太合适,太浪费空间了

标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM 的设计者需要在此基础之上进行改进。标记–压缩(Mark - Compact)算法由此诞生

1970 年前后,G.L. steele .c. J. Chene和p.s. wise 等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

使用标记整理算法后,下一次给对象分配空间就可以使用指针碰撞的方式(找到已用内存和未用内存之间的分隔指针,而不需要一个空闲列表去记录所有的空闲空间)

在这里插入图片描述

优点:
·消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
·消除了复制算法当中,内存减半的高额代价。缺点:
·从效率上来说,标记-整理算法要低于复制算法(也比标记清除算法效率更低,因为还涉及到碎片整理)。
·移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。·移动过程中,需要全程暂停用户应用程序。即:STW(因为涉及到赋值,所以STW的时间会更长)

垃圾收集小结

在这里插入图片描述

分代收集算法

有没有最好的垃圾收集算法
答案是没有,就比如排序算法,如果有一种最好的,那为什么还要有那么多中排序算法呢,不同的排序算法有不同的应用场景

分代收集算法不是一种垃圾回收算法,而是说根据应用场景的不同选择不同的算法

前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。

分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如: string对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

在这里插入图片描述
老年代:新生代=3:1
新生代Eden:from:to=8:1:1,所以浪费的空间不是很高,只有1/10 * 1/3=1/30

以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的serial old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用serial old执行Full Gc以达到对老年代内存的整理。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。

增量搜集算法 分区算法

在这里插入图片描述

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

分区算法

一般来说,在相同条件下,堆空间越大,一次Gc时所需要的时间就越长,有关Gc产生的停顿也越长。为了更好地控制Gc产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次Gc所产生的停顿。

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。

每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

分区算法和增量收集算法都是为了降低Gc的停顿时间

对于G1垃圾回收器,把堆空间分成了很多小的region,有的region存放eden区的数据,有的region存放s区的数据,有的存放old区的数据,白色是空白的区域
在这里插入图片描述

System.gc()的理解

在这里插入图片描述

上图的免责声明是指gc不一定马上执行

内存溢出的分析

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

内存泄漏

在这里插入图片描述

内存泄漏有可能导致内存溢出
内存泄漏数据比较多的时候,超出内存的承受范围就会出现OOM

在这里插入图片描述

内存泄漏举例
在这里插入图片描述

Stop the world

在这里插入图片描述
就跟警察保护现场一样,在办案的时候,现场就不能再改变了,应该保持案发前的状态

比如我们之前测试的,scanner输入暂停和输入之后的GCRoots数量是不一样的,程序执行的过程中,GCRoots是在不断变化的,要想进行垃圾回收,需要确定哪些是GCroots,然后通过可达性分析算法,找到和GCRoots关联的对象,要想进行垃圾回收,就要保证我们的数据在这一刻就不再动了,所以需要让用户线程停下来

在这里插入图片描述

并行与并发

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

并行

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

二者对比:
并发工指的是多个事情,在同一时间段内同时发生了。并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的。
只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

垃圾回收的并行与并发

在这里插入图片描述

在这里插入图片描述

安全点安全区域

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

在这里插入图片描述

安全区域

在这里插入图片描述

在这里插入图片描述

几种不同引用的概述

我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象。

【既偏门又非常高频的面试题】强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱

除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。如下图,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。

在这里插入图片描述

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

下面的情况都是根据应用关系还在来描述的,应用关系不在就算是强引用,也直接回收了
强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“object obj=new object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。(不可达的我们就回收,可达的,我们就不回收,即使OOM也不会回收)

软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。(内存不足即回收,内存够就不回收)

弱引用(weakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。(发现即回收)

虚引用(PhantomReference) :一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。(用来做对象回收跟踪)

强引用:不回收

在Java程序中,最常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。

当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。

强引用的对象是(GCRoots)可触及的,垃圾收集器就永远不会回收掉被引用的对象。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。

相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成Java内存泄漏的主要原因之一。

如果根可达的强引用都能被回收的话,那就不会OOM了呀

软引用-内存不足即回收

软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue) 。

类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。

弱引用-发现即回收

弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象

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

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况

软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用

在这里插入图片描述

虚引用-对象回收跟踪

也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个

一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。

它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知

垃圾回收器:章节概述

在这里插入图片描述
在这里插入图片描述

垃圾回收器的分类

垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。
由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的Gc版本。
从不同角度分析垃圾收集器,可以将GC分为不同的类型。

JDK分为oracle jdk和openjdk,其中open jdk是对外开源的,其他人可以自己修改的

java 9之后,每隔半年就会发布一个新版本

java不同版本的新特性:可以从三个角度学习,
1,语法层面(Lambda表达式,switch表达式,自动装箱,自动拆箱,enum)
2,api层面,(java8中的Stream,日期时间,Optional,String,集合框架)
3 底层优化:JVM优化,GC的变化

按线程数分

按线程数分,可以分为串行垃圾回收器和并行垃圾回收器

这个线程是指垃圾回收的线程数,下图橙色的线程代表垃圾回收的线程,如果只有一个垃圾回收线程,就叫串行垃圾回收器,有多个垃圾回收器并行执行,则称为并行垃圾回收器

只有一个cpu的时候我们就用串行垃圾回收器
硬件配置比较低(单cpu)的时候,我们适合用串行垃圾回收器,单cou使用并行垃圾回收器只能是并发,不会出现真正意义上的并行,只有一个cpu,只能大家交替执行,反而更慢,因为还需要切换的时间,并发能力比较强的cpu的话,用并行的就会更好

在这里插入图片描述

串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作此时工作线程被暂停,直至垃圾收集工作结束。

在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的client模武下的JVM中

在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器。

和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“stop-the-world”机制。

独占式:只进行垃圾回收,别的用户线程都不允许执行

按工作模式分

按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。

并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间(减少延迟,提高响应速度

独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
在这里插入图片描述

按碎片处理方式分

按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器

压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片(再分配内存空间使用指针碰撞)。

非压缩式的垃圾回收器不进行这步操作(再给对象分配内存空间使用空闲列表)。

分类标准就是垃圾回收完之后,是否要对内存碎片进行整理

按工作的内存区间分

可分为年轻代垃圾回收器和老年代垃圾回收器

评估垃圾回收器的性能指标

重点关注红色的部分
吞吐量:运行用户代码的时间占总运行时间的比例
(总运行时间:程序的运行时间+内存回收的时间)

垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

收集频率:相对于应用程序的执行,收集操作发生的频率。

内存占用:Java堆区所占的内存大小(堆越大,垃圾回收频率会低一点,吞吐量就上来了)。

快速:一个对象从诞生到被回收所经历的时间。

在这里插入图片描述

内存扩大对延迟带来负面影响是因为GC面对的垃圾比较多,一次垃圾回收花费的时间更长,也就是说内存空间大反而造成暂停时间更长。

此消彼长

Parallel关注 吞吐量
CMS G1 ZGC关注暂停时间,希望低延迟

在这里插入图片描述

在这里插入图片描述

就是说注重吞吐量的话,只需要保证总的主程序运行时间的比例更高,不用关注一次暂停的时间有多 长
注重暂停时间的话,保证使得每一次暂停的时间较短,但是暂停时间短,回收的频率就较高了,线程切换次数多,消耗更大

在这里插入图片描述

在这里插入图片描述
在设计(或使用)Gc算法时,我们必须确定我们的目标:一个cc算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折衷。

现在标准:在最大吞吐量优先的情况下,降低停顿时间。(比如每次垃圾回收时间控制在10ms以内),在可控的暂停时间之内,尽量大地提高吞吐量

垃圾回收器的发展迭代史

常见的垃圾回收器

JDK的不同产生可以提供不同版本的JVM,不同的JVM就可以使用不同的垃圾回收器,

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

垃圾回收器并不是每一个都能回收整个堆空间,每个回收器可以回收的区域不一样

在这里插入图片描述

垃圾回收器的组合关系:一个新生代垃圾回收器对应一个老年代垃圾回收器

在这里插入图片描述
在这里插入图片描述

在jdk8中,默认的垃圾回收器是Parallel Scavenge GC和Parallel Old GC,JDK9用的是G1.

查看默认的垃圾回收器

在这里插入图片描述查看jdk8
方法一:在IDEA中设置参数,然后打印
在这里插入图片描述

方法2:在控制台打印

在这里插入图片描述
在这里插入图片描述

查看jdk9
在这里插入图片描述

在这里插入图片描述

Serial回收器:串行回收

在这里插入图片描述

当新生代指定Serial垃圾回收器,老年代可以使用Serial Old垃圾回收器

在这里插入图片描述

在这里插入图片描述

单核情况下Serial垃圾收集器的性能才能达到最优
因为Serial在运行的时候导致用户线程完全停止(用户体验差),所以,对于交互性较强的应用而言,Serial这种垃圾回收器是不能接受的
在这里插入图片描述

ParNew垃圾回收器 并行回收

在这里插入图片描述

因为server端的硬件可能配置更高一些,适合在多线程的场景下使用ParNew,性能会更好一些

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Parallel Scavenge 回收器 吞吐量优先

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Parallel Scavenge垃圾回收器参数设置

在这里插入图片描述

在这里插入图片描述

年轻代的大小等参数可以自动条件是为了满足吞吐量优先
在这里插入图片描述

CMS垃圾回收器概述与工作原理-低延迟

这里的并发是指来及回收线程和用户线程可以同时执行

CMS(concurrent mark-sweep)见名知意,使用的是标记清除算法
在这里插入图片描述

不幸的是,CMS作为老年代的收集器,却无法与 JDK 1.4.0中已经存在的新生代收集器Parallel scavenge 配合工作(底层框架不一样),所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个
在G1出现之前(G1兼具并行和并发),CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。

在这里插入图片描述

CMS工作原理

在这里插入图片描述

首先用户线程执行,当内存不足的时候,进行垃圾回收的时候首先会初始标记,初始标记会stop the world (暂停时间非常短)

并发标记线程和用户线程并行执行

重新标记也会stop the world

并发清理与用户线程并行执行

重置线程与用户线程并行执行

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段

初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象(相当于广度优先只搜了一层)。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快

并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短

并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

CMS特点及弊端分析

在这里插入图片描述

阈值:(cpu数+3)/4

在这里插入图片描述

标记压缩才能使用指针碰撞,因为内存是规整的,用一个指针指向内存空闲的区间

既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?

在并发清理阶段,在垃圾清除的过程中,用户线程也在执行,如果使用标记压缩算法,需要对内存进行重新整合,可以这时用户线程还在执行,在执行的过程中你把用户线程中对象的地址给修改了

在这里插入图片描述

CMS的优点:

并发收集
低延迟.

CMS的弊端:

1)会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
2 ) CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
3)CMS收集器无法处理浮动垃圾。可能出现“ Concurrent Mode Failure"失败而导致另一次 Full GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行Gc时释放这些之前未被回收的内存空间。

对吞吐量降低的理解:因为并发了,分出一部分线程用于垃圾回收,如下图,是设置CMS的线程数量,可以理解为(cpu数+3)/4,如果有4个cpu会有一个用于垃圾回收

在这里插入图片描述

CMS垃圾回收器的参数设置

在这里插入图片描述

在这里插入图片描述

CMS小结,垃圾回收器的选用

HotSpot有这么多的垃圾回收器,那么如果有人问,serial GC、Parallel cc、Concurrent Mark Sweep Gc这三个Gc有什么不同呢?请记住以下口令:
如果你想要最小化地使用内存和并行开销,请选serial Gc+serial old;

如果你想要最大化应用程序的吞吐量,请选Parallel GC+parallel old;

如果你想要最小化Gc的中断或停顿时间,请选CMS Gc+ParNew Gc.

在这里插入图片描述

为什么要删除CMS?从CMS的弊端出发

在这里插入图片描述

G1回收器 区域化分代式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值