这个相隔有点远了 本节整理的内容为类文件和类字节码还有类加载
这章内容较前面的垃圾回收并不困难理解

这次就是探讨JVM如何编译我们写的代码的

类文件和类字节码

JVM编译后的java代码字节码 OS无法识别高级语言 需要编译成字节码文件
然后放到各个平台的虚拟机读取执行 实现一次编写到处运行 .class文件 其为二进制流文件

例如我们看到的class文件实际上是这种的

Screenshot 2026-01-14 150259

接下来我们就研究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文件
image

然后编译文件:

 javap -v Test_class_hello.class
-v 即verbose 详细信息

你会得到如下 我们重点就是看的这个来搞懂过程
image

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

类加载

要做的事情就三件

image

当然还有运行啥的好理解 然后我们分析每个阶段

加载
就干三件事情:
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 为的是使用我们自定义的类加载器进行加载类

参考:https://pdai.tech/md/java/jvm/java-jvm-classload.html

posted on 2026-01-20 20:55  蒸饺  阅读(0)  评论(0)    收藏  举报