当前位置: 首页 > news >正文

简单理解java虚拟机

简单理解java虚拟机

一、学习 JVM 的核心意义

  1. 面试刚需:避免依赖死记硬背 “面试八股”,从底层理解问题本质(如 Integer 缓存、静态方法能否重写)。
  2. 基础支撑:明确代码执行逻辑,是编写高可靠性系统的前提;若不理解 JVM,无法判断代码在底层的运行机制。
  3. 调优基础:解决线上实际问题,如内存配置(4G 是否足够)、服务崩溃定位、FullGC 频繁等,是 JVM 调优的必要前提。
  4. 能力分水岭:区分 “自主解决问题的一流程序员” 与 “仅做 CRUD 的二流程序员”—— 一流程序员会构建 JVM 底层知识体系,二流程序员认为 JVM 无关开发。

二、JVM 核心学习范围与执行流程

  1. JVM 定位:Java 是 “标准” 而非仅语言,只要生成符合 JVM 规范的class 文件,即可在 JVM 执行,实现 “一次编写,多次执行”,支持 Java/Scala/Groovy/Kotlin 等多语言。
  2. 主流实现:目前最主流的 JVM 是 Oracle 官方的HotSpot 虚拟机(JDK8 默认),通过java -version可查看(如Java HotSpot(TM) 64-Bit Server VM)。
  3. Java 文件执行流程
    • 源码编译:.java文件通过javac编译为class文件;
    • JVM 执行:class文件进入 HotSpot JVM,经过类加载(ClassLoader) → 内存分配(线程私有区域:程序计数器、虚拟机栈、本地方法栈;共享区域:堆、方法区 / 元空间)→ 执行引擎执行字节码。

image-20250922171800522

三、Class 文件规范

1. Class 文件结构

  • 本质:二进制文件,无法直接文本阅读,需通过工具(如 UltraEdit、javap 指令、IDEA 的 ByteCodeView 插件)查看。

  • 固定开头:所有 Class 文件必须以十六进制CAFEBABE(魔数)开头,是 JVM 规范的强制要求。

  • 核心组成(按 JVM 规范):

    结构字段 类型 说明
    magic u4 魔数,固定为 CAFEBABE
    minor_version u2 次版本号,JDK8 通常为 00 00
    major_version u2 主版本号,JDK8 为 00 34(对应十进制 52)
    constant_pool cp_info 常量池,存储类 / 方法 / 字段的符号引用等
    access_flags u2 访问标志(如 public、final)
    this_class u2 当前类索引(指向常量池)
    super_class u2 父类索引(默认指向 java/lang/Object)
  • 版本兼容性:高版本 JDK 编译的 Class 文件(如 JDK17 的 major_version=61)无法在低版本 JVM(如 JDK8)执行,体现 “向前兼容”。

2. 字节码指令

  • 指令结构:1 字节操作码(OpCode) + 0~N 字节操作数(Operand),JVM 指令集总数≤256 条。
  • 核心指令示例
    • bipush 10:将常量 10 压入操作数栈(操作码 bipush,操作数 10);
    • astore_1:将操作数栈顶的值存入局部变量表索引 1 的位置(仅操作码);
    • invokestatic:调用静态方法(如 Integer.valueOf ())。
  • 执行逻辑:从程序计数器读取指令地址→获取操作码→读取操作数(若有)→执行操作,循环至字节码流结束。

3. 字节码解读案例(Integer 缓存问题)

  • 代码现象

    Integer i1 = 10; Integer i2 = 10; System.out.println(i1 == i2); // true
    Integer i3 = 128; Integer i4 = 128; System.out.println(i3 == i4); // false
    
  • 字节码分析Integer i1 = 10对应指令bipush 10invokestatic #2 <Integer.valueOf>astore_1,核心是调用Integer.valueOf()

  • 底层原因Integer.valueOf()[-128, 127] 范围内的数值做缓存,直接返回缓存对象(地址相同),超出范围则 new 新对象(地址不同)。

4. try-catch-finally 执行流程

  • 控制核心:字节码中的异常表,每一行记录 “起始 PC→结束 PC→跳转 PC→捕获异常类型”,定义异常分支逻辑;
  • 执行规则
    • try 块出现Exception或其子类异常:跳转至 catch 块处理;
    • try/catch 块出现非Exception异常:跳转至 finally 块处理;
  • finally 特性:无论 try/catch 是否抛出异常,finally 代码都会通过 “插入字节码” 的方式执行(如将 finally 代码复制到 try 结束、catch 结束、异常抛出前)。

