类的加载过程

首先,小伙伴们有没有想过这样一个问题呢:Java字节码文件是如何加载到JVM的呢?

 

一个类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。

 

 

 在Java虚拟机中类加载的全过程,包括加载、验证、准备、解析和初始化这5个阶段所执行的具体动作,这些都是有类加载器来实现的。

 

                  类加载

              ————————————————————————————————————

加载

加载是类加载过程的一个阶段。首先来一个简单的代码,打印###以及创建一个Hello对象。

1
2
3
4
5
6
public class ClassLoad {
    public static void main(String[] args) {
        System.out.println("########################");
        Hello hello = new Hello();
    }
}

运行之前,设置-XX:+TraceClassLoading

 

运行结果如下(截取后面部分),可以看到com.jvm.load.ClassLoad先被加载,然后是com.jvm.cls.Hello。ClassLoad是这个main方法的主类,所以优先加载。Hello的加载,是在实例化的时候,也就是被用到的时候,如果读者自己去断点,那就更直观的看到了。

 

 

 

上面这个图,可以看到输出了类的全限定名,类加载器就是通过这个来获取它的二进制字节流,这个二进制字节流来源如下:

  • class文件
  • zip、jar、war包中读取
  • 网络中读取,比如Applet
  • 运行时计算生成,比如动态代理技术
  • 由其他文件生成,比如JSP

 

 

验证

验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。当加载的class文件不符合虚拟机的要求,java虚拟机是无法执行这个字节码的,所以要先看看有没有符合,符合了才给虚拟机执行后续操作。

 

 

 

 

准备

准备是正式为类变量分配内存并设置类变量初始值的阶段。也就是说com.jvm.load.ClassLoadcom.jvm.cls.Hello在虚拟机中的内存分配是在这个阶段。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。设置类变量初始值通常情况下就是数据类型的零值。

1
2
3
4
// 准备阶段value=0
public static int value = 123;
// 准备阶段value2=123
public static final int value2 = 123;

  

 

解析

解析是虚拟机将常量池内的符号引用替换为直接引用的过程。比如com.jvm.load.ClassLoad编译的时候,不知道com.jvm.cls.Hello的实际内存地址,此时用符号引用,当com.jvm.cls.Hello加载到内存后,此时就改为直接引用,指向Hello的内存位置。

 

 

 

 

初始化

在准备阶段value=0,在初始化阶段,value才赋值为123。类初始化的条件:

  1. new一个对象,静态变量的赋值和取值,静态方法的调用。
  2. 通过反射机制调用。
  3. 如果子类初始化的时候,父类未初始化。
  4. 执行的主类(main方法)的时候。

下面看看类虽然被加载,却没有初始化的例子。

SuperClass:

1
2
3
4
5
6
public class SuperClass {
    static {
        System.out.println("SuperClass init");
    }
    public static int value = 123;
}

SubClass:

1
2
3
4
5
public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init");
    }
}

ClassLoad:

1
2
3
4
5
6
7
public class ClassLoad {
    public static void main(String[] args) {
        System.out.println("########################");
        //Hello hello = new Hello();
 System.out.println(SubClass.value);
    }
}

运行结果如下:

 

 

 可以看到SubClass被加载了,但是并没有输出SubClass init

 

                    类加载器

                 ——————————————————————————————————

类加载器有这几个:

  • 启动类加载器:jvm启动的时候,会优先加载<JAVA_HOME>\lib这个目录的核心类库。
  • 扩展类加载器:负责加载<JAVA_HOME>\lib\ext这个目录的类。
  • 应用程序类加载器:负责加载我们写的代码。
  • 自定义类加载器:根据我们的需要,加载特定的类。

下图展示了类加载器直接的层次关系,成为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

 

 

 

它的工作过程是这样的:

  1. 应用程序类加载器收到了Hello类的加载请求,先问扩展类加载器是否可以加载。
  2. 扩展类加载器也不会直接去加载,他也是向上级启动类加载器询问是否可以加载。
  3. 启动类加载器在自己负责的目录搜索了一下,发现自己找不到这个类,就说不行,你自己加载吧。
  4. 扩展类加载器在自己负责的目录搜索了一下,发现自己找不到这个类,就说不行,你自己加载吧。
  5. 应用程序类加载器在自己负责的目录搜索了一下,找到了这个类,把Hello类加载进来 

双亲委派模型一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

 好了,今天就到这儿吧,我是路人,我们下期见~~

posted @   杰然不同2019  阅读(310)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示