【Java EE】-JVM

作者:学Java的冬瓜
博客主页:☀冬瓜的主页
专栏:【JavaEE】
分享:
雨下整夜
我的爱溢出就像雨水
——《七里香》

主要内容:JDK,JRE,JVM三者之间的联系。JVM内存区域划分:本地方法栈(Native Method Stacks),虚拟机栈(JVM Stacks),程序计数器(PC),堆区(Heap),元数据区(MetaSpace)。JVM类加载机制,JVM类加载时机,JVM类加载过程,JVM类加载方式(双亲委派模型)。JVM垃圾回收机制(GC),如何判定一个对象是垃圾?看是否有引用指向这个对象。如何清理垃圾?标记清除,复制算法,标记整理,分代回收算法。

【Java EE】-JVM_第1张图片

文章目录

    • 一、JDK JRE JVM
    • 二、JVM 内存区域划分
    • 三、JVM 类加载机制
      • 1)类加载时机
      • 2)类加载的过程
      • 3)双亲委派模型(类加载的方式)
    • 四、JVM 垃圾回收机制
      • 1)什么是 GC
      • 2)GC 如何判定对象是否是垃圾
        • 2.1> 引用计数
        • 2.2> 可达性分析
      • 3)GC 如何清理垃圾
        • 3.1> 三个基本回收算法
          • a 标记清除
          • b 复制算法
          • c 标记整理
        • 3.2> 分代回收算法
        • 3.3> 垃圾回收器

一、JDK JRE JVM

问题·:我们都知道,要想在电脑上运行Java程序,首先得安装上jdk,那么jdk是啥?它是拿来干啥的?为啥安装好jdk后就可以运行Java程序了?

JDK: 全称 Java Development Kit(Java开发工具包),包含Java运行时环境(JRE),Java开发工具。Java开发工具又包含Java编译器(Javac),Java学习文档和代码示例等。
Javac负责将Java源代码编译为字节码文件(.class文件)。

JRE: 全称 Java Runtime Environment(Java运行时环境),包含Java虚拟机和Java类库(Java标准库和API)。

JVM:全称Java Virtual Machine(Java虚拟机):
JVM 负责将Java字节码翻译成计算机可执行的机器码,并提供了一系列的运行时环境支持。
Java虚拟机中包含:
类加载器(Class Loader) :负责将编译后的字节码加载到内存中。
执行引擎(Execution Engine):负责执行加载到内存中的字节码。
运行时数据区(Runtime Data Area):包含了Java程序运行时所需要的内存空间,如方法区、堆、栈等。
垃圾回收器(Garbage Collector):负责自动回收不再使用的对象所占用的内存空间。


总的来说:JDK和JRE以及JVM的关系如下,(如下图所示):
JDK = JRE + Java开发工具
JRE = JVM + Java核心类库

【Java EE】-JVM_第2张图片

解答:了解了上述的JDK、JRE、JVM后,我们不难理解,当我们安装好JDK后,就可以使用JDK中的Java编译器 将Java源代码编译成字节码文件,然后使用JRE中的 JVM来执行这些 字节码文件。JVM会将字节码翻译成计算机可执行的机器码,并提供运行时环境支持,使得Java程序能够在计算机上运行。

二、JVM 内存区域划分

【Java EE】-JVM_第3张图片

可分为五个部分:
① 本地方法栈(Native Method Stacks):
用于记录native方法调用。是给要调用的 native方法(是由C/C++实现的JVM内部的方法)准备的栈空间。
② 程序计数器(Program Counter Register):
简称PC,PC存储着当前线程即将执行的指令的地址。
③ 虚拟机栈(JVM Stacks):
用于记录方法调用。每个线程有一份虚拟机栈,当该线程中有一个方法调用时,会在虚拟机栈中申请一个栈帧(开辟空间空间),并记录下当前方法的调用关系。每个栈帧会包含入口地址,参数值、局部变量、返回地址等等。
注意:每个线程中调用方法时,会为方法申请函数栈帧,这个函数栈帧是在虚拟机栈中的,一个线程对应一个虚拟机栈,而虚拟机栈中的一个栈帧对应一个方法调用。

