JVM篇(四)类的加载机制

JVM虚拟机加载Class文件(二进制字节流)到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java类型,这一系列过程就是类的加载机制

类从被虚拟机加载到内存开始,直到卸载出内存为止,整个生命周期包括:加载——验证——准备——解析——初始化——使用——卸载 这7个阶段。其中验证、准备、解析3个部分统称为连接。

其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定、动态绑定、晚期绑定)。 

哪些情况能触发类的初始化阶段?(前提:加载、验证、准备已经执行完)

  1. 遇到new、getstatic、putstatic、invokestatic 这4条指令时如果类没有初始化则会触发其初始化,(工作中触发这4种指令最常见的场景:new实例化对象、读取or设置类的静态字段【final修饰或者已经把静态字段放入常量池的除外】、调用类的静态方法)
  2. 使用反射的时候
  3. 初始化类的时候如果其父类还没进行初始化,则需要先触发父类的初始化
  4. 虚拟机启动时,需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个类
  5. 使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄。切这个句柄对应的类没有初始化,则需要先触发其初始化

   注意:所有引用类的方式都不会触发初始化(被动引用)例如:创建数组、引用final修饰的变量、子类引用父类的静态变量 不会触发子类初始化但是会触发父类初始化

public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 1127;
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWORLD = "hello world!";
}

class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
        /**
         *  output : SuperClass init!
         *
         * 通过子类引用父类的静态对象不会导致子类的初始化
         * 只有直接定义这个字段的类才会被初始化
         */

        SuperClass[] sca = new SuperClass[10];
        /**
         *  output :
         *
         * 通过数组定义来引用类不会触发此类的初始化
         * 虚拟机在运行时动态创建了一个数组类
         */

        System.out.println(ConstClass.HELLOWORLD);
        /**
         *  output :
         *
         * 常量在编译阶段会存入调用类的常量池当中,本质上并没有直接引用到定义类常量的类,
         * 因此不会触发定义常量的类的初始化。
         * “hello world” 在编译期常量传播优化时已经存储到 NotInitialization 常量池中了。
         */
    }
}

类的加载过程

1、加载

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

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

数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:

  1. 如果数组的组件类型是引用类型,那就递归采用类加载加载。
  2. 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
  3. 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。

相对于类加载的其他阶段,加载阶段(准确的说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的。因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由开发人员自定义的类加载器来完成(即重写类加载器的 loadClass() 方法)。

加载完成后,外部的二进制字节流就转化成虚拟机所需的格式存储在方法区中,然后在内存中实例化一个java.lang.Class类的对象。这个对象将作为程序访问方法区中的这些类型数据的外部接口。

 加载阶段与连接阶段的部分内容是交叉进行的,并不是加载完成后才能执行验证等操作。这些夹在加载之中的动作仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

2、验证

验证是连接的第一步,为了保证加载的二进制字节流所包含的信息是符合虚拟机规范的。

  • 文件格式验证:验证字节流是否符合Class文件格式规范。
    • 是否以魔数 0xCAFEBABE 开头
    • 主、次版本号是否在当前虚拟机处理范围之内
    • 常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
    • Class 文件中各个部分集文件本身是否有被删除的附加的其他信息
    • ……
  • 元数据验证:对字节码描述的信息进行语义分析。
    • 这个类是否有父类(除 java.lang.Object 之外)
    • 这个类的父类是否继承了不允许被继承的类(final 修饰的类)
    • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    • 类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)
  • 字节码验证:通过数据流和控制流的分析,确定程序语义是合法的、符合逻辑的(说白了就是对类的方法体进行分析确保方法在运行时不会危害虚拟机)。
    • 保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
    • 保证跳转指令不会跳转到方法体以外的字节码指令上
    • 保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
    • ……
  • 符号引用验证:确保解析动作能正常执行。
    • 符号引用中通过字符创描述的全限定名是否能找到对应的类
    • 在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
    • 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问

只有通过文件格式验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。元数据验证阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。字节码验证是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。符号引用验证的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

