我们都知道,编写的java代码是不能直接运行的,需要编译成class文件才行,这个编译需要通过javac命令就可以实现。然而生成的class文件是一个二进制文件,是不方便我们阅读的,我们需要通过javap命令进行反编译。
比如,我下面一段代码
反编译后是这样的内容:
我们分析一下反编译之后的结果
可以看到 一个class文件包含了几部分
第一部分:是类的描述信息,他记录了类的存储位置,最后一次修改时间,md5值,以及从哪个java类里编译出来的
第二部分:还是一些描述信息,主要描述了这个类是用什么样的jdk编译的,52表示jdk8
第三部分:常量池
第四部分:字段信息
第五部分:方法信息
//1.描述信息 Classfile /Users/weichenglong/Desktop/work/jvmTest/src/main/java/com/wcl/JVMTest.class Last modified 2021-10-18; size 649 bytes MD5 checksum e062633c7095f59d343f5f311df1413c Compiled from "JVMTest.java" //2.描述信息 public class com.wcl.JVMTest minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER //3.常量池 Constant pool: #1 = Methodref #12.#28 // java/lang/Object."加载":()V #2 = Class #29 // java/lang/StringBuilder #3 = Methodref #2.#28 // java/lang/StringBuilder." ":()V #4 = Fieldref #7.#30 // com/wcl/JVMTest.staticField:Ljava/lang/String; #5 = Methodref #2.#31 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #6 = Fieldref #7.#32 // com/wcl/JVMTest.field:Ljava/lang/String; #7 = Class #33 // com/wcl/JVMTest #8 = String #34 // AAA #9 = Methodref #2.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String; #10 = Methodref #7.#28 // com/wcl/JVMTest." ":()V #11 = Methodref #7.#36 // com/wcl/JVMTest.add:()Ljava/lang/String; #12 = Class #37 // java/lang/Object #13 = Utf8 CONST_FIELD #14 = Utf8 Ljava/lang/String; #15 = Utf8 ConstantValue #16 = Utf8 staticField #17 = Utf8 field #18 = Utf8 #19 = Utf8 ()V #20 = Utf8 Code #21 = Utf8 LineNumberTable #22 = Utf8 add #23 = Utf8 ()Ljava/lang/String; #24 = Utf8 main #25 = Utf8 ([Ljava/lang/String;)V #26 = Utf8 SourceFile #27 = Utf8 JVMTest.java #28 = NameAndType #18:#19 // " ":()V #29 = Utf8 java/lang/StringBuilder #30 = NameAndType #16:#14 // staticField:Ljava/lang/String; #31 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #32 = NameAndType #17:#14 // field:Ljava/lang/String; #33 = Utf8 com/wcl/JVMTest #34 = Utf8 AAA #35 = NameAndType #40:#23 // toString:()Ljava/lang/String; #36 = NameAndType #22:#23 // add:()Ljava/lang/String; #37 = Utf8 java/lang/Object #38 = Utf8 append #39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #40 = Utf8 toString { //3.字段信息 private static final java.lang.String CONST_FIELD; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL ConstantValue: String AAA private static java.lang.String staticField; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC private java.lang.String field; descriptor: Ljava/lang/String; flags: ACC_PRIVATE //4.方法信息 public com.wcl.JVMTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: return LineNumberTable: line 3: 0 public java.lang.String add(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder." ":()V 7: getstatic #4 // Field staticField:Ljava/lang/String; 10: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 13: aload_0 14: getfield #6 // Field field:Ljava/lang/String; 17: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: ldc #8 // String AAA 22: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 25: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 28: areturn LineNumberTable: line 10: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: new #7 // class com/wcl/JVMTest 3: dup 4: invokespecial #10 // Method " ":()V 7: invokevirtual #11 // Method add:()Ljava/lang/String; 10: pop 11: return LineNumberTable: line 15: 0 line 16: 11 } SourceFile: "JVMTest.java"
那么jvm是怎样加载class文件的呢?
当一个类被创建事例,或者被引用到的时候,如果虚拟机发现之前没有加载过这个类,会通过类加载器,即ClassLoader把类加载进内存,在加载的过程中,主要做了三件事:
- 读取类的二进制流
- 把二进制流转换成方法区的数据结构,并存到方法区
- 在java堆中产生一个java.lang.Class对象
加载完成之后,就会进入连接的步骤,连接这个步骤又可以细分为验证,准备,解析三个部分
验证1- 作用:验证class文件是不是符合规范
- 文件格式的验证(是否以0xCAFFBABE开头;版本号是否合理)
- 元数据验证
* 是否有父类
* 是否继承了final类(final类不能被继承,如果继承了就有问题了)
* 非抽象类实现了所有抽象方法
- 字节码验证
- 运行检查
- 栈数据类型和操作码操作参数吻合(比如栈空间只有2字节,但其实需要大于2字节,此时就认为这个字节码是有问题的)
- 跳转指令指向合理的位置
- 符号引用验证
- 常量池中描述类是否存在
- 访问的方法或字段是否存在且有足够的权限
- 可以使用-Xverify:none关闭验证
如果验证通过,那么将会进入到准备的环节了
准备的作用是:
- 为类的静态变量分配内存,初始化为操作系统的初始值
- final static 修饰的变量:直接复制为用户定义的初始值,比如private final static int value = 123,直接赋值为123
- private static int value = 123,该阶段的值依然是0
准备完成之后就可以进入解析了,解析的作用是:把符号引用转换成直接引用
初始化解析完成会后,就会进入初始化阶段,在这个阶段,jvm 会执行clinit方法,clinit方法由编译器自动收集类里面的所有静态变量的赋值动作及静态语句块合并而成,也叫类构造器方法
- 初始化的顺序和源文件中的顺序一致
- 子类的clinit被调用前,会先调用父类的clinit
- JVM会保证clinit方法的线程安全性
- 初始化时,如果实例化一个新对象,会调用init方法对实力变量进行初始化,并执行对应的构造方法内的代码。
看一下下面这个类,我编写代码里有静态块,构造块,构造方法以及main方法,那么他们的执行顺序会是怎么样子的呢?
可以看到,先执行了,静态代码块,然后执行了main方法,再执行了构造块,最后执行了构造方法。
初始化完成这个类后,就可以使用该类了
卸载当不使用这个类的时候就可以把他卸载掉
当然呢,该文中描述的这个类加载流程是一个比较常规的类加载流程,事实上类加载的时候并不一定完全按照这个流程去做,比方说,解析不一定要在初始化之前,也有可能在初始化之后去做解析



