JVM

【JVM】类加载机制

2021-06-30  本文已影响0人  叨唧唧的

一 类加载的时机

类从被加载到虚拟机内存中开始,到卸载(GC)出内存为止,它的整个生命周期包括:

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、

使用(Using)和卸载(Unloading)七个阶段,如图:

其中加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的进行,

然而解析阶段不一定:它在某些情况下可以在初始化阶段之后再开始,主要是为了支持Java语言的运行时绑定。

关于初始化阶段,虚拟机规范严格规定以下5种情况需要立即对类进行"初始化"(在此之前,加载、验证、准备需要完成):

1)遇到new、getstatic、putstatic和invokestatic这四条字节码指令时,如果类没有进行过初始化,需要进行初始化。

这4条指令最常见的Java代码场景:使用new关键字实例化对象、读取或设置一个类的静态字段、以及调用一个类的静态方法。

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果没有进行过初始化,需要先进行初始化。

3)当初始化一个类的时候,如果发现其父类还没有进行初始化化,需要先触发父类初始化。

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5)当使用JDK1.7动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、

REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
二 类加载的过程

JVM类加载分为加载、验证、准备、解析和初始化5个阶段。
1、加载

"加载"是"类加载"的一个阶段,在加载阶段,虚拟机需要完成以下3件事情:

1)通过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

虚拟机规范的这3点要求其实并不算具体,虚拟机可以根据自己的情况实现。

比如我们可以从以下几个方面获取二进制字节流文件:

1)从ZIP包中读取,这个是JAR、EAR、WAR包基础。

2)从网络中获取,比如典型的Applet。

3)运行时动态生成,比如动态代理技术。

4)从其他文件生成,比如JSP文件等等。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在方法区之中,

方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范规定此区域的具体数据结构。

然后在内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些

类型数据的外部接口。

总结:

通过类的完全限定名,查找类的字节码文件,利用字节码文件创建Class对象。
2、验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,

并不会危害虚拟机自身的安全。验证大概分为4个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
1、文件格式验证

 第一阶段验证字节流是否符合Class文件格式的规范,并能被当前版本的虚拟机处理。主要包括以下验证点:

1)是否以魔数0xCAFEBABE开头。

2)主、次版本号是否在当前虚拟机处理范围之内。

3)常量池的常量中是否有不被支持的常量类型。

4)指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。

5)CONSTANT_UTF8_info型的常量中是否有不符合UTF8编码的数据。

6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。

......

验证内容还有很多,这个阶段的验证时基于二进制字节流进行的,这个阶段验证通过,字节流才会进入内存的

方法区中进行存储,后面3个阶段的验证全部基于方法区中的存储结构进行验证,不直接操作字节流。
2、元数据验证

 第二阶段是对字节码描述的信息进行语义的分析,以保证其描述的信息符合Java语义规范的要求,

主要包括以下验证点:

1)这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类,Object为除自身外的所有父类)。

2)这个类的父类是否继承了不允许被继承的类(被final修饰的类)。

3)如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法。

4)类中的字段、方法是否与父类产生矛盾。

......

这个阶段的目的主要是对元数据信息进行校验,保证不存在不符合Java语言规范的元数据信息。
3、字节码验证

第三阶段比较复杂,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

主要包括以下验证点:

这个阶段比较复杂,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。

2)保证跳转指令不会跳转到方法体以外的字节码指令上。

3)保证方法体中类型转换是有效的。
4、符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段,

即解析阶段中发生。符号引用验证可以看做是对类自身以外的信息进行匹配性校验,主要包括以下验证点:

1)符号引用中通过字符串描述的权限定名是否能找到对应的类。

2)在指定类中是否存在符合方法的字段描述符以及简单名称锁描述的方法和字段。

3)符号引用中的类、字段、方法的访问性是否可以被当前的类访问。

符号引用验证的目的是确保解析动作能正常执行。

对于虚拟机来说,验证很重要,但不是一定必要的阶段。如果锁运行的全部代码已经被反复使用和验证过,

那么在实施阶段接可以考虑使用-Xvefify:none来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

总结:

确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全。
3、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些类变量所使用的内存都将在方法区中进行分配。

首先,需要明确这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会

在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值"通常情况"下是数据类型的零值,假设一个

类变量的定义为:

public static int value = 123;

变量值在准备阶段过后的初始值为0,而不是123,因为这个时候尚未开始执行任何Java方法,而把value赋值为123的

putstatic指令是程序被编译后,存放于类构造器<clinit>方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。

但是有一种特殊情况就是常量定义:

public static final int value = 123;

在编译时Javac将会为value生成ConstanValue属性,在准备阶段虚拟机就会根据ConstanValue的设置将value赋值为123。

总结:

进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null)。

不包含final修饰的静态变量,因为final变量在编译时分配。
4、解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:

CONSTANT_Class_info

CONSTANT_Field_info

CONSTANT_Method_info

等类型的常量。

符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以

各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范

的Class文件格式中。

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的

目标必定已经在内存中存在。

总结:

将常量池中的符号引用替换为直接引用的过程,直接引用为直接指向目标的指针或者相对偏移量等。
5、初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,

