码迷,mamicode.com
首页 > 其他好文 > 详细

虚拟机类加载和双亲委派机制

时间:2020-09-14 19:22:46      阅读:68      评论:0      收藏:0      [点我收藏+]

标签:ini   防止   bee   alac   inf   gets   it!   直接   使用   

虚拟机类加载和双亲委派机制

概述

Java虚拟机把描述类的数据从Class文件加载到内从中,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程叫做虚拟机的类加载机制。

类加载生命周期

一个类型从被加载到虚拟机的内存中开始、到卸载出内存,整个生命周期经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析单个部分统称为连接(Linking),如下图所示:

技术图片

在整个过程中,加载、验证、准备、初始化、卸载五个阶段的顺序确定的,类型加载过程必须按照这种顺序按部就搬的开始,但是解析阶段不一定:它在某些情况下可以在初始化阶段之后开始,这个为了支持Java语言的运行时动态绑定。

动态绑定:主要体现在继承、多态的时候,比如一个父类Animal,子类Dog继承了父类, Animal animal = new Dog();在父类和子类中都存在相同的方法name(),编译器是不知道对象的类型的,但方法调用机制能够自己去调查,找到正确的方法主体。Java方法的执行主要采用动态绑定技术,在程序运行时,虚拟机将调用对象实际类型所限定的方法。

初始化

在Java虚拟机中严格的规定了,有且只有六种情况必须立即对类进行初始化

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行初始化,则需要先触发其初始化阶段。能够生成这四条指令的Java代码场景:
    • 使用new关键字实例化对象
    • 读取或者设置一个类型的静态字段时候(被final修饰、已在编译把结果放入常量池的静态字段除外)
    • 调用一个类型的静态方法
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行初始化,则需要先触发其初始化
  3. 当初始化类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  5. 当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getstatic、REF_putstatic、REF_invokestatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
  6. 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

如下用例:

1)被动引用例一:通过子类引用父类的静态字段,不会导致子类的初始化

/**
* 通过子类引用父类的静态字段,不会导致子类的初始化
**/
public class SuperClass{
	public static int value = 123;
	static{
		System.out.println("SuperClass init!");
	}
}
public class SubClass{
	static{
		System.out.println("SubClass init!");
	}
}
/**
* 非主动使用类字段演示
*/
public class NotInitialization{
	public static void main(String[] args){
		System.out.println(SubClass.value);    // SuperClass init!
	}
}

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,这点取决于虚拟机的具体实现。对于 Sun HotSpot 虚拟机来说,可通过 -XX:+TraceClassLoading 参数观察到此操作会导致子类的加载(这里的加载指的是将二进制字节流读取到内存中的过程,而不是类加载。前者是后者的一个阶段)。

(2)被动引用例二:通过数组定义来引用类,不会触发此类的初始化

/**
* 通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization{
	public static void main(String[] args){
			SuperClass[] sca = new SuperClass[10];
	}
}

这边没有输出 "SuperClass init!",说明没有触发 SuperClass 的初始化阶段。但是这段代码里面触发了另外一个名为 “[Lxxx.xxx.SuperClass”的类的初始化阶段,对于用户代码来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于 java.lang.Object 的子类,创建动作由字节码指令 newarray 触发。
这个类代表可一个元素类型为 xxx.xxx.SuperClass 的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为 public 的 length 属性和 clone() 方法)都实现在这个类里。 Java 语言中对数组的访问比 C/C++ 更加安全,是因为这个类封装了数组元素的访问方法,而 C/C++ 直接是数组指针的移动。在 Java 语言中,当检查到发生数组越界时,会抛出 ArrayIndexOutOfBoundsException 异常。

(3)被动引用例三:常量在编一阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

/**
* 常量在编一阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
**/
public class ConstClass {
	public static final String HELLO_WORLD = "hello world";
	static {
		System.out.println("ConstClass init!");
	}
}
/**
* 非主动使用类字段演示
**/
public class Test{
	public static void main(String[] args) {
		System.out.println(ConstClass.HELLO_WORLD);
	}
}