验证阶段是非常重要,但不一定是必要的阶段(因为对程序运行期没有影响)。如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段可以使用-Xverify:none参数来关闭验证。

3、准备

这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

public static int value = 1127;
这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 1127 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。
 

数据类型零值数据类型零值
int0booleanfalse
long0Lfloat0.0f
short(short) 0double0.0d
char'\u0000'referencenull
byte(byte) 0

特殊情况:如果类字段的字段属性表中存在 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 1127。

注意:

  • 此时被分配的仅仅是静态变量,而不是实例变量,实例变量将随着对象实例一起分配在Java堆中
  • 初始值通常情况下是数据类型的零值。假如定义一个静态变量 public static int value = 1127;那么value在准备阶段初始值为0而不是1127
  • 被final修饰的变量在准备阶段就初始化为属性所指定的值。例如: public static final int value = 123;那么value在准备阶段初始值就是123。

4、解析

这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应于常量池的 7 中常量类型。

  • 符号引用:以一组符号来描述引用的目标,符号可以是任何形式的字面量。
  • 直接引用:指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。 

5、初始化

前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码

初始化阶段是执行类构造器<clinit>()方法的过程。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员制定的参数值去初始化类变量和其他资源。

类构造器<clinit>()方法:是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。

编译器收集的顺序是由语句在源文件中出现的顺序决定的;静态代码块只能访问定义在静态块之前的变量,定义在它之后的变量,在前面的静态块中可以赋值,但不能访问。

    public class Demo {
        
        public static int va;
        
        static {
            value = 1;            //可以编译通过
            va = value;           //报错  非法向前引用
            System.out.println("父类初始化");
        }

        public static int value = 123;
    }

类加载器(ClassLoader)

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

虚拟机设计团队把类加载中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码块称为类加载器。

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

  • BootStrap ClassLoader(启动类加载器)

    它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
     
    public class ClassLoaderTest {
     
    	public static void main(String[] args) {
    		
    		URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
    		for(URL url : urls){
    			System.out.println(url.toExternalForm());
    		}
    	}
    
    }
    运行结果如下:

  • Extension ClassLoader(扩展类加载器)

    它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
  • APP ClassLoader(应用类加载器,也称为系统类加载器)
     

    被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。 

类加载器加载Class大致要经过如下8个步骤:

  1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
  2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
  3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
  4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
  5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
  6. 从文件中载入Class,成功后跳至第8步。
  7. 抛出ClassNotFountException异常。
  8. 返回对应的java.lang.Class对象。

JVM的类加载机制主要有如下3种。

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

双亲委派模型

从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)

双亲委派模型的工作过程是:如果一个类加载器收到了一个类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的加载器都是如此,因此所有的加载请求最终都应该到达顶层的启动类加载器。只有当父加载无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派机制:

1、当ApplicationClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

  1. 启动类加载器
    加载 lib 下或被 -Xbootclasspath 路径下的类

  2. 扩展类加载器
    加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类

  3. 引用程序类加载器
    ClassLoader负责,加载用户路径上所指定的类库。

除顶层启动类加载器之外,其他都有自己的父类加载器。
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。

双亲委派模型意义:

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

ClassLoader源码分

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 先检查此类是否已被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //委派给父类加载器去加载
                    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.
                    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;
        }
    }                        

首先找缓存(findLoadedClass),没有的话就判断有没有 parent,有的话就用 parent 来递归 的 loadClass,然而 ExtClassLoader 并没有设置 parent,则会通过 findBootstrapClassOrNull 来加载 class,而 findBootstrapClassOrNull 则会通过 JNI 方法”private native Class findBootstrapClass(String name)“来使用 BootStrapClassLoader 来加载 class。
然后如果 parent 未找到 class,则会调用 findClass 来加载 class,findClass 是一个 protected 的空方法, 可以覆盖它以便自定义 class 加载过程。
另外,虽然 ClassLoader 加载类是使用 loadClass 方法,但是鼓励用 ClassLoader 的子类重写 findClass(String),而不是重写 loadClass,这样就不会覆盖了类加载默认的双亲委派机制。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值