这个相隔有点远了 本节整理的内容为类文件和类字节码还有类加载
这章内容较前面的垃圾回收并不困难理解
这次就是探讨JVM如何编译我们写的代码的
类文件和类字节码
JVM编译后的java代码字节码 OS无法识别高级语言 需要编译成字节码文件
然后放到各个平台的虚拟机读取执行 实现一次编写到处运行 .class文件 其为二进制流文件
例如我们看到的class文件实际上是这种的
接下来我们就研究class文件 JVM是如何编译它的实现
规范结构jvm编译的class文件:
ClassFile {u4 magic; 魔法数标识告知这是class文件u2 minor_version; Java次版本号u2 major_version; Java主版本号u2 constant_pool_count; 常量池中多少条项目cp_info constant_pool[constant_pool_count-1]; 存各种东西u2 access_flags; 类修饰符u2 this_class; 指向constant_pool内表示“类自身名字”的索引u2 super_class; 指向constant_pool内表示父类的索引u2 interfaces_count; 实现多少接口u2 interfaces[interfaces_count]; 接口索引数组u2 fields_count; 成员变量个数field_info fields[fields_count]; 字段描述u2 methods_count; 方法个数 method_info methods[methods_count]; 方法描述u2 attributes_count; 附加属性数量attribute_info attributes[attributes_count]; 属性
}
我们并未使用字节码 而是使用工具javap来可视化查看效果
用一个例子来看 就是一个打印hello
public class Test_class_hello {public static void main(String[] args) {System.out.println("hello");}
}
javac 名.java 生成class文件
然后编译文件:
javap -v Test_class_hello.class
-v 即verbose 详细信息
你会得到如下 我们重点就是看的这个来搞懂过程