上述代码运行之后,也没有输出 "ConstClass init!",这是因为虽然在 Java 源码中引用了 ConstClass 类中的常量 HELLO_WORLD,但其实在编译阶段通过常量传播优化,已经将此常量的值 "hello world 存储到了 Test 类的常量池中,以后 Test 对常量 ConstClass.HELLO_WORLD 的引用实际都被转化为 Test 对自身常量池的引用了。也就是说,实际上 Test 的 class 文件中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 之后就不存在联系了。

接口的加载过程与类加载过程稍有一些不同,针对接口需要做一些特殊说明:接口也有初始化过程,这点与类是一直的,上面的代码都是用静态语句块来输出初始化信息的,而接口中不能使用静态语句块,但编译器仍然会为接口生成 "<clinit>()"类构造器,用于初始化接口中所定义的成员变量。接口和类真正有所区别的是千米昂讲述的5中场景中的第三种:当一个类在初始化时,要求其父类全部都已经初始化了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类加载过程

加载

加载是类加载过程的一阶段,将class字节码加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。

加载阶段是开发人员可控制的,可以自定义类加载器去控制字节流的获取方式。

对于数组而言,数组类本身是不通过类加载器创建,它是有Java虚拟机直接在内存中动态构造出来的。但是还是靠类加载器来完成加载。遵循以下规则:

  • 数组的组件是引用类型,那就递归采用本节中定义的加载过程去加载这个组件
  • 数组的组件不是引用类型,Java虚拟机将会把数组C标记为与引导类加载器关联
  • 数组类的可访问性与他的组件类型的可访问性一致,如果组件不是引用类型,默认为public,可以被所有类和接口访问到
验证

? 确保Class文件的字节流中包含的信息符合Java虚拟机规范的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面四个阶段的校验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

文件格式验证

? 验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理。

  • 是否已魔数0xCAFEBABE开头
  • 主、次版本号是否在当前Java虚拟机接收范围之内
  • 常量池的常量中是否有不被支持的常量类型
  • 指向常量的各种索引值中是否有指向不存在的常量或者不符合类型的常量等等

上面只是简单的举例,该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。

元数据验证

主要是对数据类型校验,字节码描述的信息进行语义分析,保证其描述的信息符合Java虚拟机的要求。比如:

  1. 这个类是否有父类(所有的类都一定有一个父类,Object类)
  2. 这个类的父类是否继承了不允许被继承的类
  3. 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法

还有很多其他的等等,简单的理解就是验证类的编写是否符合规范

字节码验证

主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。比如:

  1. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
  2. 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
  3. 保证方法体中的类型转换总是有效的

在JDK6之后的Javac编译器和Java虚拟机里进行了一项联合优化,把尽可能多的校验辅助措施都放在了Javac编译器中进行。具体做法是给方法体Code属性表中新增了一项名为“StackMapTable”属性。

符号引用

校验行为发生在虚拟机将符号引用转化为直接引用的时候,可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配校验,简单的来说:该类是否缺少或者被禁止访问它的依赖的某些外部类、方法、字段等资源。通过校验的类容:

  1. 符号引用中通过字符串描述的全限定名是否能找到类
  2. 在指定类是否哦存在符合方法的字段描述符及简单名称所描述的方法和字段
  3. 符号引用中的类、字段、方法的可访问性是否可被当前类访问

符号引用主要的目的确保解析行为能正常执行,如果无妨通过符号引用验证,将抛出java.lang.IncompatibleClassChangeError异常

准备

准备阶段为类中定义的静态变量分配内存并设置类变量初始值的阶段。从概念上来讲,这些变量所使用的内存都应当在方法区中进行分配,方法区本身是一个逻辑概念的。在JDK7之前,hotspot使用永久代来实现方法区。但是在JDK8之后,类变量则会随着Class对象存放在Java堆中。

比如

public static int value = 123

这个在准备阶段只是将value值初始化为0,而不是123,真正的赋值实在初始化阶段

