更新时间:2022-05-10 00:46:56
参考资料:
《深入理解 Java 虚拟机 - JVM 高级特性与***实践》
第1部分主题为自动内存管理,以此延伸出 Java 内存区域与内存溢出、垃圾收集器与内存分配策略、参数配置与性能调优等相关内容;
第2部分主题为虚拟机执行子系统,以此延伸出 class 类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎等相关内容;
第3部分主题为程序编译与代码优化,以此延伸出程序前后端编译优化、前端易用性优化、后端性能优化等相关内容;
第4部分主题为高效并发,以此延伸出 Java 内存模型、线程与协程、线程安全与锁优化等相关内容;
本系列学习笔记可看做《深入理解 Java 虚拟机 - JVM 高级特性与***实践》书籍的缩减版与总结版,想要了解细节请见纸质版书籍;
8位bit(1字节)为基础单位
的二进制流,各个数据项目严格按照顺序紧凑的排列在 class 文件之中,中间没有任何分隔符。当遇到需要占用 1 字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 1 字节进行存储;包含两种数据类型:
u2+ 常量池:常量池容量计数器用来记录常量个数。常量池中主要存放两大类常量:
new
、getstatic
、putstatic
或 invokestatic
这 4 条字节码指令时没初始化触发初始化;(即:new 关键字实例化对象、读取一个类的 finel 静态字段、调用一个类的静态方法);java.lang.reflect
包的方法对类进行反射调用;java.lang.invoke.MethodHandle
实例最后的解析结果 REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化;1. 加载:将类的 class 文件读入到内存。通过一个类的全限定名来获取定义次类的二进制流。将这个字节流所代表的静态存储结构转换成方法区中的运行时数据结构。在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区类数据的访问入口(反射接口)。这个过程需要类加载器参与;
数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的:
连接:负责把类的二进制数据合并到 JRE 中(将 Java 类的二进制代码合并到 JVM 的运行状态之中);
2. 验证:确保加载的类信息符合 JVM 规范,没有安全方面的问题。验证是否符合 Class 文件格式规范,并且是否能被当前的虚拟机加载处理;
4. 解析:(这里是静态解析)虚拟机常量池的符号引用替换为直接引用过程;
<clinit>()
方法的过程。为类的变量赋予正确的初始值。类构造器 <clinit>()
方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。如果发现父类还没有进行过初始化,则需要先触发其父类的初始化。虚拟机保证一个类的 <clinit>()
方法在多线程环境中被正确加锁和同步;概述:
几种类加载器:
lib
下或被 -Xbootclasspath
路径下的类。C++ 实现。不允许直接通过引用启动类加载器进行操作。<JAVA_HOME>/lib/ext
目录下或者由系统变量 -Djava.ext.dir
指定位路径中的类库。开发者可以直接使用标准扩展类加载器;-classpath
选项、java.class.path
系统属性,或者 CLASSPATH
将变量所指定的 JAR 包和类路径。程序可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由 Java 语言实现,父类加载器为 ExtClassLoader;类加载器间的关系:
类加载器的执行步骤:
工作原理:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载;
“栈帧”的概念在《JVM | 第1部分:自动内存管理与性能调优》提到,这里不再赘述;
解析:所有方法调用的目标方法在 Class 文件里都是一个常量池中的符号引用。有两种解析:
类加载的解析阶段
会被转化为直接引用(即:静态方法、final 修饰的方法、私有方法、父类方法、\<init>方法,统称非虚方法
);运行期
被解析为直接引用;分派:用来确定虚方法的目标方法。体现 Java 面向对象的继承、封装和多态 3 大特性。有如下 4 种:
静态分派:典型应用是处理方法重载
。重载的方法在经过编译期
编译后得到相同的方法调用字节码指令和指令参数。虚拟机在处理重载时是通过参数的静态类型。方法参数的允许发送类型转变,但方法接收者本身静态类型不变;
B b = new A();
其中 B 称为 b 变量的静态类型
(Static Type,编译器可知),A 称为 b 变量的实际类型
(Actual Type,运行期可知);选择静态分派目标的过程(重载的本质)。例如:尝试调用方法 say('a')
:
say(char arg)
;say(int arg)
;say(long arg)
;say(Character arg)
;say(Serializable)
;say(Object arg)
;say(char... arg)
;动态分派:典型应用是方法重写
。Java 虚拟机在运行期
会依据 invokevirtual
指令的多态查找过程,通过实际类型来分派方法执行版本的。过程如下:
接收者
和方法的参数
统称为方法的宗量
。 根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种;解析阶段
就进行直接引用的转化,而静态方法也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派
完成的;Java 语言的 静态多分派、动态单分派 示例:
public class Main {
static class A {
}
static class B extends A {
}
static class C extends B {
}
public void say(A a) {
System.out.println("A");
}
public void say(B b) {
System.out.println("B");
}
public void say(C c) {
System.out.println("C");
}
public static void main(String[] args) throws Exception {
Main main = new Main();
Main superMain = new Super();
B os = new C();
main.say(os);
superMain.say((A) os);
//输出 B S-A
}
}
class Super extends Main {
public void say(A a) {
System.out.println("S-A");
}
public void say(B b) {
System.out.println("S-B");
}
public void say(C c) {
System.out.println("S-C");
}
}
编译期看静态分派 - 多分派:
运行期看动态分派 - 单分派: