JVM之类加载子系统与双亲委派机制
一、类加载子系统
1.类加载子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识,JVM并不是通过检查文件后缀是不是.class
来判断是否需要加载的,而是通过文件开头的特定文件标志即16进制CA TE BA BE;
2.加载后的Class类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
来一张经典的JVM内存结构图:其中类加载器的工作范围只限于下图的左半部分,不包含调用构造器实例化对象
- 定义: 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构
3.ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
4.如果调用构造器实例化对象,则其实例存放在堆区
功能细分
加载模块
1.通过一个类的全限定名获取定义此类的二进制字节流;
2.将这个字节流所代表的的静态存储结构转化为方法区的运行时数据;
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接模块分为三块,即验证、准备、解析
验证
1.目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
2.主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。
准备
1.为类变量分配内存并且设置该类变量的默认初始值,即零值;
2.这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
3.不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
解析
1.将常量池内的符号引用转换为直接引用的过程。
2.事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
3.符号引用就是一组符号来描述所引用的目标。符号应用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
4.解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
初始化模块,初始化阶段就是执行类构造器方法clinit()的过程
1.clinit()即“class or interface initialization method”,注意他并不是指构造器init()
2.此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
3.我们注意到如果没有静态变量c,那么字节码文件中就不会有clinit方法
java程序对类的使用方式分为:主动使用和被动使用,即是否调用了clinit()方法
主动使用在类加载系统中的第三阶段initialization(初始化模块),即初始化阶段调用了clinit()方法
而被动使用不会去调用
主动使用,分为七种情况
1.创建类的实例
2.访问某各类或接口的静态变量,或者对静态变量赋值
3.调用类的静态方法
4.反射 比如Class.forName(com.dsh.jvm.xxx)
5.初始化一个类的子类
6.java虚拟机启动时被标明为启动类的类
7.JDK 7 开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁
类加载器种类
即一个类只需被clinit一次,之后该类的内部信息就被存储在方法区。
- 作用:加载类文件,引用在栈中,具体实例在堆中
- 虚拟器自带三种类加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器(系统类加载器)
注意:
- Class loader有多种,可以说三个,也可以说是四个(第四个为自己定义的加载器,继承 ClassLoader),系统自带的三个分别为:
- 启动类加载器(Bootstrap) ,C++所写
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部
- 它用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载拓展类和应用程序类加载器,并指定为他们的父加载器,即ClassLoader
- 出于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类
- 扩展类加载器(Extension) ,Java所写
- java语言编写 ,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类,不是继承
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载
- 应用程序类加载器 (AppClassLoader)
- java语言编写, 由sun.misc.Launcher$AppClassLoader实现。
- 派生于ClassLoader类,不是继承
- 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
- 该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
用户自定义类加载器:
在日常Java应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,必要时可以自定义类加载器来定制类的加载方式。
为什么要自定义类加载器呢?
- 隔离加载类
- 修改类加载方式
- 扩展加载源
- 防止源码泄露
我们自己new的时候创建的是应用程序类加载器(AppClassLoader)。
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass = car1.getClass();
System.out.println(aClass.getClassLoader()); // 应用程序加载器 AppClassLoader
System.out.println(aClass.getClassLoader().getParent()); // 扩展类加载器 ExtClassLoader D:\jdk1.8\jre\lib\ext/*.jar
System.out.println(aClass.getClassLoader().getParent().getParent()); // null 1. 不存在, 2. java程序获取不到 D:\jdk1.8\jre\lib\rt.jar
}
注意:
- 如果是JDK自带的类(Object、String、ArrayList等),其使用的加载器是Bootstrap加载器;如果自己写的类,使用的是AppClassLoader加载器;Extension加载器是负责将把java更新的程序包的类加载进行
- 输出中,sun.misc.Launcher是JVM相关调用的入口程序
- Java加载器个数为3+1。前三个是系统自带的,用户可以定制类的加载方式,通过继承Java. lang. ClassLoader
JVM中表示两个class对象是否为同一个类
1.在jvm中表示两个class对象是否为同一个类存在的两个必要条件
- 类的完整类名必须一致,包括包名
- 即使类的完整类名一致,同时要求加载这个类的ClassLoader(指ClassLoader实例对象)必 须相同;是引导类加载器、还是定义类加载器
2.换句话说,在jvm中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的.
3.对类加载器的引用,JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类型由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证两个类型的加载器是相同的。
二、双亲委派机制
注意:
- 双亲委派机制:“我爸是李,有事找我爹”。
例如:需要用一个A.java这个类,首先去顶部Bootstrap根加载器去找,找得到你就用,找不到再下降一层,去Extension加载器去找,找得到就用,找不到再将一层,去AppClassLoader加载器去找,找得到就用,找不到就会报"CLASS NOT FOUND EXCEPTION"。
//测试加载器的加载顺序
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("hello world!");
}
}
/*
* output:
* 错误: 在类 java.lang.String 中找不到 main 方法
* */
上面代码是为了测试加载器的顺序:首先加载的是Bootstrap加载器,由于JVM中有java.lang.String这个类,所以会首先加载这个类,而不是自己写的类,而这个类中并无main方法,所以会报“在类 java.lang.String 中找不到 main 方法”。
这个问题就涉及到,如果有两个相同的类,那么java到底会用哪一个?如果使用用户自己定义的java.lang.String,那么别使用这个类的程序会去全部出错,所以,为了保证用户写的源代码不污染java出厂自带的源代码,而提供了一种“双亲委派”机制,保证“沙箱安全”。即先找到先使用。