【学习笔记】深入理解JVM之垃圾回收机制

news2024/11/16 5:33:06

【学习笔记】深入理解JVM之垃圾回收机制

更多文章首发地址:地址
请添加图片描述

参考:

  • 《深入理解JAVA虚拟机》第三版 第三章
  • 尚硅谷 第134 - 203 集
  • 参考文章:https://blog.csdn.net/qq_48435252/article/details/123697193

1、概念

🌻 首先我们要知道在程序中 垃圾 是怎么样的一个定义?

在现实中,我们知道 垃圾 是我们人类丢弃的废弃物品,而在程序中也是相对如此 垃圾 是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空 间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。

🌻 为什么需要垃圾回收机制呢?

面对这个问题,我们也可以想一想我们人类自己的生存问题,如果我们一直不解决 垃圾 的存储问题,则可能会导致我们生存的地方被 垃圾 所侵占,从而导致没有生存空间。

  • 对于高级语言来说,一个基本认知是,如果不进行 垃圾回收 ,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活 垃圾 而从来不打扫一样。
  • 除了释放没用的对象,垃圾回收 也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。

2、常见的垃圾回收算法

概念补充:对象已死

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就 是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对 象)了。

  • 判断对象存活一般有两种方式:引用计数算法可达性分析算法

我们比较常见的 垃圾回收算法 有以下几种:

  • 引用计数法
  • 可达性分析算法
  • 标记清除算法
  • 复制算法
  • 标记压缩算法

2.1 引用计数法

概念: 引用计数算法(Reference Counting) 比较简单,对每个对象保存一个整型 的引用计数器属性。用于记录对象被引用的情况。

对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象 A 的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

优点:

  • 实现简单方便,回收效率高

缺点:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hXfw8IC6-1674746881708)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20230126131919308.png)]

2.2 可达性分析算法

这个算法的基本思路 就是通过 一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为 “引用链”(Reference Chain) ,如果某个对象到 GC Roots 间没有任何引用链相连, 或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0S4nv0dN-1674746881709)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20230126133618404.png)]

Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种:

  • 栈帧中的本地变量表中引用的对象。

  • 静态变量。

  • 字符串常量池里的引用。

  • 在本地方法栈中 JNI(即通常所说的Native方法) 引用的对象。

  • Java 虚拟机内部的引用,如基本数据类型对应的 Class对象 ,一些常驻的异常对象(比如

    NullPointExcepiton、OutOfMemoryError )等,还有系统类加载器。

  • 所有被同步锁(synchronized关键字)持有的对象.

2.3 对象的finalization机制

  • Java语言提供了对象终止 (finalization) 机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
  • 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的 finalize() 方法。
  • finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
  • 永远不要主动调用某个对象的 finalize () 方法,应该交给垃圾回收机制调用。理由包括下面三点:
    • ➢在 finalize() 时可能会导致对象复活。
    • finalize() 方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则 finalize() 方法将没有执行机会。
    • ➢一个糟糕的 finalize () 会严重影响GC的性能。
  • 从功能上来说,finalize()方法与C++ 中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法在本质,上不同于C++ 中的析构函数。

2.4 判断对象的死亡

即使在可达性分析算法中判定为不可达的对象,也不是 “非死不可” 的,这时候它们暂时还处于 “缓刑” 阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没 有与 GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是 否有必要执行 finalize() 方法。假如对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为 “没有必要执行”

如果对象重写了 finalize() 方法,且还没有执行该方法,则会把该对象插入到 F一Queue 队列中,由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其 finalize() 方法执行。

2.5 标记清除算法

概念:

如它的名字一样,算法分为 “标记”“清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Itl4yXbL-1674746881709)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20230126164851113.png)]

优点:

  • 比较简单且经常使用。

缺点:

  • 第一个是执行效率不稳定,如果 Java 堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  • 第二个是内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.6 复制算法

为了解决 标记-清除算法 面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为 “半区复制”(Semispace Copying) 的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

核心思想

将活着的内存空间分为两块,每次使用一块,进行垃圾回收的时候,将存活对象复制到另一块未使用的区域,然后将源区域清空,然后交换两个内存的角色

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GMUxy5lg-1674746881710)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20230126165545577.png)]

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

在新生代,对常规应用的垃圾回收,一次通常可以回收70%一 99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。

2.7 标记压缩算法

背景:

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

