快速了解Java虚拟机基础入门
JDK、JRE、JVM的关联

⚙
- 解释器:逐行把字节码转换成机器码
- 即时编译器(JIT):将经常执行的代码段(热点代码)编译成高效的本地机器码,并缓存起来供后续直接执行 J ust-I n-T ime Compiler
💡
从范围来看,JDK包含JRE和开发工具,JRE包含JVM和类库,即JDK > JRE > JVM:
- JDK = JRE + 开发工具
- JRE = JVM + 类库
jar包→java字节码→机器码
📖
我们使用JDK调用Java API来开发Java程序,将其编译成字节码或打包程序。然后可以用JRE启动一个JVM实例,JVM会加载、验证、执行Java字节码及依赖库来运行Java程序。JVM会把程序和依赖库的Java字节码解析成本地代码(机器码)来执行
解释执行与即时编译
📖
- 解释执行:
- 在解释执行阶段,Java虚拟机直接读取并运行Java字节码(.class文件中的内容),逐条解释每条字节码指令并执行。
- 解释执行的优点是启动速度快,因为不需要等待编译过程。
- 解释执行的缺点是执行速度较慢,因为每条指令都要解释。
- 即时编译(JIT):
- 当Java虚拟机检测到某些代码被频繁执行(热点代码)时,会启动即时编译器,把这些热点代码编译成本地机器码。
- 编译后的机器码能直接在硬件上运行,执行速度更快。
- JIT编译是动态的,编译后的代码会存储在内存中供后续使用。
字节码与机器码的转换流程
- 初始运行:程序启动时,Java虚拟机会先通过解释执行的方式运行字节码。
- 热点检测:运行过程中,Java虚机会监控代码的执行频率。
- JIT编译:检测到热点代码时,Java虚机会启动JIT编译器,将这些代码编译成机器码。
- 执行切换:代码编译成机器码后,后续执行会直接用机器码,不再解释执行字节码
性能优化
📌
80⁄20原则:前20%的瓶颈问题,至少会对性能影响占80%比重
💡
三个维度:
* 延迟(95线、99线 - Latency)
* 吞吐量(TPS、QPS - Throughput)
* 系统容量(-硬件配置 - Capacity)
跨平台特性
-
解释型语言 - 以python为例(不过python很多方面已优化)
运行时一直依赖解释器,每次启动都重新解释,错误在不运行时不报错“’. . . ‘”
📌
源码跨平台 -cpp
“一次编写,到处调试”,一份源码在不同平台编译,会产生很多环境配置问题,开发维护成本高,但效率高
📌
二进制跨平台 -java字节码
真正的“一次编写,到处(不同平台)编译”,编译生成jar包(通用java字节码),部署到不同平台,JVM通过jar包将字节码加载到目标机器,效率不如本地编译
Runtime相关
- JVM(Java Virtual Machine):负责执行字节码的核心引擎。
- JRE(Java Runtime Environment):提供运行时所需的完整环境,包括JVM和标准类库。
- 运行时数据与状态:如内存分配、线程状态、异常处理等,在程序运行时动态管理。
所以,“runtime”是抽象概念,JVM是实现该概念的具体技术实体
编程语言简评
📖
- C/C++完全信任程序员,让程序员自行管理内存,能编写自由代码,但一不小心会造成内存泄漏等问题导致程序崩溃。
- Java/Golang不完全信任程序员但也适度包容,所有内存生命周期由JVM运行时统一管理,大部分场景下可自由写代码,不用关心内存情况,内存使用有问题时可通过JVM分析诊断和调整。
- Rust语言既不信任也不纵容程序员,要求写代码时必须按Rust规则管理变量,让机器能高效分析管理内存,但代码不利于人理解,编写不自由,学习成本高
Java基本类型
💡
java基本类型不属于object类,int类传入object类时需转为Integer类的封箱,java内部缓存了int[-128,127]对应的封箱。除long和double外,其他基本类型与引用类型在解释执行的方法栈帧中占用大小一致,但在堆中占用大小不同。将Boolean、byte、char及short的值存入字段或数组单元时,Java虚机会进行掩码操作,读取时会扩展为int类型
Java编译器
自动装箱与自动拆箱
基本类型→对象(int→Integer)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可通过调节java.lang.Integer.IntegerCache.high调高缓存int的范围,但没有java.___.low
泛型与类型擦除
💡
对于泛型方法,传出参数类型大概率是(Object.class),代表类型擦除
📖
不是所有泛型参数擦除后都变成Object类。对于限定了继承类(extend A)的泛型参数,擦除后所有泛型参数变成所限定的继承类(class A),即Java编译器会选取泛型能指代的所有类中层次最高的作为替换泛型的类
桥接方法
由于泛型擦除,父方法传入参数改变(Object t)不符合重写规则,引入桥接方法
class Parent<T> {
public void method(T t) {
System.out.println("Parent");
}
}
class Child extends Parent<String> {
@Override
public void method(String s) {
System.out.println("Child");
}
}
泛型擦除后的父方法
public void method(Object t) {
System.out.println("Parent");
}
java编译器生成的桥接方法
public void method(Object t) {
method((String) t);
}
public void method(String s) {
System.out.println("Child");
}
Java对象的内存布局
// Foo foo = new Foo(); 编译而成的字节码
0 new Foo
3 dup
4 invokespecial Foo()
7 astore_1
// Foo 类构造器会调用其父类 Object 的构造器
public Foo();
0 aload_0 [this]
1 invokespecial java.lang.Object() [8]
4 return
💡
总之,调用构造器时会优先调用父类构造器直至Object类,这些构造器调用的是同一对象(new指令新建的对象)。new指令新建的对象内存涵盖所有父类中的实例字段,虽子类无法访问父类私有实例字段或子类字段隐藏父类同名字段,但子类实例仍会为父类实例字段分配内存
压缩指针
💯
其实就是相对寻址,只存储偏移量,减小存储压力。偏移量固定单元大小为8b,为什么是8?long类型占8b,double/float、int也分别占相应字节,即使浪费一点内存换高速缓存也划算
💡
object header(16B)→(12B)
* 标记字段(64b):存储Java虚拟机有关对象的运行数据,如哈希码、GC信息及锁信息
* 类型指针(64b)→(32b)压缩指针:指向对象的类
⚙
- 指针压缩:32位指针本身只能直接寻址2^32字节(4GB),通过转换机制扩展寻址范围
- 地址转换:Java虚拟机用“地址压缩和解压缩”技术,32位指针视为偏移量,乘以固定值(通常8bit)后加基地址得实际64位内存地址
- 寻址范围:32位指针最大偏移量2^32−1,乘以8后最大地址为2^32×8=235字节(32GB)
📖
字段重排列是Java虚拟机重新分配字段顺序以内存对齐,有三种排列方法(对应Java虚拟机选项-XX:FieldsAllocationStyle,默认值1),遵循两个规则:其一,字段占据C字节时,偏移量需对齐至NC;其二,子类继承字段偏移量与父类对应字段偏移量一致。具体实现中,使用压缩指针的64位虚拟机,子类第一个字段对齐至4N;关闭压缩指针的64位虚拟机,子类第一个字段对齐至8N。以long类型为例,使用压缩指针的64位虚拟机中,对象头12字节,long类型字段偏移量为16,中间4字节浪费
JAVA字节码
感觉和机器码类似,不做过多叙述,偷懒了哈哈🙌
📖
java字节码编译
* C1:又称Client编译器,面向启动性能要求高的客户端GUI程序,优化手段简单,编译时间短
* C2:又称Server编译器,面向峰值性能要求高的服务器端程序,优化手段复杂,编译时间长但生成代码执行效率高
* Graal(java10+)
从Java7开始,HotSpot默认分层编译:热点方法先由C1编译,C1内分三层(无数据支持优化、有一定数据支持优化、全数据支持优化),热点方法中的热点再由C2编译。为不干扰应用运行,HotSpot即时编译在额外编译线程中进行,按CPU数量设置编译线程数,C1与C2编译器按1:2配置。资源充足时,字节码解释执行和即时编译可同时进行,编译后的机器码下次调用时启用替换解释执行
02正在路上了!!!