这个相隔有点远了 本节整理的内容为类文件和类字节码还有类加载
这章内容较前面的垃圾回收并不困难理解
这次就是探讨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: 0
major version: 61 52为jdk8 此处+9 即jdk17
flags: (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_1
6: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
9: iconst_3 将3压入栈
10: istore_1 从栈弹出存入局部槽1 x=3
11: goto 34 去return
14: astore_2 把e从栈顶弹出存入局部槽2
15: iconst_2 将2压入栈
16: istore_1 从栈弹出存入局部槽1 x=3
17: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
20: iload_1
21: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
24: iconst_3 此处又是finally
25: istore_1
26: goto 34
29: astore_3 代码出错的异常槽3
30: iconst_3
31: istore_1
32: aload_3
33: athrow
34: return
Exception table:
from to target type
0 9 14 Class java/lang/Exception try从0-9出叉劈了去到14即catch语句
0 9 29 any 其他任何异常都进入29 finally
14 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: I
flags: (0x0008) ACC_STATIC
static java.lang.String b;
descriptor: Ljava/lang/String;
flags: (0x0008) ACC_STATIC
static final int c;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
ConstantValue: int 10
static final java.lang.Integer d;
descriptor: Ljava/lang/Integer;
flags: (0x0018) ACC_STATIC, ACC_FINAL
在准备阶段final修饰的c已经赋值
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_3
1: putstatic #7 // Field a:I
4: ldc #13 // String 楂樿繙
6: putstatic #15 // Field b:Ljava/lang/String;
9: bipush 20
11: 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 为的是使用我们自定义的类加载器进行加载类




浙公网安备 33010602011771号