④ 堆区(Heap):
new出来的对象,都是在堆上的。因此堆的空间比其他空间大很多。一个进程对应一个堆,该进程中的线程共享该堆空间。堆的空间会使用垃圾回收机制。
⑤ 元数据区(Metaspace),或方法区:
用于存储类对象、常量池、静态成员。JVM虚拟机加载解析好.class文件后,会将类对象保存到这里。
注意:每个Java程序在运行时都会创建一个Java虚拟机(JVM)实例,一个进程对应一个Java虚拟机,同时一个进程有一个堆和一个元数据区。

三、JVM 类加载机制

1)类加载时机

在Java程序启动时,首先使用javac编译将源文件(.java)编译生成字节码文件(.class),然后由Java虚拟机(JVM) 解析和执行字节码文件,最开始是将包含“main”方法的主类加载并解析;程序运行过程中,后续需要使用其他类的时候,再进行所需类的类加载操作。

类加载是在Java程序运行过程中第一次使用类时发生的。包含下面几种触发情况:
① 创建类的实例对象时,会先进行类加载。比如 new 一个对象时,会先加载这个对象所属的类。
② 访问类的 静态成员(静态变量,静态方法)时,触发类加载。
③ 使用反射机制访问类时,会先类加载。如获取类的class对象,调用类的方法等,都会先进行类加载。
④ 当启动Java程序时,会先加载包含"main"方法的主类。

2)类加载的过程

【Java EE】-JVM_第4张图片

在Java中,类加载的过程由三个阶段组成:加载(Loading)、连接(Linking)、初始化(Initialization)。其中,连接阶段又包括验证(Verification)、准备(Preparation)、解析(Resolution) 三个步骤。下面我们就来了解这些阶段分别做了什么。

Loading:加载阶段,通过类的名字来查找编译后生成的对应类的字节码文件,并将字节码文件读取到内存中。
Linking:连接阶段包含三个部分。

Verification:验证字节码文件的正确性和安全性,确保符合Java虚拟机规范。
Preparation:准备,为类对象分配空间,并将内存空间赋予默认值"0",保存部分类信息。
Resolution:解析,对常量池进行初始化,虚拟机将常量池中类或接口等的符号引用转换成直接引用,以便定位到实际的内存空间。(在解析阶段,虚拟机会根据符号引用的信息,通过在方法区中查找类信息(在Preparation阶段保存的),来找到对应的直接引用)

注解:符号引用是一种以符号形式进行引用的方式,它包括类的名称、方法描述等,但是不具体指向实际空间上的某个位置。而直接引用则是内存中直接指向目标空间的指针、偏移量等。

Initialization:初始化,对类的对象进行真正的初始化操作,比如执行构造方法,执行静态代码块代码等。

3)双亲委派模型(类加载的方式)

1> 什么是双亲委派模型?
在Java中,所有的类都需要 类加载器(ClassLoader) 进行加载才能被JVM识别和使用。
双亲委派模型就是类加载器的一种工作机制:当一个类加载器加载一个类时,它会首先将加载请求交给父类加载器去完成,直到父类加载器无法加载时,才由子类加载器加载。需要注意的是:这里的父类加载器和子类加载器之间不是继承关系,而是理解为层次关系。

【Java EE】-JVM_第5张图片

2> 双亲委派模型包含哪些来加载器?
如上图就是一个双亲委派模型(用户自定义添加了类加载器)
·BootstrapClassLoader·是启动类加载器,负责加载标准库中的类。ExtensionClassLoader是扩展加载类,负责加载JVM开发厂商提供的扩展库中的类。
ApplicationClassLoader是应用库扩展类,负责加载用户代码中的第三方库和项目中的类。
下面两个是用户自定义类加载器,可有可无。根据用户自己的需求而定。

3> 例子理解

举个例子,比如上图中,此处需要加载一个类时,会先从自定义加载器开始,但是自定义加载器不会直接进行这个类的加载,而是将加载请求交给它的父类加载器ApplicationClassLoader。但是AppClassLoader也不会直接进行这个类的加载,而是将加载请求交给它的父类加载器ExtensionClassLoader。ExtensionClassLoader也不会直接进行加载,而是将加载请求交给其父类加载器BootstrapClassLoader。BootstrapClassLoader会想要将加载请求交给它的父类加载器,但是它的父类加载器为null了,因此BootstrapClassLoader就根据要加载的这个类信息查找负责自己加载的标准库的相关类,如果找到就加载,如果找不到就算了。接着交给ExtensionClassLoader,也是查找自己负责加载的扩展库中的相关类,找到就加载,找不到就算了。然后交给ApplicationClassLoader,进行相应操作,如果还有自定义加载器也是一样。
总的来说:和是类似递归的方式实现。

