一、类的生命周期
二、类的加载过程
加载
通过一个类的完整路径查找此类字节码文件(class 文件即二进制文件)。将二进制文件的静态存储结构转化为方法区的运行时数据结构,并利用二进制流文件创建一个Class对象,存储在 Java 堆中用于对方法区的数据结构引用的入口;
class 文件的来源:有一点需要注意的是类加载机制不仅可以从文件系统读取 class 文件,也可以通过网络获取,其他 jar 包或者其他程序生成,如 JSP 应用。
类加载器:讲到类加载不得不讲到类加载的顺序和类加载器。Java 中大概有四种类加载器,分别是:启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),系统类加载器(System ClassLoader),自定义类加载器(Custom ClassLoader),依次属于继承关系(注意这里的继承不是 Java 类里面的 extends)
启动类加载器(Bootstrap ClassLoader):主要负责加载存放在Java_Home/jre/lib下,或被-Xbootclasspath参数指定的路径下的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载),启动类加载器是无法被Java程序直接引用的。
扩展类加载器(Extension ClassLoader):主要负责加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载Java_Home/jre/lib/ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
系统类加载器(System ClassLoader):主要负责加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器(Custom ClassLoader:自己开发的类加载器
双亲委派原则:类加载器在加载 class 文件的时候,遵从双亲委派原则,意思是加载依次由父加载器先执行加载动作,只有当父加载器没有加载到 class 文件时才由子类加载器进行加载。这种机制很好的保证了 Java API 的安全性,使得 JDK 的代码不会被篡改。
验证
JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。
确保二进制字节流格式符合预期(比如说是否以 cafe bene
开头)。
是否所有方法都遵守访问控制关键字的限定。
方法调用的参数个数和类型是否正确。
确保变量在使用之前被正确初始化了。
检查变量是否被赋予恰当类型的值。
准备
JVM 会在该阶段对类变量(也称为静态变量,static
关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
此时不会分配实例变量的内存,因为实例变量是在实例化对象时一起创建在Java 堆中的。而且此时类变量是赋值为零值,即 int 类型的零值为 0,引用类型零值为 null,而不是代码中显示赋值的数值。
解析
该阶段将常量池中的符号引用转化为直接引用。
在 class 文件中常量池里面存放了字面量和符号引用,符号引用包括类和接口的全限定名以及字段和方法的名称与描述符。
在 JVM 动态链接的时候需要根据这些符号引用来转换为直接引用存放内存使用。
初始化
该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
三、类加载时机
new、getstatic、putstatic、invokestatic 这 4 个字节码指令时对类进行初始化(即:实例化对象、读写静态对象、调用静态方法时,进行类的初始化);
使用反射机制对类进行调用时,进行类的初始化;
初始化一个类,其父类没有初始化时,先初始化其父类;
虚拟机启动时,初始化一个执行主类;
使用 JDK1.7 的动态语言支持时,如果 MethodHandle 实例的解析结果为 REF_getstatic、REF_putstatic、REF_invokestatic 的方法句柄(即:读写静态对象或者调用静态方法),则初始化该句柄对应类。
四、Java字节码文件中的JVM指令
1、创建一个 Java 源文件 Test02.java,并在 main 方法中完成简单的逻辑操作,如下所示:
publicclassTest02{publicstaticvoidmain(String[]args){inti=5;intj=10;intk=i+j;System.out.println(k);}}
2、在终端通过 javac 命令编译 HelloWorld.java。
javacTest02.java
3、反编译成我们能看懂的 JVM 指令,这里我们使用 javap -c 命令完成。
javap-cTest02.class
4、反编译之后的 JVM 指令如下所示:
1Compiledfrom"Test02.java"2publicclassorg.example.jvm.Test02{3publicorg.example.jvm.Test02();4Code:50:aload_061:invokespecial#1//Methodjava/lang/Object."<init>":()V74:return89publicstaticvoidmain(java.lang.String[]);10Code:110:iconst_5121:istore_1132:bipush10144:istore_2155:iload_1166:iload_2177:iadd188:istore_3199:getstatic#2//Fieldjava/lang/System.out:Ljava/io/PrintStream;2012:iload_32113:invokevirtual#3//Methodjava/io/PrintStream.println:(I)V2216:return}
具体解释一下上述的 JVM 指令
第 1 行表示当前的字节码文件编译自 Test02.java
。
第 3 行表示调用 Test02
的无参构造函数来实例化当前对象。
第 4 行到第 7 行表示无参构造函数的执行流程。
第 5 行表示把 this 压入操作数栈中。
第 6 行表示调用 Test02父类 Object 的无参构造,我们知道每个对象在实例化的时候都会默认先实例化其父类对象,并且默认调用父类的无参构造。
第 7 行 return 表示构造方法执行完毕。
第 10 行到第 22 行表示 main 方法的执行流程。
第 11 行表示将常量 5 压入操作数栈。
第 12 行表示取出操作数栈栈顶元素,即 5,然后保存到局部变量表第 1 个位置,即变量 i。
第 13 行表示将常量 10 压入操作数栈。
第 14 行表示取出操作数栈栈顶元素,即 10,然后保存到局部变量表第 2 个位置,即变量 j。
第 15 行表示将局部变量表第 1 个变量(i)压入操作数栈。
第 16 行表示将局部变量表第 2 个变量(j)压入操作数栈。
第 17 行表示取出操作数栈中的前两个值相加,并将结果压入操作数栈顶。
第 18 行表示取出操作数栈栈顶元素,保存到局部变量表第 3 个位置,即变量 k。
第 19 行表示读取静态实例 PrintStream。
第 20 行表示将局部变量表第 3 个变量(k)压入操作数栈。
第 21 行表示调用 PrintStream 的 println 方法,将操作数栈顶元素(变量 k)输出。
第 22 行 return 表示 main 方法执行完毕。
作者:初念初恋