四、类加载

1. JDK8 的类加载体系

  • 三大核心特性:
    1. 缓存机制:每个类加载器对已加载的类保持缓存,避免重复加载;
    2. 双亲委派机制:核心加载逻辑,优先委托父加载器查找类;
    3. 沙箱保护机制:防止恶意类覆盖 JDK 核心类。

2. 双亲委派机制

  • 核心逻辑:“向上委托查找,向下委托加载”,通过ClassLoader.loadClass()方法实现:
    1. 先检查缓存,若已加载则直接返回;
    2. 若未加载,委托父加载器(如 AppClassLoader→ExtClassLoader→BootstrapClassLoader)加载;
    3. 父加载器均无法加载时,自身调用findClass()加载。
  • 打破场景:Tomcat 需加载 webapps 下多个应用的不同版本类,需自定义类加载器覆盖loadClass(),打破双亲委派。

3. 沙箱保护机制

  • 实现方法ClassLoader.preDefineClass()方法,若类名以 “java.” 开头,直接抛出SecurityException,禁止加载,防止核心类(如 java.lang.String)被篡改。

4. 类与对象的关系

  • 存储位置
    • 类(Class):存于元空间(MetaSpace,JDK8 替代永久代 PermSpace),存储类元数据、版本、注解、依赖关系等;
    • 对象:存于堆内存,是类的实例化结果。
  • 关联方式:堆中每个对象的对象头含 “类指针(classpoint)”,指向元空间中对应的类,通过getClass()可获取该类。
  • 元空间配置:通过-XX:MetaspaceSize(初始大小)和-XX:MaxMetaspaceSize(最大大小)配置,JVM 默认动态分配,且支持 GC(仅回收自定义类加载器加载的类)。

五、执行引擎

1. 执行方式:解释执行 vs 编译执行

执行方式 原理 特点 应用场景
解释执行 逐行翻译字节码为机器码 启动快,执行效率低 程序启动初期、低频代码
编译执行 JIT(即时编译),将热点代码编译为机器码存于 CodeCache 启动慢,执行效率高 高频热点代码(如循环)
  • HotSpot 默认模式混合模式mixed mode),自动判断代码执行频率,选择最优方式;可通过-Xint(纯解释)、-Xcomp(纯编译)强制指定。

2. 编译优化与编译器

  • 编译器类型
    • C1 编译器(客户端编译器):简单优化,编译快,启动快,占内存小,适用于桌面应用;
    • C2 编译器(服务端编译器):激进优化,编译慢,执行效率高,占内存大,JDK8 默认,适用于服务器应用;
    • Graal 编译器(JDK10+):Java 编写,支持 AOT(提前编译),目标替代 C2,衍生 GraalVM(直接编译为本地可执行文件)。
  • 分层编译:JDK8 引入,分 0~4 层,平衡启动速度与执行效率:
    • 0 层:纯解释,无监控;
    • 1~3 层:C1 编译,逐步开启监控(如方法调用次数、分支跳转);
    • 4 层:C2 编译,基于监控信息做激进优化。

3. 静态执行 vs 动态执行

  • 静态执行:编译期确定调用方法(如静态方法、私有方法);
  • 动态执行:运行期确定调用方法(如重载方法、接口方法),依赖invokedynamic指令(JDK7 引入,为 Lambda 表达式铺垫)。

六、GC 垃圾回收