其它操作都由JVM主导和控制。到了初始阶段,才开始真正执行类中定义的Java程序代码(或者说字节码)。

初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作

和静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行之前,父类的<client>方法已经执行完毕。

注意: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。

注意以下几种情况不会执行类初始化:

1)通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

2)定义对象数组,不会触发该类的初始化。

3)常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

4)通过类名获取Class对象,不会触发类的初始化。

5)通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是

告诉虚拟机,否要对类进行初始化。

6)通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

总结:

1)主要完成静态块执行以及静态变量的赋值,先初始化父类,再初始化当前类,只有对类主动使用时才

会初始化。

2)触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候,

或者某个子类初始化的时候。
三 类加载器
1、类和类加载器

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。

类加载虽然只能用于实现类的加载动作,但它在Java程序中的作用却远远不限于类的加载阶段。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在JVM中的唯一性,

每一个类加载器都拥有一个独立的类命名空间。也就是说,比较两个类是否"相等",只有在这

两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载不同,哪这两个类必定不相等。

相等是指equals()、isInstance()方法、instanceof返回结果。

eg:

package com.jpeony.jvm.loadclass;
 
import java.io.IOException;
import java.io.InputStream;
 
/**
 * 类加载器与instanceof关键字演示:
 *
 * obj为自定义累加器加载对象,而ClassLoaderTest为另外一个类加载器加载的,所以,通过instanceof比较返回结果为false。
 *
 * @author yihongeli
 */
public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new java.lang.ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
 
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
 
        Object obj = myLoader.loadClass("com.jpeony.jvm.loadclass.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof ClassLoaderTest);
    }
}

运行结果:

从结果可以看出,obj为自定义累加器加载对象,而ClassLoaderTest为另外一个类加载器加载的,

但是,所以,通过instanceof比较返回结果为false。
2、双亲委派模型

类加载器双亲委派模型图:

1)启动类加载器(Bootstrap ClassLoader)

负责加载JRE核心类库,如jre目标下的rt.jar、charsets.jar等。

2)扩展类加载器(Extension ClassLoader)

这个类加载器由sun.misc.Launcher中的静态内部类ExtClassLoader负责实现。

负责加载JRE扩展目录ext中的jar包 ,开发者可以直接使用扩展类加载器。

3)应用程序类加载器(Application ClassLoader)

这个类加载器由sun.misc.Launcher中的静态内部类AppClassLoader负责实现。负责加载用户路径(classpath)

上的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般就默认使用

应用程序类加载器。

4)用户自定义类加载器(User ClassLoader)

负责加载用户自定义路径下的jar包。

在我们的应用程序都是由前3个类加载器互相配合进行加载的,如果有必要,可以加入自定义类加载器。

类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Deletation Model)。双亲委派模型

要求除了顶层的类加载器外,其余的类加载器都应当有自己的父类加载器。这些类加载器之间的父子关系不是

通过继承(Inheritance)的关系来实现,而都是通过组合(Composition)的关系来复用父加载器的代码。

双亲委派模型的工作过程:当一个类加载器收到类加载请求,它首先不会去尝试加载这个类,而是把这个请求

委派给父类加载器去完成,每个层的类加载器都是如此,因此最终加载任务都会传递到顶层的启动类加载器,

只有当父类加载器无法完成加载任务时,子加载器才会尝试自己去执行加载请求。
3、双亲委派机制的优势

1)沙箱安全机制

防止核心API被串改,JDK核心类,都由引导类加载器或扩展类加载器加载,

比如自己写的String.class类,会被委托到顶层加载,顶层类加载器直接从rt.jar里面就能够加载String,

则我们自己写的String.class就不会被加载。

2) 避免类的重复加载

当父类已经加载了该类时,子类加载器就没有必要再加载一次。

比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,

最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

双亲委派模型对于保证java程序的稳定运作非常重要,咱们看下源码是如何实现的,这里是jdk8的源码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 判断一个类是否已经被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 遵循双亲委派的模型,首先会通过递归从父加载器开始找,
                // 直到父类加载器是Bootstrap ClassLoader为止。
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
                // 如果父类加载器抛出异常,说明父类加载器无法加载请求
            }
 
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                // 如果父类加载器无法加载的时候,调用本身的findClass进行类加载
                long t1 = System.nanoTime();
                c = findClass(name);
 
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

源码分析:

1、首先通过Class c = findLoadedClass(name);判断一个类是否已经被加载过。

2、如果没有被加载过执行if (c == null)中的程序,遵循双亲委派的模型,首先会通过递归从父加载器开始找,

直到父类加载器是Bootstrap ClassLoader为止。

3、最后根据resolve的值,判断这个class是否需要解析。

而上面的findClass()的实现如下,直接抛出一个异常,并且方法是protected,很明显这是留给我们开发者自己去实现的。

总结:

1)双亲委派模式加载器加载类的时候,先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器。

父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载。

2)优点就是沙箱机制即避免核心api被篡改,同时避免类的重复加载。
————————————————
版权声明:本文为CSDN博主「街灯下的校草」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yhl_jxy/article/details/81295635

上一篇下一篇

猜你喜欢

热点阅读