JVM会在什么情况下加载一个类

一个类从加载到使用,一般会经历如下过程:
加载——>验证——>准备——>解析——>初始化——>使用——>卸载
什么时候会将.class字节码文件加载这个类到JVM内存里来:当代码中使用到这个类的时候就会加载
例如:

  1. public class Dog {
  2. public static void main() {
  3. }
  4. }

有一个Dog类,其中main方法作为程序主入口,那么一旦你的JVM进程启动以后,它一定会先把你的这个类(Dog)加载到内存里,然后从main()方法的入口代码开始执行。
流程如下:
接着上边代码,做如下改动:

  1. public class Dog {
  2. public static void main() {
  3. Food food = new Food();
  4. }
  5. }

代码中使用到了Food类,这时JVM通过类加载器,从Food.class字节码文件中加载对应的类到内存里来使用,这样代码才能跑起来。
流程如下:

简单总结:首先Dog代码是包含main()方法的主类,一定会在JVM进程启动之后被加载到内存中,开始执行main()方法中的代码,然后遇到了别的类,如Food,此时就会从对应的.class字节码文件加载对应的类到内存里来。

JVM不同阶段解析

  • 验证阶段:根据Java虚拟机规范,来校验加载进来的.class文件中的内容是否符合指定的规范
  • 准备阶段:我们写好的类中一般会定义静态成员变量、静态方法,在验证阶段验证通过以后,就需要给类分配一定的内存空间来进行初始化静态变量
  • 解析阶段:实际上就是把符号引用替换为直接引用的过程

以上三个阶段的流程:

  • 核心阶段:初始化

类初始化代码举例:

  1. public class Food {
  2. public static int fish = Configuration.getInt("fish.num");
  3. }

准备阶段只是给fish变量分配了一个内存空间,然后赋个初始值为0。
初始化阶段时才会执行Configuration.getInt("fish.num")这段代码,然后赋值给fish。
静态代码块也会在这个阶段执行。

什么时候会初始化一个类?
一般来说在以下一些情况会进行初始化:

  • new Food() 来实例化类的对象,此时就会触发类的加载到初始化的全过程,把这个类准备好,然后再实例化一个对象出来
  • 包含main()方法的主类,必须是立马初始化的
  • 此外还有一个非常重要的规则,如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化它的父类

    类加载器和双亲委派机制

  • 启动类加载器:Bootstrap ClassLoader,主要是负责加载我们在机器上安装的Java目录下的核心类,这个核心目录就是lib目录,所以一旦jvm启动,首先就会依托启动类,去加载你的Java安装目录下的lib目录中的核心类库

  • 扩展类加载器:Extension ClassLoader,主要是负责加载Java目录下的lib\ext目录
  • 应用程序类加载器:Application ClassLoader,这个类加载器主要负责加载ClassPath环境变量所指定的路径中的类
  • 自定义类加载器:根据需要加载类
  • 双亲委派机制:JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。

基于这个亲子层级结构,就有一个双亲委派的机制。
双亲委派机制简单来说就是:父加载器先进行加载,加载不了再由儿子来加载,以此类推。

每日一问

如何保证.class文件不被人拿到以后反编译获取源代码?
编译时可以用一些工具对字节码加密,或者做混淆处理。然后在类加载的时候,对加密类,考虑采用自定义的类加载器来解密文件即可。