【jvm】jvm的类加载机制

前言:提到jvm的类加载机制,就不得不说我当年的沙雕经历了,当年不明白为啥面试官都喜欢问jvm的类加载机制,当时心想学这东西有啥用,它怎么加载关我啥事呀,能写代码不就好了吗?但无奈应试教育教会了我,虽然不知道为啥要学,但人家要考,你就得学,然后学呗,学完算是知道它是怎么加载类的了,但依旧没能深刻理解,而且所学的也仅仅停留在理论这个层面,而平时写代码大多是写业务,又很少接触到这些,于是没多久就给忘了...为了你们跟我一样,开篇我先通过一道面试题来勾引你们,加深印象,这道面试题可以说是非常经典了:

class Singleton{
    private static Singleton singleton = new Singleton();
    public static int value1;
    public static int value2 = 0;

    private Singleton(){
        value1++;
        value2++;
    }

    public static Singleton getInstance(){
        return singleton;
    }

}

class Singleton2{
    public static int value1;
    public static int value2 = 0;
    private static Singleton2 singleton2 = new Singleton2();

    private Singleton2(){
        value1++;
        value2++;
    }

    public static Singleton2 getInstance2(){
        return singleton2;
    }

}

public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("Singleton1 value1:" + singleton.value1);
        System.out.println("Singleton1 value2:" + singleton.value2);

        Singleton2 singleton2 = Singleton2.getInstance2();
        System.out.println("Singleton2 value1:" + singleton2.value1);
        System.out.println("Singleton2 value2:" + singleton2.value2);
    }

上面代码运行后的结果是:

Singleton1 value1 : 1 
Singleton1 value2 : 0 
Singleton2 value1 : 1 
Singleton2 value2 : 1

是不是有点惊讶,不要紧,在我真正理解Jvm类加载机制之前,也是这种想法,why? 摆了佛冷!

先不要急问为啥是这样的结果,我们先来学习下Jvm的类加载机制,学完再来分析,你就豁然开朗了.


先来看下Jvm的类加载步骤,一般分为五步,如果拆分开来,可以分为七步:

第一步:加载

加载主要是将.class文件以二进制流的形式读入jvm中.

加载.class文件的方式
– 从本地系统中直接加载
– 通过网络下载.class文件
– 从zip,jar等归档文件中加载.class文件
– 从专有数据库中提取.class文件
– 将Java源文件动态编译为.class文件

在类的加载阶段,Jvm需要完成以下三件事:

1)通过类的全限定名获取该类的二进制字节流; 
2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构; 
3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

第二步:连接

连接一共由三个步骤构成

①验证:主要是为了检查加载进来的字节码是否符合Jvm规范,不要是那些不合法的甚至对Jvm造成损害的字节码文件.

验证阶段会完成以下4个阶段的检验动作: 

1)文件格式验证 
2)元数据验证(是否符合Java语言规范) 
3)字节码验证(确定程序语义合法,符合逻辑) 
4)符号引用验证(确保下一步的解析能正常执行)

②准备:为静态变量所在方法区分配内存,并设置默认初始值.

③解析:虚拟机将常量池内的符号引用替换为直接引用的过程

第三步:初始化

初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值.

在Java中对类变量进行初始值设定有两种方式:

1)声明类变量是指定初始值

2)使用静态代码块为类变量指定初始值

类的初始化步骤:

如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化;所以其实在java中初始化一个类,那么必然是先初始化java.lang.Object,因为所有的java类都继承自java.lang.Object

类初始化时机:

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

– 创建类的实例,也就是new的方式

– 访问某个类或接口的静态变量,或者对该静态变量赋值

– 调用类的静态方法

– 反射(如Class.forName(“com.shengsiyuan.Test”))

– 初始化某个类的子类,则其父类也会被初始化

– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

第四步:使用

使用没啥好说的,就是调用类里面的方法/变量/常量等...

第五步:卸载

GC把无用对象从jvm内存中卸载.


在以下几种情况下,jvm会结束生命周期:

– 执行了System.exit()方法