1. 垃圾回收器的核心作用

  • 回收 JVM 内存中 “无用对象”,避免内存泄漏与 OOM(OutOfMemoryError),主要回收堆内存(对象存储区)和元空间(部分类信息)。
  • 核心工具:阿里开源的Arthas(官网:https://arthas.aliyun.com/),通过dashboard指令查看 JVM 内存使用与 GC 情况。

2. 分代收集模型(JDK8 主流)

  • 内存划分

    内存区域 细分区域 比例(默认) 特点 回收频率
    年轻代(YoungGen) eden + survivor0 + survivor1 8:1:1 存放 “朝生夕死” 对象(占 80% 对象) 频繁(YoungGC/MinorGC)
    老年代(OldGen) 独立区域 年轻代:老年代 = 1:2 存放长期存活对象(分代年龄≥16) 低(OldGC/FullGC)
  • 对象晋升流程

    1. 对象优先在 eden 区创建;
    2. 经历 1 次 YoungGC 后存活,进入 survivor 区,分代年龄 + 1;
    3. 每次 YoungGC 在 survivor 区之间转移,年龄累计;
    4. 年龄≥16(可通过-XX:MaxTenuringThreshold调整),晋升老年代;
    5. 大对象(eden 区放不下)直接进入老年代;
    6. TLAB(线程本地分配缓冲区):eden 区中线程专属区域,避免线程竞争,小对象优先在 TLAB 创建。

3. JVM 垃圾回收器分类

分类 回收器名称 特点 适用场景 JDK8 默认组合
分代回收器 Serial 单线程,STW(Stop The World)长 单 CPU,小型应用 -
ParNew 多线程,C1 配合,STW 短 多 CPU,配合 CMS -
Parallel Scavenge 多线程,关注吞吐量,C2 配合 多 CPU,服务器应用 年轻代默认
SerialOld 单线程,Serial 的老年代版本 单 CPU,小型应用 -
Parallel Old 多线程,Parallel Scavenge 的老年代版本 多 CPU,关注吞吐量 老年代默认
CMS(Concurrent Mark Sweep) 并发回收,STW 短,关注响应时间 多 CPU,高并发服务(如电商) -
不分代回收器 G1(Garbage-First) 区域化回收,兼顾吞吐量与响应时间 大内存(如 10G+),JDK9 默认 -
ZGC 超低延迟(<10ms),大内存支持 超大内存(如 TB 级),高并发 -
Shenandoah OpenJDK 版 ZGC,并发回收 与 ZGC 竞争,开源生态 -
Epsilon 无回收,仅测试用 性能测试,验证内存泄漏 -

七、GC 情况分析实例

1. 定制 GC 运行参数

JVM 参数分三类:

参数类型 前缀 示例 说明
标准参数 - java -version-classpath 所有 HotSpot 版本支持,java -help可查
非标准参数 -X -Xms200M(初始堆)、-Xmx200M(最大堆) 特定 HotSpot 版本支持,java -X可查
不稳定参数 -XX -XX:+PrintGCDetails-XX:SurvivorRatio=8 版本依赖,可能变更,java -XX:+PrintFlagsFinal可查所有生效参数
  • 生产建议-Xms-Xmx设为相同值,避免内存扩展的性能消耗。

2. 打印 GC 日志

核心日志参数:

参数 作用
-XX:+PrintGC 打印基础 GC 信息(同-verbose:gc
-XX:+PrintGCDetails 打印详细 GC 信息(含各区域内存变化)
-XX:+PrintGCTimeStamps 打印 GC 时间戳(相对于 JVM 启动时间)
-XX:+PrintHeapAtGC 打印 GC 前后堆内存快照
-Xloggc:./gc.log 将 GC 日志输出到指定文件(如./gc.log)
  • 示例:执行GcLogTest时添加参数-Xms60m -Xmx60m -XX:SurvivorRatio=8 -XX:+PrintGCDetails,控制台会输出 YoungGC 与 FullGC 的内存变化、耗时等信息。

3. GC 日志分析

  • 核心工具:开源网站gceasy.io(收费但有免费额度),上传 GC 日志文件后,自动分析:
    • 问题诊断(如 FullGC 频繁、内存泄漏);
    • 参数建议(如调整堆大小、Survivor 比例);
    • 详细指标(如 GC 暂停时间、内存使用率峰值)。

学习 JVM 是理解 Java 底层、解决线上问题、实现调优的核心,需重点掌握 Class 文件结构、类加载机制、执行引擎优化、GC 分代模型与日志分析,逐步构建完整的 JVM 知识体系。

关键问题

问题 1:为什么说 “学习 JVM 是区分一流与二流 Java 程序员的分水岭”?具体体现在哪些实际场景中?

答案

这一说法的核心是 “JVM 决定了程序员能否自主解决底层问题,而非仅依赖框架做 CRUD”,具体场景体现在三方面:

  1. 面试与知识深度:二流程序员依赖死记 “Integer 缓存、静态方法能否重写” 等八股题,一流程序员能通过字节码指令(如invokestatic调用Integer.valueOf())、类加载机制(如invokestaticinvokevirtual的区别)解释底层原因;
  2. 代码可靠性:二流程序员无法判断代码在 JVM 中的执行风险(如k = k++的结果),一流程序员能通过操作数栈与局部变量表的交互逻辑(iload_1iincastore_1)明确执行结果,避免潜在 BUG;
  3. 线上问题解决:面对 “服务频繁 FullGC”“OOM 异常”,二流程序员无法定位原因,一流程序员能通过定制 GC 参数(如-XX:+PrintGCDetails)、分析日志(用 gceasy.io)、结合分代模型(如年轻代比例不合理)给出调优方案(如调整-Xms/-Xmx-XX:SurvivorRatio)。

问题 2:JDK8 的双亲委派机制是什么?其核心作用是什么?为什么 Tomcat 需要打破双亲委派机制?

答案

  1. 双亲委派机制定义:JDK8 中类加载器(AppClassLoader→ExtClassLoader→BootstrapClassLoader)遵循 “向上委托查找,向下委托加载” 的逻辑,通过ClassLoader.loadClass()实现:先检查自身缓存→委托父加载器查找→父加载器均无法加载时,自身调用findClass()加载;
  2. 核心作用沙箱保护,防止恶意类覆盖 JVM 核心类(如自定义java.lang.String)—— 由于 BootstrapClassLoader 优先加载 JDK 自带的java.lang.String,自定义类会被父加载器拦截,无法加载;
  3. Tomcat 打破双亲委派的原因:Tomcat 需部署多个 Web 应用(如 A 应用用 Spring 5,B 应用用 Spring 6),若遵循双亲委派,ExtClassLoader/AppClassLoader 会优先加载某个版本的 Spring 类,导致其他应用类冲突;因此 Tomcat 自定义WebAppClassLoader,覆盖loadClass(),优先加载当前应用WEB-INF/classes下的类,再委托父加载器,避免跨应用类版本冲突。

问题 3:基于 JVM 字节码指令,解释为什么Integer i1=10; Integer i2=10; i1==i2结果为true,而i3=128; i4=128; i3==i4结果为false

答案

该现象的核心是Integer的缓存机制,需通过字节码指令与Integer.valueOf()方法逻辑结合解释:

  1. 字节码指令分析Integer i1=10对应的字节码指令为bipush 10invokestatic #2 <java/lang/Integer.valueOf:(I)Ljava/lang/Integer;>astore_1,可见变量赋值时并非直接创建对象,而是调用Integer.valueOf()静态方法;

  2. Integer.valueOf()逻辑:该方法对 [-128, 127] 范围内的 int 值做缓存,逻辑如下:

    public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high) // low=-128, high=127return IntegerCache.cache[i + (-IntegerCache.low)]; // 返回缓存对象return new Integer(i); // 超出范围,新建对象
    }
    
  3. 结果差异原因

    • i=10(在 [-128,127] 内):valueOf()返回同一缓存对象,i1i2指向同一内存地址,==比较地址时结果为true
    • i=128(超出范围):valueOf()每次新建Integer对象,i3i4指向不同内存地址,==比较地址时结果为false
