不必死记硬背,理解之后,每次写代码的时候就去思考该类在 JVM 的运行过程
简介
JVM 里的搬运工和检察官,把 .class 文件加载到机器内存中,并在内存中构建 Java 类的原型—类模版对象!
类加载过程
第一阶段:加载
读取 .class 文件,将其转化为某种静态数据结构存储在方法区,并在堆内存中生成一个 java.lang.Class 对象。
第二阶段:连接(验证、准备、解析)
-
验证:主要验证第一阶段加载在内存中的 class 静态结构,确保其符合 JVM 规范,保证这些代码运行后不会危害虚拟机。
1)文件格式验证:魔数是否以 0xCAFEBABY 开头、主次版本号、编码是否符合等等,在上一步加载阶段其实已经完成
2)元数据验证:是否有父类、是否覆盖了 final 、是否实现了父类接口中所有方法等等
3)字节码验证:确定程序语意是否合法、主要验证 class 文件中的 code 属性(存储了编译后的字节码指令)
4)符号引用验证:对类自身以外的引用属性进行匹配性验证,这个验证过程其实是在解析阶段完成的
5)-Xverifynone:关闭大部分类的验证 -
准备:为类的静态变量分配内存并设置默认值
注意:类变量(静态变量)会分配在方法区的运行时常量池中
-
解析:符号引用转换为直接引用(对引用的类、接口、方法、字段进行处理)
符号引用:字面上的表现形式,看到这个符号就知道代表哪个东西,此时被引用的目标不一定已经被加载到虚拟机内存了
直接引用:可以理解为在内存的地址(指向目标的指针、相对偏移量、间接定位的句柄),此时被引用的目标一定已经被加载到虚拟机内存了这个阶段可以是在初始化环节之后进行,实现所谓的“后期绑定”
第三阶段:初始化
将类变量(静态变量、实例变量)初始化为指定的值(如果指定的话),并执行静态代码块,主动权由 JVM 转移到应用程序,触发初始化有以下场景:
new的时候、设置或读取一个静态字段、调用一个静态方法
进行反射调用
JDK8 新引用了由 default 修饰的接口方法,如果该接口的实现类发生了初始化,那么该接口要先一步初始化
第四阶段:使用
由 JVM 动态调用执行
第五阶段:卸载
当一个 class 对象不再被引用,那么它的生命周期也将结束,对应方法区的数据也会被卸载
JVM 自带的 ClassLoader 装载的类是不会出现卸载的,只有我们自定义的 ClassLoader 装载的类才可以卸载
命令
// 把一个 .class 文件转换成可读的 txt 文件
javap -v Test.class > Test.txt
// 反编译
javap -c Test.class
思考
javac 编译过程中已经进行了一次校验怎么在 jvm 还要再进行一次
1)防止出现 .class 文件本身就是非法的,采用了不规范的编译器,或者恶意修改了 .class 文件
2)防止加载 .class 文件过程中发生数据丢失
3)版本检查,jdk8 编译的 .class 文件不能在 jdk8 以下的版本运行
关于方法区的实现
方法区是 JVM 规范中的抽象概念,永久代和元空间均是其在不同版本的实现方式,概念不要混淆了。
JDK6 以前:使用永久代实现方法区,用来存储:类的元信息(类型信息、域信息、方法信息)、JIT代码缓存、静态变量、字符串常量池
JDK7 版本:使用永久代实现方法区,静态变量、字符串常量池移到堆内存中
JDK8 以后:使用元空间实现方法区
解析阶段的一些思考
A.class 中引用了 B.class,此时 B.class 并未加载到内存中,在 A 中用符号 Y 来代表 B,那么 Y 就是 B 在 A 的符号引用
静态解析:当加载 A 的时候发现 B 没有被加载,那么就会自动触发 B 的加载,对应的 Y 也会被替换为 B 在内存中的实际地址,这个实际地址就是直接引用
动态解析:对于 Java 开发来说多态是很重要的特性,如果 B 作为接口有多个实现类,当加载 A 的时候,无法确定加载 B 的哪一个实现类,只有当运行过程中发生了调用的时候,这时虚拟机调用栈中会有具体的信息来确定了要用哪个实现类,这时才会把 Y 替换成 B 的实现类的直接引用
静态变量、实例变量、常量、静态常量池、运行时常量池、字符串常量池
- 静态变量:由 static 修饰的类变量,被该类的所有实例对象共享
- 实例变量:非 static 修饰的类变量,只被当前实例对象拥有
- 常量:其实就是 final 修饰得变量,可用来修饰静态变量、实例变量、局部变量
- 静态常量池:可理解为 Class 文件的常量池,包含了字面量(声明为 final 的变量)和符号引用量,编译期产生
- 运行时常量池:把静态常量池加载到内存中形成的,经常说的常量池其实就是运行时常量池,相比于静态常量池多了些动态性,在运行期间也可以将常量放在常量池中,每个类在方法区都会有一个运行时常量池
- 字符串常量池:字符串对象在堆内存创建后,会把引用存放到字符串常量池中,是由 StringTable 实现的,是一个哈希表。
声明为 final 的常量一定会在编译期投放在 Class 文件的常量池吗
不一定,取决于这个值能不能在编译期就确定下,钉是钉铆是铆,整个随机数肯定不能被当作常量。
对一个类进行调用的时候,一定会触发这个类的类加载吗
不一定,如果是调用了这个类中已经确定的常量(投放在 Class 文件常量池的),这时就不需要对该类进行类加载
代码悟道
public class Test {
public static void main(String[] args) {
String a = "hello";
String b = "hello";
String c = new String("hello");
String d = new String("hello");
// a b c d 对应的地址值差异思考
System.err.println(String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(a)));
System.err.println(String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(b)));
System.err.println(String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(c)));
System.err.println(String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(d)));
}
}
public class Test {
// 会怎样输出呢
public static void main(String[] args) {
System.out.println(Constant.A);
System.out.println(Constant.B);
}
}
class Constant {
public static String A = "A";
public static final String B = "B";
static {
System.out.println("load this static code block");
}
}