核心思想:

  • 第一阶段和标记一清除算法一样,从根节点开始标记所有被引用对象.
  • 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
  • 之后,清理边界外所有的空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4sTbzzbE-1674746881710)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20230126170036703.png)]

  • 标记一压缩算法 的最终效果等同于标记一清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记一清除一压缩(Mark一 Sweep一Compact)算法。

  • 二者的本质差异在于 标记清除算法 是一种 非移动式 的回收算法,标记压缩 是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。

清除算法的对比:

属性\算法标记清除算法复制算法标记压缩算法
时间复杂度
空间复杂度占用2倍
内存碎片
移动对象

2.8 分代收集算法

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

分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

一般是把 Java 堆分为 新生代老年代 ,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

目前几乎所有的GC都是采用分代收集(Generational Collecting) 算法执行垃圾回收的。

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

  • 年轻代(Young Gen)
    • 年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
    • 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过 hotspot 中的两个 survivor 的设计得到缓解。
  • 老年代(Tenured Gen)
    • 老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
    • 这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记清除或者是标记整理的混合实现。
      • ➢标记阶段的开销与存活对象的数量成正比。
      • ➢清除阶段的开销与所管理区域的大小成正相关。
      • ➢压缩阶段的开销与存活对象的数据成正比。

3、内存溢出与内存泄露

3.1 内存溢出

内存溢出

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

首先说没有空闲内存的情况:说明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

这里面隐含着一层意思是,在抛出 OutOfMemoryError 之 前,通常垃圾收集器会被触发,尽其所能去清理出空间。

  • ➢例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。
  • ➢在 java.nio.BIts.reserveMemory() 方法中,我们能清楚的看到,System.gc() 会被调用,以清理空间。
    当然,也不是在任何情况下垃圾收集器都会被触发的。
  • 比如,我们去分配一一个超大对象,类似一个超大数组超过堆的最大值, JVM 可以判断出垃圾收集并不能解决这个问题,所以直接拋出 OutOfMemoryError

3.2 内存泄漏(Memory Leak)

  • 也称作 “存储渗漏” 。严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫 内存泄漏
  • 但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致内存溢出 OOM ,也可以叫做宽泛意义上的 内存泄漏 .
  • 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现0utOfMemory 异常,导致程序崩溃。

注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。

举例

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

4、垃圾回收器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2InjTRGb-1674746881710)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20230126200435618.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QmR3vxFn-1674746881710)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20230126200509562.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J2i6T7P2-1674746881710)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20230126200523570.png)]

  • 1.优先调整堆的大小让JVM自适应完成。
  • 2.如果内存小于100M,使用串行收集器
  • 3.如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  • 4.如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
  • 5.如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
  • 官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/391049.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mock.js介绍及使用总结

1 什么是Mock.js Mock.js用于生成随机的模拟数据,拦截 Ajax 请求,返回伪造的数据。因此在前端开发阶段就可以通过这个工具进行沉浸式开发,实现数据自产自销,降低和后端的沟通成本,实现真正意义上的前后端开发解耦合。…

2023年轻资产创业怎么做?哪些项目容易创业?

2023年,越来越多的创业者开始考虑轻资产创业,但是,多数人在选择项目的时候,没有一个标准,不知道怎么选择轻资产创业项目,那么,在这里给大家讲讲,轻资产创业怎么做,哪些轻…

反思当下所处的环境,有没有让你停滞不前、随波逐流

环境对人的影响真的很大,小时候的环境、长大后的环境、工作环境、生活环境、好的环境、差的环境......我们都生活在一定的环境中所以既是环境的产物,又是环境的创造者与改造者。我们与环境的关系是相辅相成的我们的生活和工作当中接触到的人或事或物&…

emac接口与phy交互

nuc970的emac接口 nuc970的EMAC以太网接口与PHY芯片之间的数据交换是通过MII(Media Independent Interface)或RMII(Reduced Media Independent Interface)接口实现的。 在MII接口中,EMAC和PHY之间通过4对数据线&…

C语言:当scanf语句中有转义字符时,你该这样输入

最近在重新学习一下C语言的有关知识,突然发现现在来看过去所学的知识,自己的感受又有很大的不同,就拿输入语句scanf来说吧!看到CSDN问答上有一个关于scanf语句的问题,当时题主代码很简单,可是就是没有输出语…

idea:地址被占用

问题启动idea报:java.net.BindException: Address already in use: bind,具体截图如下:解决步骤1、首先想到的是改idea端口,但按网上方法试下了几个4位数和5位数的端口,没啥作用2、根据idea抛异常的弹出框提示&#xf…

