JVM详解(包括JVM内存模型与GC垃圾回收)

发布于:2025-03-26 ⋅ 阅读:(25) ⋅ 点赞:(0)

📖前言:

        学会使用Java对于一个程序员是远远不够的。Java语法的掌握只是一部分,另一部分就是需要掌握Java内部的工作原理,从编译到运行,到底是谁在帮我们完成工作的?

        接下来着重对Java虚拟机,也就是JVM有一个深刻认识,对日后完成项目的开发或是更底层的开发有很大的帮助。

        接下来说的均为个人的一点小见解和观点,希望大家多多指点!

🎈JVM:

        JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。

虚拟机是指通过软件模拟的具有完整硬件功能的、运⾏在⼀个完全隔离的环境中的完整计算机系统。

常⻅的虚拟机:JVM、VMwave、Virtual Box。

JVM 和其他两个虚拟机的区别:
1 .  VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
2.JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进⾏了裁剪。
JVM 是⼀台被定制过的现实当中不存在的计算机。

🎨JVM与JDK的关系?

        知道JVM的基础概念,可能现在就有点懵了,JVM和JDK有什么关系吗?那么开始梳理一下与JAVA开发相关的组成部分;

JDK:

        官方JDK(Java Development Kit) 是Java开发工具包,包含了Java编译器(javac)、Java程序打包工具(jar)、Java程序运行环境(JRE)、文档生成工具(javadoc)以及其他开发工具,如调试的工具(jdb)。JDK是为Java开发人员提供的完整开发环境,包含了开发和运行Java程序所需的一切。

         非官方:JDK就是一个工具包,JDK是JRE的超集,JDK包含了JRE的所有开发,调试和监视应用程序等工具。当要开发Java应用程序时,需要安装JDK.

(JDK里面的工具非常多,是这么多工具才能支撑起我们编写Java程序)

        当然有一个重要的组成部分——JRE(Java程序运行环境).

接下来就针对JRE做一个详细说明:

JRE:

        官方JRE(Java Runtime Environment) 是Java运行环境,包含了JVM和Java类库。JRE是运行Java程序所需的环境,它提供了Java程序运行所需的库文件和JVM。因此,如果你只需要运行Java程序,那么只需要安装JRE即可。        

        

需要区分的地方:

        这里再穿插一下IDEA是什么,IDEA是编写代码的平台,里面集成了一些好用的工具,例如预编译工具,能够在出现一些语法错误的时候提示我们,但是起到编译运行的还是JVM虚拟机!

JVM内存模型:        

        首先在开头处需要声明一下: JVM内存模型与JMM内存模型不是一回事,JVM内存模型就是虚拟机内部的内存模型,但是JMM内存模型主是描述Java线程之间如如何工作的,如何做到线程之间共享变量的。

        JVM内存模型分为5个区,由这5个区合力搭建出JVM内存模型的。

程序计数器:记录了要执行的下一条指令的地址

栈:

        虚拟机栈:保存临时变量、参数、函数调用的关系

        本地方法栈:与虚拟机栈类似、底层是C或C++代码

堆:保存了new的对象

方法区(元数据区):保存了静态变量、常量、常量池等

        栈区、程序计数器是线程私有的

        堆区、方法区是线程共享的 

🏉类加载过程:

        了解了JVM内存模型,接下来需要了解.class文件在JVM读取与加载的过程。

        1.加载:读取.class文件中的内容

        2.验证:校验读取到的内容是否符合JVM规范。

        3.准备:为类的静态变量分配内存,并将其初始化为默认值。

        4.解析:将符号引用全部替换为直接引用

        5.初始化:此阶段是执行类构造器()方法的过程。此方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并而成。当初始化一个类的时候,如果发现其父类还未进行初始化,则需要先触发其父类的初始化。Java虚拟机会保证一个类的()方法在多线程下被同步加锁。

双亲委派模型:

        在类加载时,需要遵循双亲委派模型:

        如果一个类被请求加载时,首先不会自己加载,将加载的请求抛给父类,如果父类可以加载,自己就不再加载,如果父类不能加载,自己再进行加载。

优先级由高到低:标准库类加载器->扩展库类加载器->第三方库类加载器->自定义类加载器

        如下图:

双亲委派模型作用:       

1. 避免重复加载类: ⽐如 A 类和 B 类都有⼀个⽗类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进⾏加载时就不需要在重复加载 C 类了。
2. 安全性 :使⽤双亲委派模型也可以保证了 Java 的核⼼ API 不被篡改,如果没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object类的话,那么程序运⾏的时候,系统就会出现多个不同的 Object 类,⽽有些 Object 类⼜是用户自己提供的因此安全性就不能得到保证了。

🥎类初始化顺序(带有继承关系):

        父类静态代码块、静态变量->子类静态代码块、静态变量->父类普通变量->父类构造函数->子类普通方法->子类构造方法

🏀GC垃圾回收:

        首先需要搞清楚,哪里的垃圾需要回收,那里的垃圾不需要回收?        

        线程私有的区域中的垃圾我们不需要回收,当线程结束时,这些区域也会跟着清空

        线程共享的区域,如果线程结束,但是线程共享区中的引用对象不会随之消失。

        因此着重关注的是两个区域——堆区、方法区

        但是相较于创建的量和空间的消耗来说,堆区是垃圾回收的重灾区,但是方法区也有垃圾回收机制,但是不怎么关注。

