类加载相关
1. 类装载器子系统
类装载器子系统负责查找并装载类型信息。其实Java虚拟机有两种类装载器:
- 系统装载器
- 用户自定义装载器
前者是Java虚拟机实现的一部分,后者则是Java程序的一部分。
1.1 系统装载器
Java 系统装载器主要有3种:
- 启动类装载器(bootstrap class loader):也叫根装载器。它用来加载 Java 的核心库,是用原生代码(HotSpot 虚拟机使用C++)来实现的,并不继承自java.lang.ClassLoader。负责将存放在\lib目录中的类库加载到虚拟机中。其无法被Java程序直接引用,无法直接获取。
- 扩展类装载器(extensions class loader):是启动类装载器的子类。它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类,即负责加载\lib\ext目录中的所有类库,开发者可以直接使用。
- 应用程序类加载器(Application ClassLoader):是扩展类装载器的子类。它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。默认情况下,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
名词解释 —— HotSpot 虚拟机
SunJDK和OpenJDK中自带的虚拟机,也是最被广泛使用的虚拟机。
一般说的启动类装载器加载器使用C++语言实现。这里只限于HotSpot,像MRP, Maxine等虚拟机, 整个虚拟机本身都是由Java编写的, 自然BootstrapClassLoader 也是由Java语言而不是C++实现的, 退一步讲, 除了HotSpot以外的其他两个高性能虚拟机JRockit和J9都有一个代表BootstrapClassLoader的java类存在, 但是关键方法的实现仍然是使用JNI回调到C, 不是C++的实现上, 这个BootstrapClassLoader的实例也无法被用户获取到。
1.2 用户自定义类装载器
开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类装载器,以满足一些特殊的需求。
1.3 双亲委派模型
我们的应用程序都是由4.1.1中的3种类加载器互相配合进行加载的, 如果有必要, 还可以加入自己定义的类加载器. 这些类加载器之间的关系是:1
2
3
4
5graph TD
boot(启动类装载器 bootstrap class loader) --> ext(扩展类装载器 extensions class loader)
ext --> app(应用程序类加载器 Application ClassLoader)
app --> user1(用户自定义类加载器 User ClassLoader)
app --> user2(用户自定义类加载器 User ClassLoader)
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器. 这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现, 而是都使用组合(Composition)关系来复用父加载器的代码。
类加载器的双亲委派模型在JDK1.2期间被引入并被广泛应用于之后几乎所有的java程序中,但它并非不是一个强制性的约束模型,而是java设计者推荐给开发者的一种类加载器实现方式.
双亲委派模型的工作过程是 :
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有的加载请求最都应该传送到顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围内没有找到所需的类)时, 子加载器才会尝试自己去加载。
使用双亲委派模型的好处:
- java类随着它的类加载器一起具备了一种带有优先级的层次关系. 例如类Java.lang.Object,它存放在rt.jar之中, 无论哪一个类加载器要加载这个类, 最终都是委派给处于模型最顶端的启动类加载器进行加载, 由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的classpath中,那系统中将会出现多个不同的Object类, java类型体系中最基础的行为也就无法保证, 应用程序也将会变得一片混乱。
- 实现非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中, 逻辑清晰。
- 从安全上考虑,有利于防止第三方往JVM中装载恶意的基础类。比如一个恶意的java.lang.String,由于双亲委派机制,基础类java.lang.String就会被往上请求加载,使用启动类装载器进行装载,从而避免恶意事件发生。
2 类加载机制
Class文件描述的各种信息,都需要加载到虚拟机后才能运行。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
在Java中类装载器把一个类装入JVM,有以下步骤:
- 加载: 查找并加载类的二进制数据
- 连接:
- 验证 确保被加载的类的正确性
- 准备 为类的静态变量分配内存,并将其初始化为默认值(int就是0 boolean就是 false 引用类型就是null)
- 解析 把类中的符号引用变为直接引用
- 初始化: 为类的静态变量赋予正确的初始值,只有在初始化阶段,赋值的=才发挥作用,且从上往下顺序执行。
理解起来应该没什么问题,那么
提问:在我们最开始编写java的时候总是javac命令编译成class字节码,java命令运行。如果java代码有什么问题,在javac的时候就会抛出问题,换句话说等我们连接class文件的时候它肯定是没问题的,那还验证什么呢?
答案:如果class的产生只能通过javac命令的话,那就没有任何问题了,可关键就是人们也可以手动产生class文件,所以验证这一步还是有用的。
2.1 加载
类的加载指的是通过一个类的全限定名来获取定义此类的二进制字节流,最一般的情况是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在Java 堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。也可以通过ZIP、JAR、WAR、网络(Applet)、JSP、或动态代理技术读取。
类的加载的最终产品是位于Java 堆区中的Class对象
名词解释 —— Class对象:
每个类也都有一个描述自己信息的东西,它就是Class对象。这个类的Class对象里就包含了这个类里面有几个属性,每个属性是什么类型,有什么方法,每个方法的参数都是什么,返回值都是什么等等。
如果有使用过反射类java.lang.Reflect中的对象与接口,对此应该不陌生。具体信息,参考JDK文档。
2.2 连接之验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外;是否继承了不允许继承的类(被final修饰的类)。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2.3 连接之准备
准备阶段是正式为类变量分配内存并设置类变量默认初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量。如:public static int a = 123;
会被赋值为0(零值),赋值为123是在之后的初始化阶段,另外,如果是public static final int a = 123;
则会赋值为123.
2.4 连接之解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
再论符号引用、直接引用
符号引用(Symbolic References):在class文件中以CONSTANT_Class_info,CONSTANT_Fieldref_info等类型的常量出现。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可,与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
直接引用(Direct References):直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
2.5 初始化
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主管计划去初始化类变量和其他资源
初始化时机:Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。即主动引用。
主动引用包含以下六种情况:
- 创建类的实例 (如在代码中new Person())
- 访问某个类或接口的静态变量,或者对该静态变量赋值 (Singlean.a=8)
- 调用类的静态方法 (Singleton.getInstance();)
- 反射(如Class.forName(“com.shengsiyuan.Test”)
- 初始化一个类的子类 (有father类,有child类,且child继承或实现father类。 )
- Java虚拟机启动时被标明为启动类的类(调用java启动命令 如Java Test)
除此之外的引用方法都不会引起初始化,称为被动引用,比如:
- 通过子类引用父类的静态字段,不会导致子类的初始化;
- 通过数组定义来引用类,不会导致此类的初始化;
- 常量在编译时期会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此调用该常量不会触发定义常量的类的初始化。
类构造器<clinit>()
是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,顺序由语句在源文件出现的顺序决定,静态语句只能访问到在其之前定义的变量,但可以赋值。父类的类构造器会保证在子类的类构造器之前执行。类构造器在多线程环境会被加锁、同步,同时只有一个线程会去执行这个类的类构造器,且在同一类加载器中类构造器只会被调用一次。
2.6 类装载的具体例子
1 | public class Singleton { |
请问:输出是什么?
解析:
在连接的准备阶段 k1,k2都是0,s是null
到了初始化阶段, 从上往下,s的初始化调用了构造函数,
k1,k2都从0变成了1,
继续往下,
k1没有被赋值还是1,
但是k2却被赋值为0了,
所以最后的结果就是:1 0
3 类实例创建过程
对比类加载过程,补充一下类实例的创建过程:
父子继承关系,先父类再子类。
父类的静态->子类的静态->父类的初始化块->父类的构造方法->子类的初始化块->子类的构造方法**
Copyright © 2018, GDUT CSCW back-end Kanarien, All Rights Reserved