http://www.hskmm.com/?act=detail&tid=13553

相关文章:

  • 东方通中间件嵌入式监控脚本
  • 004_元组操作
  • 个人作业-第二次软件工程作业
  • 代码流水线
  • 洛谷题单指南-进阶数论-P1516 青蛙的约会
  • electron中的几个概念
  • 实用指南:告别IP被封!分布式爬虫的“隐身”与“分身”术
  • 从 “盲调” 到 “精准优化”:SQL Server 表统计信息实战指南
  • 别的摄像机都能国标GB28181注册上,就这台海康摄像机注册不上来,国标配置都反复检查没问题
  • 保护眼睛小程序
  • CSP-2025游寄
  • [::-1]的用法
  • 003_for循环操作列表和元组
  • linux 文件传输命令
  • 新手也能轻松上手!midas Gen 2019 安装详细图解
  • Redis AOF原理
  • 001_string操作
  • hbase 面试题
  • ANSYS Electronics 2025 R1 安装与使用全流程图文教程
  • mall项目学习笔记
  • 实用指南:通义DeepResearch论文六连发全面解读
  • glTF/glb:现在和未来
  • 存储多边形网格的文件格式:OBJ、FBX、RenderMan、glTF、USD 等。
  • 安防监控中常见的报警类型有哪些?国标GB28181平台EasyGBS的报警能力解析
  • Notepad++8.6免费版下载及安装教程(附安装包)2025最新整理
  • VTable-Sheet:重新定义Web电子表格的开源解决方案
  • Coolmuster Android Assistant:Windows架构下的Android设备管理专家
  • 负载均衡+Tomcat集群+MySQL主从 实验
  • mysql表新增字段,基本语法
  • 2025年运营商数据分类分级最佳实践、案例与方案