4> 双亲委派模型的优点?
优点:它可以保证Java类的安全性和一致性。 比如你自定义实现了一个Integer类时,类加载的双亲委派模型方式会先使用父类加载器进行加载,因此会先加载到标准库中的Integer,这样可以防止和冲突。


注意:双亲委派模型也不一定要必须遵守。具体情况看需求。

四、JVM 垃圾回收机制

1)什么是 GC

1> GC,全称Garbage Collection,垃圾回收机制。

理解GC
我们知道,在C/C++中可以动态申请内存空间(malloc),这一部分的空间是在堆上的,当我们的程序不再需要使用这部分空间时,需要手动将动态申请的这一部分空间释放(free)掉,防止发生内存泄漏。
GC就是要做把不用的空间释放,归还给操作系统这样的操作。即GC就是Java虚拟机中的垃圾回收器自动帮我们回收了垃圾,而不需要我们手动回收垃圾,释放空间。像Java,python,Go,PHP,JS都是使用GC来解决垃圾回收的问题的。

问题1:我们知道,当进程关闭的时候(也就是退出程序),代码中开辟的空间就自动释放了。那么为啥必须将内存释放,似乎不释放也行?
解释:当程序作为客户端的程序的时候,确实是进程关闭,空间释放,即使是内存泄露一些也没多大影响,我把程序关了就行。但是,如果在服务器端就出大问题了,因为服务器7*24小时运行,很小的内存泄漏经过日积月累就很有可能成为一颗定时炸弹,不知道哪一天爆发。因此,应当时刻考虑内存泄露的问题。
问题2那么C/C++为啥不使用GC呢?
解释:原因是GC会消耗一定的资源,并且在垃圾达到一定量的时候进行清理时可能会出现卡顿问题,这个问题又称stw(Stop The World),但是C/C++追求的是极致的性能,因此没有采用GC来进行垃圾回收,而是需要程序员进行手动垃圾回收。

2> GC 的操作单位是对象,而不是字节

①垃圾回收(GC) 主要是针对堆区进行的。因为堆区是Java虚拟机分配对象内存的主要区域。
②垃圾回收(GC) 是以"对象"作为基本单位进行垃圾回收,即当这个对象的成员变量,方法等各种属性后续代码中不再用到时,就会进行垃圾回收。

3> GC 的实际工作流程

①找到垃圾,判定垃圾
②清理垃圾,释放空间

2)GC 如何判定对象是否是垃圾

问题:那么,怎么判断这个对象是否是垃圾呢?
核心思路:看是否有引用指向这个对象。

具体实现:
①引用计数
②可达性分析

2.1> 引用计数

在Python,PHP中的 垃圾回收(GC)使用的就是引用计数的方法。即给每个对象配备一个计数器,当有引用指向这个对象时,计数器+1,当引用被销毁时,引用-1。

引用计数进行垃圾判断的优缺点?
优点在于简单方便。
缺点在于:

有时内存浪费很多。当对象的空间很小且数量很多时,比如大量整形的对象(new Integer()),如果使用给这个对象配备一个计数器,那么就相当于增大了一倍的空间,浪费空间就很大。
存在循环引用的问题
举个例子,如下图:在指向下面的前两部分的代码后,1号对象和2号对象的引用计数都是2。当把car1和car2置为空,即不再指向它原来的对象时,可以发现1号对象和2号对象的引用都还是为1,这两个对象无法回收,本质就是car1的成员变量car还指向2号对象,car2的成员变量car还指向1号对象,但是car1和car2的car变量无法再被访问,因为car1和car2引用无了。
因此如果使用引用计数的方法来确定垃圾,还需要解决这个问题,在python和PHP中是搭配了其他机制,来解决循环引用的问题。
【Java EE】-JVM_第6张图片