在函数中,用指针接收就可以改变相应的内容吗??

作者:小树苗渴望变成参天大树 作者宣言:认真写好每一篇博客 作者gitee:gitee 如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧! 我们在不管指针那篇博客,还是在函数那篇博客中,我都给大家讲解过…

Unity 打包代码到 DLL

Unity 打包代码到 DLL 使用Unity API PlayerBuildInterface.CompilePlayerScripts 将项目中的代码生成为 DLL 程序集 在 Editor 文件夹下新建脚本 CompileDll using UnityEngine; using UnityEditor; using UnityEditor.Build.Player; using System.IO;public class Compile…

【springmvc】获取请求参数

SpringMVC获取请求参数 1、通过ServletAPI获取 将HttpServletRequest作为控制器方法的形参,此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的对象 RequestMapping("/testParam") public String testParam(HttpServletRequest request)…

Allegro如何输出ODB文件操作指导

Allegro如何输出ODB文件操作指导 在PCB设计完成之后,需要输出生产文件用于生产加工,除了gerber文件可以用生产制造,ODB文件同样也可以用于生产,如下图 用Allegro如何输出ODB文件,具体操作如下 首先确保电脑上已经安装了ODB这个插件,版本不受限制点击File

开学季准备哪些电容笔好?apple pencil一代平替笔推荐

一支简单而又易用的电容笔,配上我们的ipad,将会为我们的工作、学习带来更多的乐趣。在画画时,也不能忽略电容笔的重要作用。关于电容笔的相关知识,本人也略有知不少,以下是我今天要带来的几款具有极高性价比的电容笔&a…

【C#进阶】C# 特性

序号系列文章10【C#基础】C# 正则表达式11【C#基础】C# 预处理器指令12【C#基础】C# 文件与IO文章目录前言1,特性的概念1.1 特性的属性1.2 特性的用途2,特性的定义2.1 特性参数2.2 特性目标3,预定义特性3.1 AttributeUsage3.2 Conditional3.2…

【springmvc】java bean 的区分

bean JavaBean分为两类: 一类称为实体类Bean:专门存储业务数据的,如 Student、User 等一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问。 在一个java的springboot中,一般…

day59-day60【代码随想录】二刷数组

文章目录前言一、移动零(力扣283)【双指针】二、轮转数组(力扣189)三、寻找数组的中心下标(力扣728)四、和为 K 的子数组(力扣560)五、按奇偶排序数组 II(力扣922&#x…

【SPSS】多因素方差分析详细操作教程(附案例实战)

🤵‍♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞&#x1f4…

【微信小程序-原生开发】实用教程14 - 列表的分页加载,触底加载更多(含无更多数据的提醒和显示,自定义组件)

此页可在动态列表的基础上完善,也可以单独学习 【微信小程序-原生开发】实用教程10 - 动态列表的新增、修改、删除 https://blog.csdn.net/weixin_41192489/article/details/128835069 效果预览 核心技术 列表的分页加载 skip 跳跃到指定下标开始查询limit 限制返…

Vulnhub系列:SickOS1.1

靶机地址:SickOS1.1渗透思路:信息收集对于目标靶机进行扫描,可以利用nmap或arp-scan -l进行查询,查询到靶机后,探测其开放的端口,常见的端口21、22、80、3306、445等等,对于不同的端口进行不同的…

博途PLC开放式以太网通信之TRCV_C指令编程应用

博途PLC开放式以太网通信TSENG_C指令应用,请参看下面的文章链接: 博途PLC 1200/1500PLC开放式以太网通信TSEND_C通信(UDP)_plc的udp通信_RXXW_Dor的博客-CSDN博客开放式TSEND_C通信支持TCP 、UDP等,关于TSEND_C的TCP通信可以参看下面这篇文章:博途PLC 1200/1500PLC开放式…

存储器分类

存储器(Memory)包括: RAM(Random Access Memory)(计算机运行内存、CPU的L1/L2 Cache等)、 ROM(Read Only Memory)(用于BIOS等固化程序/数据的存储)和 Flash(可用于机械硬盘等)。 存储器&#x…

现代神经网络(VGG),并用VGG16进行实战CIFAR10分类

专栏:神经网络复现目录 本章介绍的是现代神经网络的结构和复现,包括深度卷积神经网络(AlexNet),VGG,NiN,GoogleNet,残差网络(ResNet),稠密连接网络…