• JVM类加载和垃圾回收


    目录

    1.JVM 类加载过程

    2.双亲委派模型

    2.1 概念

    2.2 双亲委派类加载机制的类加载流程

    2.3 双亲委派模型的优点

    2.4 双亲委派模型的缺点

    3.垃圾回收

    3.1 死亡对象的判断算法

    3.1.1 引用计数算法

    3.1.2 可达性分析算法

    3.2 垃圾回收算法

    3.2.1 标记-清除算法

    3.2.2 复制算法

    3.2.3 标记-整理算法

    3.2.4 分代算法

    哪些对象会进入新生代?哪些对象会进入老年代?

    4.垃圾收集器

    4.1 概念

    4.2 各种收集器

    4.2.1 Serial收集器(新生代收集器,串行GC)

    4.2.2 ParNew收集器(新生代收集器,并行GC)

    4.2.3 Parallel Scavenge收集器(新生代收集器,并行GC)

    4.2.4 Serial Old收集器(老年代收集器,串行GC)

    4.2.5 Parallel Old收集器(老年代收集器,并行GC)

    4.2.6 CMS收集器(老年代收集器,并发GC)

    4.2.6 G1收集器(唯一一款全区域的垃圾回收器)



    1.JVM 类加载过程

    1. 加载:加载class字节码到java进程的内存中,在堆中创建类对象。
    2. 验证:验证java字节码是否符合规范(是否符合JVM规范)。
    3. 准备:准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
    4. 解析:将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
    5. 初始化:(类的初始化)Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。

    2.双亲委派模型

    2.1 概念

    JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到了类加载的请求后不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,其父类在接收到该类加载请求后优惠委派给自己的父类,以此类推,这样所有的加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的 Class 文件在父类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被加载成功,若找不到该类,则JVM会抛出ClassNotFound异常。

    2.2 双亲委派类加载机制的类加载流程

    双亲委派类加载机制的类加载流程如下:

    1. 将自定义加载器挂载到应用程序类加载器。
    2. 应用程序类加载器将类加载请求委托给扩展类加载器。
    3. 扩展类加载器将类加载请求委托给启动类加载器。
    4. 启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载。
    5. 扩展类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载。
    6. 应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义加载器加载。
    7. 在自定义加载器下查找并加载用户指定目录下的Class文件,如果在自定义加载路径下未找到目标Class文件,则抛出ClassNotFoud异常。

    2.3 双亲委派模型的优点

    唯一性和安全性(确保优先采取 启动/扩展/应用 类加载器来加载类)

    2.4 双亲委派模型的缺点

    扩展性/灵活性就没那么好(遵循双亲委派机制的类加载,某些场景下可能没法事先知道需要加载的类名(比如:jdbc中的代码,jdk是无法知道数据库驱动类的类名))

    3.垃圾回收

    GC:堆,方法区;其他没有

    Java语言,是不用程序员自己分配内存,也不用自己回收内存。 => 原因:jvm中,实现了垃圾回收机制(自动回收)。

    3.1 死亡对象的判断算法

    3.1.1 引用计数算法

    引用计数描述的算法为:

    给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。

    引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计数法进行内存管理。

    但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题。

    3.1.2 可达性分析算法

    为了解决引用计数法的循环引用问题,Java还采用了可达性分析来判断对象是否可以被回收。具体做法是首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果在GC Roots和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然是不可达的,则将被垃圾收集器回收。

    GC Roots:垃圾回收要检查的根节点

    引用链:某个对象到达GC Roots的路径(对象与对象之间的连接关系)

    3.2 垃圾回收算法

    3.2.1 标记-清除算法

    "标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

     缺陷:

    1. 效率 : 标记和清除这两个过程的效率都不高
    2. 内存碎片: 标记清除后会产生大量的内存碎片 => 剩余可用空间足够存放某个大对象,但连续空间不足以存放,也就无法存放

    3.2.2 复制算法

    "复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。

    缺陷:

    1. 可用的内存空间被压缩到一半,因此存在大量的内存浪费
    2. 该算法只在对象为“朝生夕死”状态时运行效率比较高

    3.2.3 标记-整理算法

    标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

    3.2.4 分代算法

    针对不同的对象类型,JVM才用了不同的垃圾回收算法,该算法被称为分代收集算法。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

    哪些对象会进入新生代?哪些对象会进入老年代?

    • 新生代:一般创建的对象都会进入新生代;
    • 老年代:大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代。

    两个GC的特性:

    1. 新生代GC:又叫Minor GC,采取复制算法,效率比较高。
    2. 老年代GC:又叫Major GC,采取标记清除/标记整理算法;效率比较差,一般比新生代GC慢10倍以上。

    4.垃圾收集器

    4.1 概念

    •  并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态
    • 并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个CPU上。
    • 吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值

    4.2 各种收集器

    4.2.1 Serial收集器(新生代收集器,串行GC)

    新生代收集器(复制算法);单线程;不常用

    4.2.2 ParNew收集器(新生代收集器,并行GC)

    新生代收集器(复制算法);多线程搭配;CMS方案

    4.2.3 Parallel Scavenge收集器(新生代收集器,并行GC)

    新生代收集器(复制算法) => 吞吐量优先;适用性能优先的程序。

    4.2.4 Serial Old收集器(老年代收集器,串行GC)

    老年代收集器(标记整理法);单线程

    4.2.5 Parallel Old收集器(老年代收集器,并行GC)

    老年代收集器(标记整理法);吞吐量优先

    所以在吞吐量优化的程序,只有一种选择:Parallel Old+Parallel Scavenge

    4.2.6 CMS收集器(老年代收集器,并发GC)

    1. 老年代收集器
    2. 标记清除算法
    3. 用户体验优先 => 整体看是并发(垃圾回收线程和用户线程同时执行)的过程,有局部的stw(少许时间是暂停用户线程的)                                                                                                                                 => 此时,CMS一般是搭配新生代的ParNew收集器                                                                   => 表现特性:并发收集,低停顿
    4. 步骤:分为4个步骤

      (1)初识标记:标记GC Roots能直接关联的对象,需要STW 
      (2)并发标记:进行GC Roots引用链追踪的过程(搜索引用路径)
      (3) 重新标记:修复第2个阶段用户线程同时执行时,产生标记变动的总录,需要STW
      (4) 并发清除:并发消除垃圾

    5. 缺陷

    (1)CPU比较敏感:用户体验优先,就意味吞吐量稍微低一点(单次停顿时间短,整个停顿时间长一点)=>CPU利用率下降
    (2)浮动垃圾问题(浮动垃圾:第4个阶段用户线程并发执行时产生的垃圾,在此次GC无法回收,称为浮动垃圾)会出现两个问题:

    1. 需要预留一部分空间 (并发清除阶段用户线程创建的对象)
    2.  并发模式失致(Concurrent Model failure)

      并发清除阶段用户线程创建的对象超出预留空间大小=>再次触发另一次的老年代GC
      说明:CMS本身就是老年代GC,所以这里就是老年代gc时,又触发一次老年代gc,而者年代gc是比较耗时(效率比较低)
      方式:采取老年代gc的后备方案:Serial Qid收集器进行回收

    (3)内存碎片问题
    标记清除算法就会带来这个问题;内存碎片只是现象,对GC的影响:可能导致提前触发GC

    说明:所有可用空间足够,但连续的可用空间不足存放大对象

    4.2.6 G1收集器(唯一一款全区域的垃圾回收器)

    说明:使用G1,堆的内存划分,就不是一个新生代(E区*1+S区*2)及一个老年代
    内存划分方案:把堆划分为多个相同大小的region区,动态分配为E区,S区,或T区(Tenured区,老年代)

    1. 老年代收集器
    2. 全堆收集器=>整体看基于“标记整理算法” 局部看基于“复制算法”
    3. 用户体验优先
    4. 步骤 

      新生代回收:回收多个E区+多个S区,复制存活对象到空的region区(动态指定它为S区)
      老年代回收:分为4个阶段
      (1)初识标记:和cms类似(标记GC Roots关联的对象,STW),不同的是,可以和新生代gc同时执行
      (2)并发标记:和cms类似,多:优先回收(Garbage First,G1名词的由来)=>直接回收存活率低或几乎没有存活的region
      (3)最终标记:和cms类似
      (4)筛选回收:筛选存活率低的region回收

  • 相关阅读:
    体细胞杂交第六弹!worthington组织培养术语终章
    C++11:为何引入noexcept替代throw
    某程序员发现 CSDN官方“漏洞”,立省¥10000+,抓紧薅吧
    如何写一篇提升自己的文章
    Postman中的断言
    Java内存模型与volatile
    STM32 BootLoader设置
    【Python复用脚本】根据excel表格的IP和资产归属部门,保存docx文件到部门目录
    【C# 调试】.net中的 .pdb文件是什么,有什么用
    面试题:Java中为什么只有值传递?
  • 原文地址:https://blog.csdn.net/m0_59155415/article/details/125906774