JVM
2025-10-02 17:30:12 0 举报
JVM 架构设计相关知识点总结。JVM,即Java虚拟机(Java Virtual Machine),是一种抽象出来的计算机,存在于各种物理硬件平台上。JVM的主要职责是为Java程序提供一个统一的运行环境,实现了Java的跨平台特性。JVM的核心内容包括Java类加载器、运行时数据区、执行引擎等。
作者其他创作
大纲/内容
Normal
| 11
JIT 编译器 (Just-In-Time Compiler):在程序运行时,JVM 会监控代码的执行频率。对于那些被频繁执行的热点代码 (Hot Spot Code),JIT 编译器会将其编译成本地机器码,并缓存起来(存储在方法区的 JIT 代码缓存中)。下次执行相同代码时,直接运行机器码,大大提高了效率。
`HotSpot VM` 内嵌了两种主要的 `JIT` 编译器
本地方法栈
_owner
Object reference
类加载器
运行时数据区
虚拟机栈
类加载的时机
加载 (Loading): 获取字节流:通过一个类的全限定名(包名+类名)来获取定义此类的二进制字节流(字节码)。获取途径多样,包括 JAR/WAR 包、网络、运行时计算生成(如动态代理)、由其他文件生成(如 JSP)等。 转换数据结构:将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。 创建 Class 对象:在内存中(堆上)生成一个代表这个类的 `java.lang.Class` 对象,作为程序访问方法区中这些类型数据的外部接口连接 (Linking): 验证(Verification):确保字节码合法、安全,符合 JVM 规范。包括文件格式、元数据、字节码和符号引用验证。 准备 (Preparation):为类变量(被 `static` 修饰的变量)分配内存并设置默认零值(如 int 为 0,引用类型为 null)。但 `static final` 修饰的常量在编译时就能确定其值,在此阶段直接赋真实值。 解析 (Resolution):将常量池中的符号引用(如类和方法的名称)替换为直接引用(内存地址)。 符号引用:以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关。例如 `java.lang.Object`就是一个符号引用。 直接引用:可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局直接相关。 解析动作主要针对类或接口、字段、类方法、接口方法等符号引用进行。初始化 (Initialization): 这是执行类构造器 `<clinit>()` 方法的过程。该方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。 JVM 保证 `<clinit>()`方法在多线程环境中被正确加锁和同步。这意味着多个线程同时初始化一个类时,只有一个线程会执行 `<clinit>()`方法,其他线程会被阻塞。 初始化一个类时,如果其父类还未初始化,会先初始化父类。使用 (Using) 和卸载 (Unloading): 使用:当类完成了初始化,就可以被正常使用了,例如创建类的实例、访问类的静态成员等。 卸载:当一个类不再被需要时(即该类的 Class 对象没有任何引用,且加载该类的 ClassLoader 实例也被回收),JVM 就可以在垃圾收集时将其从方法区(元空间)中卸载,释放其占用的内存空间。 由 Java 虚拟机自带的类加载器(如启动类加载器、扩展类加载器、应用类加载器)所加载的类,在虚拟机的生命周期中通常始终存在,不会被卸载。
动态链接
运行时常量池
C2 编译器(Server Compiler)编译速度慢,但进行更多深度和激进的优化(如逃逸分析、标量替换、循环展开)。追求更高的峰值性能。对长时间运行的、对峰值性能要求高的服务端应用。
继承 ClassLoader 类:创建一个新的类,继承 `java.lang.ClassLoader`。建议在构造函数中显式指定父类加载器,以确保清晰的类加载器层次结构。重写 findClass 方法:这是自定义类加载器的核心。你需要在此方法中实现查找并加载类字节码的逻辑(例如从文件、网络或其他来源),然后调用 `defineClass`方法将字节数组转换为 `Class`对象。应优先重写 `findClass`而非 `loadClass`以保持双亲委派模型。实现字节码加载逻辑:在 `loadClassData` 等方法中,根据类名获取类的二进制字节流。这可以是从文件系统、网络、数据库或其他任何来源。
解释器:负责逐条解释执行字节码指令。 它的优点是启动速度快,无需等待编译,可以立即执行。 缺点是执行效率相对较低,因为每条指令都需要解释。
堆(Heap)
对象存储布局
核心架构设计图
程序计数器
同样是线程私有的占用空间很小(正因为程序计数器存储的是指令地址(或偏移量),而非直接存储对象或数据,并且所需空间非常小)它记录着当前线程所执行的字节码指令的地址,指示下一条要执行的指令地址。是唯一 一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。当线程执行 Native 方法时,程序计数器的值为 `undefined`,这是因为本地方法的执行不受 JVM 管理
启用与配置 GC 日志
垃圾回收算法
Mark Word
Lock Record
字节码文件
栈帧
Klass Pointer
主流的 JVM 使用 可达性分析算法 来判断对象是否存活。1. 可达性分析 (Reachability Analysis):从一组称为 \"GC Roots\" 的对象作为起点,向下搜索所有被引用的对象,所走过的路径称为引用链。任何从 GC Roots 开始不可达的对象,就会被标记为可回收的垃圾。GC Roots 通常包括:虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象等。2. 引用计数法 (Reference Counting) (已基本废弃):虽然概念简单,但无法解决对象之间循环引用的问题,因此主流 JVM 已不再采用。垃圾回收的基本算法包括:标记-清除(Mark-Sweep):标记所有需要回收的对象,然后统一清除。缺点是会产生内存碎片。标记-复制(Copying):将内存分为两块,每次只使用一块。当使用的这块内存用完,就将还存活的对象复制到另一块,然后清除已使用的内存。效率高,但代价是可用内存减半。标记-整理(Mark-Compact):标记过程与“标记-清除”一样,但后续让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
Marked for GC
类的生命周期
Java 虚拟机管理的最大一块内存区域,所有线程共享,主要用于存放对象实例和数组。Java 中几乎所有通过 `new`关键字创建的对象都会在堆中分配内存。堆也是垃圾回收器(Garbage Collector) 主要的工作区域。为了更高效地进行GC,堆内存通常进一步划分为新生代(Young Generation) 和老年代(Old Generation)。新生代又分为Eden区和两个Survivor区(From/To)。通过 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数设置。通常将 -Xms和 -Xmx设置为相同值,以避免堆内存动态调整带来的性能开销-XX:NewRatio:设置老年代与新生代的比例(默认值为 2,表示老年代:新生代2:1)-XX:SurvivorRatio:设置 Eden 区与一个 Survivor 区的比例(默认值为 8,表示 Eden\\:S0\\:S18:1:1)。
类加载的延迟性(Lazy Loading):JVM 的类加载是按需进行的。一个类只有在第一次被主动使用时才会被加载和初始化,这有助于节省内存和提高性能。主动引用 (触发加载与初始化):创建实例:遇到 `new`、`getstatic`、`putstatic`或 `invokestatic`这四条字节码指令时。对应的常见 Java 代码场景是:① 使用 `new`关键字实例化对象。② 读取或设置一个类的静态字段(被 `final`修饰、已在编译期把结果放入常量池的静态字段除外)。③ 调用一个类的静态方法。反射调用:使用 `java.lang.reflect`包的方法对类进行反射调用时。例:`Class<?> clazz Class.forName(\"com.example.MyClass\");`初始化子类:当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。主类:当虚拟机启动时,用户需要指定一个要执行的主类(包含 `main()`方法的那个类),虚拟机会先初始化这个主类。默认方法:当一个接口中定义了 JDK 8 新加入的默认方法(`default`关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。被动引用 (不会触发初始化):通过子类引用父类静态字段:只会初始化父类,而不会初始化子类。通过数组定义引用类:定义某个类的数组并不会触发该类的初始化。例:`MyClass[] arr new MyClass[10];`访问编译期常量:访问 `static final`修饰的编译期常量不会触发初始化,因为常量在编译期已被存入调用类的常量池。
双亲委派模型
State
本地方法库
自定义类加载器
虚拟机栈(Java Virtual Machine Stack)
Tomcat 的类加载机制
C1 编译器(Client Compiler) 编译速度快,进行简单可靠的优化(如方法内联、去虚拟化、冗余消除)。对启动速度敏感的应用,如桌面程序。
# JDK 8及之前常用格式-XX:+PrintGCDetails # 打印详细的GC日志-XX:+PrintGCDateStamps # 输出GC的日期戳(推荐,便于追踪)-XX:+PrintGCTimeStamps # 输出GC的时间戳(相对于JVM启动时间)-Xloggc:gc.log # 将GC日志写入指定的文件# 如需日志滚动,避免单个文件过大,可添加-XX:+font color=\"#e74f4c\
堆
常见的垃圾回收器
对象创建的步骤
JVM 的垃圾回收
是类加载器之间的协作关系和加载规则。它的工作流程是:1. 当一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。2. 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中。3. 只有当父加载器反馈自己无法完成这个加载请求(在自己的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去完成加载。
_recursions
局部变量表
unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01
thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01
JVM 调优命令
实例数据 (Instance Data)
方法内联(Method Inlining):将被频繁调用的小方法的代码直接“复制”到调用者方法中,减少方法调用的开销(如创建栈帧、参数传递等)。这是最基础的优化之一。逃逸分析(Escape Analysis):分析对象的动态作用域,判断对象是否会被外部方法或线程所引用。公共子表达式消除(Common Subexpression Elimination):如果一个表达式之前已经计算过,并且表达式中的变量值没有改变,那么下次就直接使用之前的结果,避免重复计算。循环展开(Loop Unrolling):减少循环控制语句(如条件判断、递增)的开销,通过将循环体的代码复制多次来实现。死代码消除:移除永远不会被执行到的代码(如 `if (false)`后的块)或计算结果不被使用的表达式。
jps: 查看系统当前所有Java进程的PID和主类信息。jps -lvmjstat:实时监控JVM统计信息,如GC、类加载、JIT编译等。jstat -gcutil <pid> 1000 5(每1秒采集1次GC情况,共5次)jinfo:查看和font color=\"#e74f4c\
启动类加载器 (Bootstrap ClassLoader) 范围:`JAVA_HOME/lib`目录下的核心类库 (如 rt.jar)扩展类加载器 (Extension ClassLoader) 范围:`JAVA_HOME/lib/ext`目录或 `java.ext.dirs`变量指定路径下的类库应用程序类加载器 (Application ClassLoader) 范围:用户类路径 (`ClassPath`) 上所指定的类库自定义类加载器 (Custom ClassLoader) 范围:用户自定义的路径
Java字节码文件采用一种平台无关的二进制格式,其结构有严格规范,确保了“一次编译,到处运行”的能力。前端编译器(如 `javac`)生成的 Class 文件是一组以 8 位字节为基础单位的二进制流,其结构非常严谨,没有任何分隔符,数据项的顺序、数量、字节序(Big-Endian)都被严格限定。它主要包含以下信息:基础信息:魔数(CAFE BABE)、Class 文件版本号(对应 Java 版本)、访问标志(public、final 等)、父类和接口信息。常量池:存放字符串常量、类/接口名、字段名、方法名等符号引用,是 Class 文件的资源仓库。字段信息:当前类或接口声明的字段信息。方法信息:当前类或接口声明的方法信息,其核心是方法的字节码指令(JVM 指令)。属性信息:如源码文件名、内部类列表等附加属性。
Biased
ptr_to_heavyweight_monitor:62 | 10
打破了 Java 标准的双亲委派模型(Parent Delegation Model),以实现应用级别的隔离。标准双亲委派的局限:在标准JVM中,类加载请求会先委派给父加载器,这保证了核心库的安全,但会导致多个Web应用无法使用同一库的不同版本,引发版本冲突。Tomcat的“逆向”委托:Tomcat 的 `WebAppClassLoader`重写了 `loadClass()`方法,调整了加载顺序: 1. 检查本地缓存:首先检查该类是否已被当前加载器加载过。 2. 优先本地加载:违反双亲委派,优先尝试从本Web应用的 `WEB-INF/classes` 和 `WEB-INF/lib` 目录下加载。 3. 委托父加载器:如果本地找不到,再按照双亲委派模型委托给父加载器(如 `SharedClassLoader` -> `CommonClassLoader`)加载。 4. 防止核心库被覆盖:对于 `java.` 和 `javax.servlet.` 等核心类,会强制委托给父加载器,确保容器基础稳定性。这种“先己后人”的策略使得每个Web应用都能拥有自己独立的类空间,从而实现库的隔离。
ptr_to_lock_record:62 | 00
类加载检查 (Class Loading Check):① 当 JVM 遇到一条 `new`指令(或其他创建对象的指令)时,首先会检查这个指令的参数能否在运行时常量池中定位到一个类的符号引用。 ② 紧接着,它会检查这个符号引用所代表的类是否已被加载、解析和初始化过。 ③ 如果没有,那就必须先执行相应的类加载过程。分配内存 (Memory Allocation): 在堆(Heap) 中为新生对象划分一块确定大小的内存空间。分配方式取决于 Java 堆是否规整,而堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定 指针碰撞 (Bump the Pointer):如果堆内存是规整的(即所有用过的内存在一边,空闲的内存在另一边),那么虚拟机将采用指针碰撞法来为对象分配内存。具体做法是,在分配内存时,将指针向空闲空间方向移动一段与对象大小相等的距离 空闲列表 (Free List):如果堆内存是不规整的(已使用的和未使用的内存相互交错),虚拟机就需要维护一个空闲列表,记录哪些内存块是可用的。在分配时,从列表中找到一块足够大的空间划分给对象实例,并更新列表记录。 初始化零值 (Initialization to Zero):内存分配完成后,JVM 会将分配到的内存空间(不包括对象头)都初始化为零值(`0`,`false`,`null`等)。设置对象头 (Setting the Object Header)。执行 `<init>`方法 (Executing the Constructor): 包括构造方法和实例代码块中的初始化代码。在这个阶段,才会将对象初始化成预期的状态(为字段赋予用户定义的初始值)
_EntryList
方法区
Heavyweight Locked
null
本地库接口
如果分析发现一个对象没有逃逸出方法,就可能进行:栈上分配(Stack Allocation):对于无逃逸的对象,JVM 可以选择将其分配在栈内存上,而不是堆内存上,无需垃圾回收器(GC)介入,减轻 GC 压力。标量替换(Scalar Replacement):如果一个对象被证明是无逃逸的,并且其字段可以被分解(例如都是基本类型),JVM 就不创建这个完整的对象实例,而是直接将它的字段当作局部变量(标量)来使用,不仅减少了内存占用,还可能因为数据存储在栈帧或寄存器中而提高访问速度。同步消除(Lock Elision):如果发现对象是线程私有的,并且没有逃逸,那么对该对象进行的同步操作(如 synchronized)就可以被移除。
吞吐量、暂停时间和内存占用这三项指标共同构成了一个“不可能三角”。这意味着:追求高吞吐量(减少GC次数)可能意味着单次GC需要处理更多对象,导致暂停时间变长。追求低暂停时间(频繁GC)可能会增加GC本身的开销,从而降低吞吐量。增大堆内存可以降低GC频率,但可能使单次GC的暂停时间延长,并且内存占用更大。JVM 调优标准:在最大吞吐量优先的情况下,降低停顿时间。
Monitor
JIT 编译器在编译过程中优化
Thread - 0
Lightweight Locked
.class 文件
每个线程在创建时都会分配一个私有的虚拟机栈。用于存储 局部变量表、操作数栈、动态链接、方法出口 等信息。方法的调用和结束对应着栈帧的入栈和出栈。随线程的创建而创建,随线程的结束而销毁。通过 `-Xss`参数设置(如 `-Xss256k`,增大此值可以为更深的方调用链提供空间,但会减少整个虚拟机中可以创建的线程总数)主要由以下几部分组成:1. 局部变量表 (Local Variable Table): 用于存储方法参数和方法内部定义的局部变量。 以变量槽(Slot)为最小单位。一个 Slot 通常为 32 位(4个字节)。`long`和 `double`等 64 位的数据类型会占用两个连续的 Slot。2. 操作数栈 (Operand Stack): 是一个后进先出(LIFO) 的栈结构,用于存储方法执行过程中的中间计算结果和计算过程中的临时变量。 字节码指令(如 `iadd`加法指令)的执行通常从操作数栈弹出所需操作数,运算后再将结果压回栈中。3. 动态链接 (Dynamic Linking): 指向运行时常量池中该栈帧所属方法的引用。在 Java 源文件被编译成字节码时,所有变量和方法引用都作为符号引用存储在 Class 文件的常量池中。动态链接的作用就是在方法调用过程中,将这些符号引用转换为直接引用(实际内存地址)。4. 方法返回地址 (Return Address): 存放调用该方法的程序计数器(PC Register)的值。这个值指向了调用方法指令的下一条指令的地址 主要作用是记录方法调用者的位置信息,确保方法执行完毕后,程序能够正确返回到调用点并继续执行
Hashcode | Age | Bias 01
对象头
执行引擎
程序计数器(Program Counter Register)
对齐填充数据 (Padding)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的方法代码等数据。 类元数据 (Class Metadata):类的结构信息,如全限定名、父类、接口列表、访问修饰符、字段描述、方法描述等。 运行时常量池 (Runtime Constant Pool):每个类独有的区域,存储编译期生成的各种字面量和符号引用,支持运行时动态解析(如 `String.intern()`方法)。 静态变量 (Static Variables):类级别的静态变量(`static`修饰) 方法代码 (Method Code):由即时编译器(JIT)编译后的本地机器码(热点代码优化后的结果)。 在JDK 8之前,方法区的实现被称为永久代(PermGen),之后被元空间(Metaspace) 取代,后者使用本地内存,避免了永久代的大小限制和常见的OutOfMemoryError。JDK 8 之前 永久代 (PermGen) font color=\"#e74f4c\
操作数栈
_header
方法出口
_WaitSet
数组长度

收藏

收藏
0 条评论
下一页