minor version: 0major version: 61 52为jdk8 此处+9 即jdk17flags: (0x0021) ACC_PUBLIC, ACC_SUPER 类修饰符 两者相加即公共类this_class: #21 本类(Test_class_hello)super_class: #2 我的父类(java/lang/Object) interfaces: 0, fields: 0, methods: 2, attributes: 1 接口数 成员变量数 方法数 附加属性
SourceFile: "Test_class_hello.java" 主流属性 保存 Java 源文件名Constant pool: 常量池#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
后面的#2.#3 指向后面的2和3(#2 = Class和#3 = NameAndType) Methodref方法定义#2 = Class #4 // java/lang/Object#3 = NameAndType #5:#6 // "<init>":()V#4 = Utf8 java/lang/Object#5 = Utf8 <init>#6 = Utf8 ()V 此处代表void 无返回值#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream; sout#8 = Class #10 // java/lang/System#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;#10 = Utf8 java/lang/System#11 = Utf8 out#12 = Utf8 Ljava/io/PrintStream;#13 = String #14 // hello 字符串hello#14 = Utf8 hello#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V 打印输出#16 = Class #18 // java/io/PrintStream#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V#18 = Utf8 java/io/PrintStream#19 = Utf8 println#20 = Utf8 (Ljava/lang/String;)V#21 = Class #22 // Test_class_hello#22 = Utf8 Test_class_hello#23 = Utf8 Code#24 = Utf8 LineNumberTable#25 = Utf8 main#26 = Utf8 ([Ljava/lang/String;)V#27 = Utf8 SourceFile#28 = Utf8 Test_class_hello.java
我们再来分析一段程序try catch
public class TestCode_Zijie {public static void main(String[] args) {int x;try {x = 1;System.out.println(x);} catch (Exception e) {x = 2;System.out.println(x);} finally {x = 3;System.out.println(x);}}
}
然后编译看信息
Code:stack=2, locals=4, args_size=1
栈深2
局部变量槽数4(槽istore__0 args istore__1 x istore__2 e istore__3 throwable)
主函数参数1(String[] args)0: iconst_1 将1压入栈1: istore_1 从栈弹出存入局部槽1 x=1 2: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;5: iload_16: invokevirtual #13 // Method java/io/PrintStream.println:(I)V9: iconst_3 将3压入栈 10: istore_1 从栈弹出存入局部槽1 x=311: goto 34 去return14: astore_2 把e从栈顶弹出存入局部槽215: iconst_2 将2压入栈16: istore_1 从栈弹出存入局部槽1 x=317: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;20: iload_121: invokevirtual #13 // Method java/io/PrintStream.println:(I)V24: iconst_3 此处又是finally25: istore_126: goto 3429: astore_3 代码出错的异常槽330: iconst_331: istore_132: aload_333: athrow34: returnException table:from to target type0 9 14 Class java/lang/Exception try从0-9出叉劈了去到14即catch语句0 9 29 any 其他任何异常都进入29 finally14 24 29 any
类加载
要做的事情就三件
当然还有运行啥的好理解 然后我们分析每个阶段
加载
就干三件事情:
1.找字节码
2.读字节码
3.在方法区创建class对象数据结构
链接:
也分三个阶段:验证-准备-解析
验证:先确保被加载的类正确性
准备:为static分配内存(准备阶段完成) 复制(设置默认值)(初始化完成)
解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程 后面运行时不用再翻字典
初始化:
为static静态变量赋值
重点搭配之前看如下:
是否触发初始化时机:
- 静态和final修饰常量不会触发
- 关于数组不会触发
- new类的实例会触发
- 访问静态字段(变量)
- 初始化子类也会先初始化父类
- main方法也会初始化
用一个例子来分析:
public class Test_ClassLoad {static int a=3;static String b="高远";final static int c=10;final static Integer d=20;
}
先提前分析 final修饰的应该不会触发 所以在加载阶段就会完成赋值 而其他会初始化阶段完成赋值
然后javap看信息
类加载准备阶段 分配内存static int a;descriptor: Iflags: (0x0008) ACC_STATICstatic java.lang.String b;descriptor: Ljava/lang/String;flags: (0x0008) ACC_STATICstatic final int c;descriptor: Iflags: (0x0018) ACC_STATIC, ACC_FINALConstantValue: int 10static final java.lang.Integer d;descriptor: Ljava/lang/Integer;flags: (0x0018) ACC_STATIC, ACC_FINAL
在准备阶段final修饰的c已经赋值static {};descriptor: ()Vflags: (0x0008) ACC_STATICCode:stack=1, locals=0, args_size=00: iconst_31: putstatic #7 // Field a:I4: ldc #13 // String 楂樿繙6: putstatic #15 // Field b:Ljava/lang/String;9: bipush 2011: invokestatic #19 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;14: putstatic #25 // Field d:Ljava/lang/Integer;17: return
a b d触发初始化 在此处进行赋值
其中d会触发初始化 是因为Integer自动装箱机制 可能会发生new对象等操作从而导致初始化
然后补充知识:
类加载器(文件从哪里给你找来用的)
- 启动类加载器:加载核心类库 底层C++写的
- 扩展类加载器
- 应用程序类加载器
双亲委派加载机制:
子类加载器加载一个类的时候先不自己干 往上推让父类加载器干 再往上推 最终都不行了再自己干
直接看例子:public Class<?> loadClass(String name)throws ClassNotFoundException {return loadClass(name, false);}protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {Class c = findLoadedClass(name);if (c == null) {//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载try {if (parent != null) {//如果存在父类加载器,就委派给父类加载器加载 c = parent.loadClass(name, false);} else {//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)c = findBootstrapClass0(name);}} catch (ClassNotFoundException e) {// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
大致流程:c即应用程序类加载器
加载class的时候先往上找 c = parent.loadClass(name, false);找扩展类加载器
扩展类加载器加载类的时候不会直接开干 先查启动类加载器c=findBootstrapClass0(name)
若启动类加载器加载失败 扩展类加载器就开干
若扩展类加载器加载失败 应用程序类加载器干 c=findClass(name);
自定义类加载器
例如插件热加载非classpath的类文件等情况用到
继承ClassLoader类 重写findClass而非loadClass 为的是使用我们自定义的类加载器进行加载类
参考:https://pdai.tech/md/java/jvm/java-jvm-classload.html



