Java 代码是如何运行起来的?


首先,开发人员会编写 .java 文件,然后将 .java 文件编译成 .class 文件。或者说打包成 .jar 包或者 .war 包, jar 包和 war 包里面的就包含了所有的 .class 文件。

然后直接使用 java 命令去运行,或者启动 Tomcat 等容器。这个过程,其实是在系统中启动一个 JVM 进程,JVM 进程去执行 .class 文件里面的代码逻辑。

什么时候加载类?


通常的类是存放在硬盘上的,什么时候 JVM 会将编写好的类加载进去 JVM 内存去执行呢?一般来说,是在代码用使用这个类的时候,JVM 才会将 .class 文件加载进去 JVM 内存中。

例如有一个 Test 类,使用 jar 命令编译后,再使用 jar 命令执行这个 Test.class 文件的时候。JVM 启动的时候就会加载这个 Test 类,并且从 main 方法开始执行。

😄JVM加载类的完全过程 - 图1

这个类稍微改一下,如下图:
😄JVM加载类的完全过程 - 图2

main 方法还会使用到 Demo 这个类,那么在执行 new Demo() 之前,就会加载 Demo 这个类进 JVM 的内存。

上面只是简单介绍了加载类的时机的整体总结出来的情况。更具体来说,在什么情况下需要开始类加载的第 1 个过程”加载”,《Java虚拟机规范》中并没有强行约束,这个加载的时机可以由虚拟机自行实现。

但是《Java虚拟机规范》中明确约束了有且只有 6 种情况,必须要对类进行”初始化”。那么,进行初始化之前,就必须加载类。这 6 种情况,总结下来,就是需要使用类的时候。

这 6 这情况分别是:

  1. 遇到 new, getstatic, putstatic, incokestatic 这 4 条字节码指令的时候。一下这几种情况会产生这 4 条字节指令。

    • 使用 new 关键字实例化对象的时候。
    • 读取或设置一个类的静态字段的时候。
    • 调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包的方法对类进行调用的时候,如果类没有进行过初始化的时候。

  3. 初始化类的时候,如果父类还没有进行过初始化,则先触发父类的初始化。
  4. 虚拟机启动的时候,会初始化 main() 方法所在的类。
  5. 一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 始终类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  6. 当一个接口中定义了默认方法 (default 关键字修饰的接口方法)时,这个接口的实现类发生了初始化,那接口就要在这实现类初始化之前触发初始化。

类的生命周期


类加载到使用到使用完毕会经过 7 个过程:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

类加载的全过程是:加载 -> 验证 -> 准备 -> 解析 -> 初始化 ,验证、准备、解析这3个阶段又成为连接阶段。类加载的过程可以简化我:加载 -> 连接 -> 初始化。

  1. 加载

这里的加载阶段,是整个类加载的第 1 个阶段。对于这个动作,《Java 虚拟机规范》没有限定用什么方式去执行,可以加载生成好的本地文件,可以从网络加载,甚至可以加载动态生成的一个字节码文件。

具体的加载执行的内容是:通过类全限定名来获二进制流,然后将类的静态存储结构转换为方法去的运行时数据结构,最后在内存中生成一个代表这个类的 Class 对象。

加载阶段和接下来的验证阶段是交替进行的,也就是说,会一边加载一边验证。但是加载阶段绝对是在验证阶段之前开始。

  1. 验证

加载字节码文件后,需要进行验证,防止加载的文件是非法文件,或者是损害JVM的文件。 主要进行
以下几个验证。

  • 文件格式验证:验证字节流是否符合 Class 文件格式的规范。
  • 元数据验证:对字节码描述的信息进行语义分析,例如,是否有父类,父类是否 final 类等。
  • 字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:这个校验行为发生在解析阶段中。将符合引用转换为直接引用。主要是保证引用一定会被访问到,不会出现类无法访问的问题。

如果程序运行的全部代码,都是经过反复验证的,那么可以使用 -Xverify:none 参数来关闭大部分
的类验证措施,从而缩短虚拟机类加载的时间。

  1. 准备
    准备阶段中,会为类和类变量( static 修饰的变量)分配内存空间,赋予初始值( 0 值)。 如果类变量是常量的话,则直接赋常量值。

  2. 解析
    将字符引用解析为直接引用的过程。非常复杂。 主要包含以下几个阶段:类或者接口的解析、字段解析、方法解析、接口方法解析。

  3. 初始化
    这个阶段,会执行类的静态代码块和对类变量( static 修饰的变量)进行赋值,赋予代码设定的值,如果值是调用一个方法产生,则会执行这个方法。

换个说话就是这个阶段就是执行类构造器 () 方法的过程。() 由编译器自动生成的,但
是缺不是必须的。只有在类中有静态变量赋值,或者有静态语句块 static{} ,才会生成这个方法。并且
JVM 保证了父类的 () 一定在在子类的 () 方法执行之前执行。

编辑器收集静态变量赋值动作和静态语句块来生成这个方法,编译器收集的顺序是由语句在源文件中
出现的顺序而定的。值得注意的是,静态语句块可以赋值但是不能读取在语句块之后定义的静态变量.

  1. 使用
    进行实例化,方法调用,等等。

  2. 卸载
    就类从永久代/元数据空间清除,这个要求比较苛刻。

双亲委派机制


加载类有多个,并且有层级关系。 最顶层的是 Bootstrap ClassLoader (启动加载器),负责加载 Java 的最基础的类库。 然后是 Extension ClassLoader (扩展加载器),负责加载 lib/ext 下的类库。然后是Application ClassLoader (应用程序加载器) ,负责加载编写好的 classPath 路径下的类库。
还有自定义的类加载器。 自定义的加载器,在应用程序加载器的下一层。

image.png

双亲委派机制的意思是,加载器首先让父类去加载,如果父类加载不了,才用子类去加载。

那么,加载的顺序是,应用程序加载器,请求扩展加载器去加载,扩展加载器请求启动加载器去加载,启动加载器加载不了,则加载这个会回到扩展类加载器,扩展加载器也加载不了,才让应用程序加载器去加载。

同一个类,被同一个加载器加载,才算是同一个类。 双亲委派机制,可以保证,同一个类,被同一个加载器加载。

Tomcat 类加载机制


Tomcat 的类加载机制是打破了双亲委派机制的。 具体还需要去研究补充。