2.2> 可达性分析

①怎么进行可达性分析?
把new出来的对象,整体上串起来,类似于一个有向图。可达性分析时,使用根搜索算法,这个算法以一组"根"的起始对象开始,从这些根对象出发,递归遍历(DFS或BFS)所有的引用关系,标记所有可达的对象。再和所有对象的名单比对,不可达的对象就可以回收,释放空间。
我们再来看看引用计数的循环引用的问题在这里怎么样:不难发现,引用car1和car2的car已经无法找到,自然car1的car对2号对象的引用也就无法访问,进而就是2号对象不可达,car2的car对1号对象的引用无法访问,进而就是1号对象不可达,所以最后1号和2号对象都可以作为垃圾清除。

②根对象的选取?
虚拟机栈的栈帧中的局部变量引用的对象
方法区中的静态成员变量

3)GC 如何清理垃圾

基本方法有:标记清除、复制算法、标记整理三个算法。分代回收算法则是"复合型"算法,除此之外,还有并发标记算法CMS,还有G1,ZGC算法等。

3.1> 三个基本回收算法

a 标记清除

在可达性分析时,给所有可达的对象打上存活标记。在清除阶段,垃圾回收器遍历整个堆内存,清除未被标记的对象,相对而言就是清除标记为垃圾的对象。

标记清除的优点在于:简单方便。
缺点:内存碎片化。 如果后续需要在堆区申请一块大的空间,有可能因为内存碎片化的问题导致无法成功。

【Java EE】-JVM_第7张图片

b 复制算法

将整个堆空间分成两半,首先使用左边一半(或右边一半也行),进行可达性分析后,就把可达的对象标记为存活对象,未标记的对象即相对的标记为垃圾的对象。当左边的空间达到一定的阈值时,会触发复制操作。将标记为存活的对象复制到右边,再把左边的整个空间全部释放。右边快满时,也做相应的操作。
复制算法优缺点:使用复制算法可以避免内存碎片化的问题。但是如果进行复制操作时,垃圾很少,但是存活的对象太多,这样复制的成本就大了。

【Java EE】-JVM_第8张图片

c 标记整理

进行可达性分析后,就将可达的对象标记为存活,未标记的对象即为垃圾。整理时,从前到后将标记为存活的对象在这个空间里从前到后排列(可能会覆盖标记为垃圾的对象),然后把最后一个存活对象后面的所有空间释放。(类似于顺序表删除中间元素,有元素搬运的过程)

【Java EE】-JVM_第9张图片

3.2> 分代回收算法

分代回收算法的核心是:分代,具体逻辑是,认为存在的越久的对象,之后也很可能长期存在。

怎么判断对象存在时间的长短?
分代回收算法依据在垃圾回收中存活下来的次数,将堆分成了新生代和老年代两个大的部分。新生代又分为Eden(伊甸区)和s0,s1幸村区。如下图所示:

【Java EE】-JVM_第10张图片

分代回收算法怎么进行垃圾回收?
①新new出来的对象,会在伊甸区Eden开辟空间,经过一轮的可达性分析后,会将垃圾对象清除,一般来说剩下的存活对象就很少了,将存活的对象放入幸存区S1
②在幸存区S1中又经过一轮垃圾回收考验存活下来的对象则拷贝到S2,然后在S2中又经过一轮垃圾回收考验存活下来的对象则拷贝到S1,如此反复,达到一定次数的垃圾回收在幸存区还存活下来的对象就将它放到老年区。
③在老年区中,则会放缓垃圾回收的频率,如果垃圾回收扫描时,扫到了垃圾,使用标记整理进行垃圾回收。

分代回收算法是"复合型"算法,体现在哪里?
在幸存区中,进行s1和s2的轮换着复制,是复制算法,但是由于空间小,复制成本不高。在老年代中,使用标记整理的方式,由于幸存到老年代的对象其实很少,所以标记整理的整理成本也不高。

3.3> 垃圾回收器

实际上,我们所说的这些垃圾回收算法,在不同的垃圾回收器中有各自的实现。有的追求垃圾回收的效率高,有的追求垃圾回收时STW短等,具体看业务场景而定。

你可能感兴趣的:(【JavaEE】,java-ee,jvm,java)