JVM笔记
概述:
JVM是一个虚构出来的计算机,可运行Java代码。JVM是运行在操作系统之上的,它与硬件没有直接的交互。
运行过程:
Java源文件===>编译器===>字节码文件===>JVM===>机器码
体系结构:
Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块
1.类加载器子系统
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称连接。类加载工作由ClassLoader及其子类负责。如下图:
Java类生命周期
(1)加载
加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:
- 通过“类全名”来获取定义此类的二进制字节流。
- 将类.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,
- 然后在堆中创建一个java.lang.Class对象,用来封装类在方法区的数据结构。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由 虚拟机实现 自行定义,虚拟机并未规定此区域的具体数据结构。然后在Java堆中实例化一个Java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。
JVM设计者把类加载阶段中的通过类全名来获取此类的二进制字节流这个动作放到Java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类。实现这个动作的代码模块成为“类加载器”。类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它
类加载器可以大致划分为以下三类:
- 启动类加载器:Bootstrap ClassLoader,它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的Java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由Java.ext.dirs系统变量指定的路径中的所有类库(如Javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的Java class文件。
类加载器的双亲委派模型
过程:如果一个类加载器收到了类请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一层都是如此,因此所有类加载的请求都会传到启动类加载器,只有当父加载器无法完成该请求时,子加载器才去自己加载。
好处:双亲委派的好处 : 主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,由于每个类加载都会经过最顶层的启动类加载器,比如 java.lang.Object这样的类在各个类加载器下都是同一个类(只有当两个类是由同一个类加载器加载的才有意义,这两个类才相等。)
(2)验证
验证是连接阶段的第一步,这一步主要的目的是为了保证从加载阶段获取的字节流中包含的信息是符合虚拟机要求,并且对虚拟机来说是安全的。这个阶段主要完成4个方面的校验工作,包括文件格式验证、元数据验证、字节码验证和符号引用验证。
- 文件格式验证主要是验证字节流是否符合Class文件格式规范,以及字节流能否被虚拟机处理,例如字节流文件是否以魔数0xCAFEBABE开头、主次版本号是否是虚拟机能处理的版本等。
- 元数据验证主要是对字节码描述的信息进行语义分析,用来确保字节码描述信息符合Java语言规范,例如类的继承关系、类是否继承了不允许被继承的类、类实现了接口是否实现了接口中的所有方法等。
- 字节码验证主要是通过数据流和控制流来分析和确定程序的语义是否合法、符合逻辑,例如验证操作数栈中数据的存取是否会出现类型不匹配、跳转指令是否会跳转到方法体以外的字节码指令上等。
- 符号引用验证主要是确保解析动作能够正常执行,主要对常量池中的符号引用进行匹配性校验,例如符号引用中的类、属性和方法的访问性是否可以被当前类访问、能否通过字符串描述的全限定名来找到对应的类等。
(3) 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
这个阶段中有两个容易产生混淆的知识点:
- 首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
- 其次是这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。假设一个类变量定义为: public static int value = 1024;那么变量value在准备阶段过后的初始值为0而不是1024,因为这时候尚未开始执行任何Java方法,而把value赋值为1024的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为1024的动作将在初始化阶段才会被执行。
注:如果是常量的话(被final修饰),就会在准备阶段变量value就会被初始化为ConstantValue属性所指定的值1024
(4) 解析
解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。
- 符号引用:符号引用是一组符号来描述所引用的对象,符号可以是任何形式的字面量,只要使用时能定位到目标即可,符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
- 直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄(一种特殊的智能指针)。直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中。
解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在.运行时常量池中记录直接引用,并把常标示为已解析状态),从而避免解析动作重复进行。
(5)初始化
初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码(初始化成为代码设定的默认值)。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源
2.Native Interface 本地接口
(1)Native Method Stack 本地方法栈
(2)PC Register 程序计数器
(3)Method Area 方法区
(4)Stack 栈
5)Heap 堆
为什么要分代勒,因为针对每个年龄代,都有不同的垃圾回收算法,以及内存分配机制。如果将所有对象放在一起,第一是会造成频繁遍历判断回收的开销,第二是会造成复制、移动的开销,为什么会有复制、移动,因为回收内存必然会造成内存碎片,而内存碎片会导致空间浪费,所以必须通过复制、移动来清理随便,使得空闲内存连续。
- 从年轻代 (包括 Eden 和 Survivor 区域) 回收内存被称为 Minor GC;从老年代 GC 称为 Major GC;同时对新生代、老年代和永久代进行垃圾回收叫做 Full GC;
- Survivor 的存在意义,就是减少被送到老年代的对象,进而减少 Full GC 的发生。
- 年轻代 gc 使用 “停止 - 复制” 算法,停止指的是,发生 GC 的时候会暂停除了 GC 线程以外的所有线程的运行。所以年轻代频繁 gc 会极大影响系统吞吐量
- 老年代使用 “标记 - 整理” 算法,即将存活的对象向一边移动,以此来保证回收后,内存依然是连续的,不会出现内存碎片。
- 每次年轻代的 Eden 发生 Minor GC 时,虚拟机都会检查每次晋级老年代的大小是否大于老年代的剩余大小,如果大于则会触发 FULL GC

说明:
HotSpot虚拟机在1.8之后已经取消了永久代,改为元空间,类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。
GC是什么?为什么要有GC?
答:GC是垃圾收集的意思(Garbage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。
补充
垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?