🏐如何判断该对象是否是垃圾?

        如何判定堆区中的对象就是垃圾呢?有以下几种方法:

🍿1.引用计数法:

        采用计数的方式,如何一个对象被引用,此时计数器+1,如何不在引用该对象计数器的值就-1。

造成的问题:

        如果采用方法进行垃圾的判定,会造成"循环引用"的问题,例如:

class Student{
    private String name;
    public Student student;
    public Student(String name) {
        name = this.name;
    }
}
public class Test {
    public void circulation(){
        Student a = new Student("a");
        Student b = new Student("b");
        a.student = b;
        b.student = a;
        a = null;
        b = null;
    }
}

此时明明a、b两个引用已经断开指向a、b的对象,但是其内部还有相互的指向,没有办法被释放了。

🍕2.可达性分析:

        为了解决上述出现的问题,可以定期进行扫描,如果发现虽然被引用着,但是到达不了GCroots也会被视为垃圾,如下图:

                     

         也就是从GC Roots开始扫描,如果是能到达的对象此时构成了引用链,如果在某个对象在经过下次扫描不在这条引用链上了,就被视为"垃圾"。

   该方法被作为GC垃圾回收的首选方法。但是在这个过程中可能会造成STW(Stop The World),可以理解为时间停顿。

🥯垃圾回收方法:

        通过可达性分析以后判断出哪些对象是垃圾,接下来就来谈一谈这些垃圾是如何被回收的?

有以下方法:

🌭 1.标记回收:

        经过可达性分析以后,如果是垃圾,此时被标记一下。之后经过多轮检测之后,将标记的垃圾进行清除:

        

缺点:

        造成内存碎片化问题,此时在存储效率上会大减折扣。

🧇2.标记整理:

        在标记回收算法的基础上,进行改良,因为上述的做法会造成内存碎片问题,如果每次回收以后,对内存中存活的对象继续宁整理,整理在一起,此时内存空间可利用率会提高。

        

缺点:

        整理复制需要消耗大量的实践,时间效率降低。

🧈3.复制算法:

        如果我么每次将内存空间分为两部分,一部分空出来,另一部分使用,垃圾回收只需要对使用的一部分进行回收,进行标记,经过多轮之后,将存活的对象复制到另一块没有使用的内存当中。

 

缺点:

        如果存活的对象较多,此时复制时比较消耗时间。

🍞4.分代回收算法:

        将堆区中的区域划分为新生代与老年代

 

新生代又被划分为:伊甸区、幸存区

  • 堆内存为两个区:新生代 (Young) 和老年代 (Old);
  • 新生代默认占堆内存的 1/3,老年代默认占堆内存的 2/3;
  • 新生代又分为 Eden 区、Survivor From区、Survivor To区默认比例是 8:1:1   

过程:

        首次被创建的对象先进入伊甸区,之后经过可达性分析与复制算法,将幸存的对象放入幸存区,再次经多轮过可达性分析与复制算法,此时将最终存活的对象放入老年代。老年代中进行可达性分析的频次就比较低了。

😶常见的GC垃圾回收器:

        上述讲的都是一些垃圾回收的一些思路,这些GC垃圾回收器内部回收垃圾时都有用到上述的思路。

1️⃣1.GMS垃圾收集器:

        老年代收集器,并发GC,GMS是一种获取最短停顿时间(STW)为目标的收集器。

实现原理:

        其内部是基于标记-清除算法实现的。(具体大家可以自行了解)

由于整个过程中耗时最⻓的并发标记和并发清除过程收集器线程都可以与⽤⼾线程⼀起⼯作,所以,从总体上来说,CMS收集器的内存回收过程是与⽤⼾线程⼀起并发执⾏的。       

2️⃣2.G1垃圾收集器 :

        全区域的垃圾收集器,其主要的做法是将分代回收算法中的区域划分为更小的区域块,此时不再是一次性进行复制算法回收,只对一部分进行可达性分析+复制算法回收。

年轻代垃圾收集:
        在G1垃圾收集器中,年轻代的垃圾回收过程使⽤复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域。
老年代垃收集:
对于⽼年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器⼀样,但略有不同:
初始标记 (Initial Mark)阶段 - G1需要暂停应⽤程序的执⾏,它会标记从根对象出发,在根对象的第⼀层孩⼦节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc⼀同发⽣的。也就是说,在G1中,你不⽤像在CMS那样,单独暂停应⽤程序的执⾏来运⾏Initial Mark阶段,而是在G1触发minor gc的时候⼀并将年⽼代上的Initial Mark给做了。
并发标记 (Concurrent Mark)阶段 - 在这个阶段G1做的事情跟CMS⼀样。但G1同时还多做了⼀件事 情,就是如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很⼩或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,⽽不⽤等到后⾯的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,⽅便后⾯的clean up阶段使⽤ 。
最终标记 (CMS中的Remark阶段) - 在这个阶段G1做的事情跟CMS⼀样, 但是采⽤的算法不同,G1采⽤⼀种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象。
筛选回收( Clean up/Copy)阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有⼀个Cleanup/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进⾏回收,这个阶段也是和minor gc⼀同发⽣的。

网站公告

今日签到

点亮在社区的每一天
去签到