– 程序正常执行结束

– 程序在执行过程中遇到了异常或错误而异常终止

– 由于操作系统出现错误而导致Java虚拟机进程终止

有了上面的jvm类加载机制储备,我们再回过头来看这道面试题:

#先看singleton1
①加载:以二进制流的形式加载进虚拟机
②验证:验证是否符合jvm规范
③准备:为静态变量value1,value2分配内存,并初始化值,由于是value1和value都是基本数据类型int,非引用类型,所以初始值均为0,此时value1和value2均为0
④解析:将常量池中的符号引用变为直接引用
⑤初始化:由于在Main方法中调用了静态方法getInstance,拆成两步,结果一目了然:
第一步:该静态方法构造实例时其构造器将value1和value2都做了自增,value1和value2的值都变为了1;
第二步:由于调用了静态方法,初始化被触发,会对变量进行赋值操作,由于value2在代码中有value2=0这样的赋值语句,于是value2的值变为了0;
⑥使用:在System.out.println打印到控制台,value1的值为1,value2的值为0
⑦卸载:被jvm GC回收
#再看singleton2
①加载:以二进制流的形式加载进jvm虚拟机
②验证:验证是否符合jvm规范
③准备:为静态变量value1和value2分配内存,并设置初始值,由于value1和value2都是基本数据类型int,非引用类型,所以初始值均为1,此时value1=0,value2=0;
④解析:将常量池中的符号引用替换为直接引用
⑤初始化:由于Main方法中调用了静态方法getInstance,同样拆成两步:
第一步:由于调用了静态方法,初始化被触发,会对变量进行赋值操作,此时value1和value2均为0,赋值语句value2=0执行后,其值仍保持0不变;
第二步:该静态方法构造实例时其构造器将value1和value2的值都做了自增,value1和value2的值都变为1
⑥使用:在System.out.println打印到控制台,value1的值为1,value2的值也为1
⑦卸载:被jvm GC回收

细心点的朋友会发现,其实singleton1和sington2的主要区别就是:

singleton1是在构造方法执行后执行了赋值操作,sington2是在构造方法执行前执行了赋值操作,这和代码所处的位置有关,java是默认从上往下执行代码的,所以在以后书写代码时务必注意代码所处的位置,否则可能会让你在jvm类加载机制上入坑一把...


说完了类的加载机制,再来了解下jvm的类加载器,Jvm的类加载器主要是在类加载的第一步,为jvm以二进制的流的形式获取字节码文件.

jvm的类加载器主要有以下三种:

启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader):也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。

各个类加载器之间的层次关系如图:
 

类加载器之间的这种层次关系叫做双亲委派模型:

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器.

双亲委派模型的工作过程是:

  • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。

  • 每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。

  • 只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

作用:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。因此,使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处:类随着它的类加载器一起具备了一种带有优先级的层次关系.

例如类java.lang.Object,它由启动类加载器加载。双亲委派模型保证任何类加载器收到的对java.lang.Object的加载请求,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并用自定义的类加载器加载,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱.

双亲委派模型的代码实现:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先检查类是否被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 没有则调用父类加载器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父类加载失败,抛出ClassNotFoundException 异常后
            c = findClass(name);
        }
    }
    if (resolve) {
	    //5 再调用自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}

破坏双亲委派模型
双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。 
若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了。 
下面介绍两个例子来讲解破坏双亲委派模型的过程。

JNDI破坏双亲委派模型 
JNDI是Java标准服务,它的代码由启动类加载器去加载。但是JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。 
为了解决这个问题,引入了一个线程上下文类加载器。 可通过Thread.setContextClassLoader()设置。 
利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。

Spring破坏双亲委派模型 
Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。 
那么Spring是如何访问WEB-INF下的用户程序呢? 
使用线程上下文类加载器。 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。 
利用这个来加载用户程序。即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。
 

参考地址:https://nomico271.github.io/2017/07/07/JVM%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/

猜你喜欢

转载自blog.csdn.net/lovexiaotaozi/article/details/83586856