但是也是存在特情况,在类字段的字段属性表中存在ConstanVlaue属性,final来修饰,那么就是直接赋值

解析

将常量池内的符号引用替换为直接引用的过程。

符号引用:一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用:可以直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄

解析主要是针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符

类或接口的解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的引用,那虚拟机完成整个解析过程需要以下3个步骤:
(1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。
(2)如果C是一个数组类型,并且数组的元素类型为对象,那将会按照第1点的规则加载数组元素类型。
(3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具有对C的访问权限。如果发现不具备访问权限,则抛出java.lang.IllegalAccessError异常。

字段解析

首先解析字段表内class_index项中索引的CONSTANT_Class_info符号引用,也就是字段所属的类或接口的符号引用,如果解析完成,将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。
(1)如果C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(2)否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(3)否则,如果C 不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(4)否则,查找失败,抛出java.lang.NoSuchFieldError异常。

如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。

如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己的父类或多个接口中出现,那编译器可能拒绝编译,并提示”The field xxx is ambiguous”。

类方法解析

首先解析类方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个类方法所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续类方法的搜索。
(1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C 是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
(2)如果通过了第一步,在类C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(4)否则,在类C实现的接口列表以及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在相匹配的方法,说明类C是一个抽象类这时查找结束,抛出java.lang.AbstractMethodError异常。
(5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
最后,如果查找成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备此方法的访问权限,则抛出java.lang.IllegalAccessError异常。

接口方法解析

首先解析接口方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个接口方法所属的接口用C表示,虚拟机规范要求按照如下步骤对C进行后续接口方法的搜索。
(1)与类解析方法不同,如果在接口方法表中发现class_index中的索引C是个类而不是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
(2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(3)否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(4)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
由于接口中所有的方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常。

初始化

类初始化阶段是类加载过程的最后一步,到了这个阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

()方法是由编译器自动收集类中的所有变量类的赋值动作和静态语句块(static{})中的语句合并产生的。

顺序:

静态变量---》静态代码块 同时父类的静态语句块要优先于子类

初始化顺序:

单个类:

  1. 静态属性初始化
  2. 静态代码块
  3. 普通属性初始化
  4. 普通代码块
  5. 构造器
public class InitTest {

	//静态属性
	public static  int value = getFiled();

	//普通属性
	public String str = getOrdinaryFiled();

	//静态代码块
	static {
		System.out.println("static code");
	}

	//普通代码块
	{
		System.out.println("ordinary code");
	}

	public InitTest() {
		System.out.println("constructor init");
	}

	public static int getFiled(){
		System.out.println("static filed");
		return 123;
	}

	public String getOrdinaryFiled(){
		System.out.println("ordinary filed");
		return "string";
	}

	public static void main(String[] args) {
		new InitTest();
	}
}

//执行后的结果
static filed
static code
ordinary filed
ordinary code
constructor init

如果继承了父类,子类初始化的顺序:

  1. 父类静态变量
  2. 父类静态代码块
  3. 子类静态变量
  4. 子类静态代码块
  5. 父类普通变量
  6. 父类普通代码块
  7. 父类构造函数
  8. 子类普通变量
  9. 子类普通代码块
  10. 子类构造函数

类加载器

类加载器负责类的加载,其为所有被载入内存中的类生成一个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开始使用如下三种类加载器:

1)根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

下面程序可以获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:

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());
		}
	}
}

运行结果:

![image-20200822153929060](技术图片

2)扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在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对象。
双亲委派机制

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个类委派给父类加载器去完成,每一个层次的类加载器都是如此,所以在最后所有的类加载请求最终都应该传送到最顶层的情动类加载器钟,只有当父类加载器反馈自己完成完成这个加载请求时,那么子类加载器才会尝试自己去完成加载。

源码
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 {
                    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;
        }
    }
作用

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

虚拟机类加载和双亲委派机制

标签:ini   防止   bee   alac   inf   gets   it!   直接   使用   

原文地址:https://www.cnblogs.com/JackQiang/p/13598255.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!