Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就被称为虚拟机的类加载机制。
特点:与编译时进行连接的语言不同,Java语言的类型加载、连接和初始化过程都在程序运行期间完成。这样做的会让类加载时稍微增加一些性能开销,但好处是提供了极高的扩展性和灵活性。例如提供了接口与实现的动态绑定(动态多态)。
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,他的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个阶段统称为连接(Linking)。顺序如下图所示:
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定,而解析阶段则不一定,它也可以发生在初始化之后,这是为了支持运行时绑定(动态绑定或者晚期绑定)。
类加载过程的第一阶段“加载”的时机,《Java虚拟机规范》没有指定,可以由虚拟机自由实现。但是初始化阶段规定了有且只有六种情况,如果类还没有初始化需要进行初始化(而加载、验证、准备自然需要在此之前):
需要注意的是类与接口在第三种情况下有所不同,当一个类初始化时必须要求他的所有父类都必须初始化,而初始化一个接口时,并不要求其父接口全部都完成初始化,只有用到其父接口时,才进行初始化。
上面这六种场景的行为对一个类型的主动引用。除此之外所有引用类型的方式都不会触发初始化,被称为被动引用。下面使用三个例子来说明何为被动引用:
1、通过子类引用父类的静态字段,不会导致子类的初始化
代码:
package jvm.classloading;
/**
* 被动使用字段演示一:
* 通过子类引用父类的静态字段,不会导致子类初始化
*/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
class NotInitially{
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
结果:
从上面的结果我们可以看出对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
2、通过数组定义来引用类,不会触发初始化
代码:
/**
* 被动使用字段演示二:
* 通过数组定义来引用类,不会触发初始化
*/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class NotInitially{
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];
}
}
结果:
什么都没有输出,说明没有初始化SuperClass。但其实这段代码里初始化另外一个由虚拟机自动生成的、直接继承与java.lang.Object的子类,创建动作由字节码指令newarray触发。这个类中实现了数组应有的属性和方法(length属性和clone方法等)。
3、常量在编译阶段会存储调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发该类的初始化
代码:
package jvm.classloading;
/**
* 被动使用字段演示二:
* 常量在编译阶段会存储调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发该类的初始化代码
*/
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String s = "Hello world";
}
class NotInitialization{
public static void main(String[] args) {
System.out.println(ConstClass.s);
}
}
结果:
没有出现“ConstClass init!”,说明没有初始化ConstClass 。这是因为在编译期间已经将常量"Hello world"的值直接存储到了NotInitialization的常量池中。此时NotInitialization已经没有了对ConstClass 的引用。通过下面的ConstClass.class
的内容也可以看出:
讲完了类加载的时机,下面对类加载的每个过程进行详细介绍:
在加载阶段,Java虚拟机需要完成以下三件事情:
在第一步中,《Java虚拟机规范》并没指明二进制字节流文件必须从Class文件获取,由此为Java虚拟机实现加载提供了很大的灵活度。后来很多技术都建立在这个基础之上,例如:
同时在第二步中,方法区中数据存储格式完全由虚拟机实现自行定义。
加载阶段和连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的。但是这个动作属于连接阶段,两个阶段仍保持固定的开始先后顺序
加载阶段的具体实现:
将字节码载入到方法区中,内部采用C++的instanceKlass描述Java类,他的重要field有:
其中instanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但是_java_mirror
指向存储在堆中的class类型。
验证是连接的第一步,这个阶段的目的是保证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证阶段占据类加载工作量相当大的比重。验证阶段大致上分为四个阶段:文件格式验证、元数据验证、字节码验证和符号引用验证。
1、文件格式验证
第一阶段要验证二进制字节流是否符合Class文件格式的规范,保证输入的字节流能正确地解析并存储到方法区,格式上符合描述一个Java类型信息的要求。这一阶段可能包含以下验证点:
上面仅仅是文件格式验证的一部分。同时这个阶段的验证是基于二进制字节流的,只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中存储,之后的三个阶段都是基于方法去的存储结构进行的。
2、元数据验证
第二阶段元数据验证的主要目的是对类的元数据信息进行语义校验,保证其描述信息符合《Java虚拟机规范》的要求,这个阶段的验证点可能包括:
3、字节码验证
第三阶段是最复杂的阶段,第二阶段元数据信息中的数据类型校验完毕之后,这一阶段就是对的**方法体(Class文件中的Code属性)**进行校验分析,主要目的是通过数据流分析和控制流分析,确定程序语义是否合法,符合逻辑。保证验证的方法不会做出危害虚拟机完全的行为,例如:
由于该阶段过于耗时,在JDK6之后Javac编译器和Java虚拟机进行联合优化,将尽可能多的校验辅助措施转移到Javac编译器中进行。具体做法是给方法体Code属性的属性表新增一项名为“StackMapTable”的新属性,它描述了方法体所有基本块(Basic Block,值按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,Java虚拟机就不需要根据程序推导出这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可。
4、符号引用验证
符合引用验证可以看作是对类自身(常量池中的各种符号引用)以外的各类信息进行匹配行的校验,也就是:该类是否缺少或者禁止访问它依赖的某些外部类、方法、字段等资源。可能包括:
准备阶段是正式为**类中定义的变量(即静态变量,被static修饰的变量)**分配内存并设置类变量的默认值。这个阶段有几个值得注意地方:
public static int value = 123;
此时value的值为0而不是123。对于静态常量,例如public static final int value = 123;
此时就会赋值为123。解析阶段是Java虚拟机将常量池内的符号引用转化为直接引用的过程,下面介绍下两者的含义:
到底是在类被加载器加载时就对常量池中的符号进行引用解析,还是等到一个符号引用将要被使用之前进行解析,根据虚拟机实现可以自行决定。另外解析阶段也会对方法或者字段的可访问性进行验证。
解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行,分别对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethonType_info、CONSTANT_MethonHandle_info、CONSTANT_Dynamic_info、CONSTANT_InvokeDynamic_info这8种常量类型。下面讲解前4种,之后讲解后4种:
1、类或者接口信息
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或者接口C的直接引用,那么虚拟机完成这个过程需要3个步骤
[Ljava/lang/Integer
的形式,那将会按照第一点的规则加载数组元素类型。 如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer",接着由虚拟机生成一个代表该数组维度和元素的数组对象。2、字段解析
要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项种索引的CONSTANT_Class_info符号引用进行解析,也就是对字段符号所在类或者接口,设其为C,接下来要按照如下步骤堆C进行后续字段的搜索:
如果成功查找到字段,之后还要对该字段进行权限验证。
3、方法解析
要解析一个未被解析过的方法符号引用,首先将会获取方法表内class_index项索引的方法所在类或者接口的符号引用,设其为C,接下来要按照如下步骤堆C进行后续字段的搜索:
最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.llegalAccessError异常。
4、接口方法解析
要解析一个未被解析过的接口方法符号引用,首先将会获取方法表内class_index项索引的接口方法所在类或者接口的符号引用,设其为C,接下来要按照如下步骤堆C进行后续字段的搜索:
进行准备阶段时,变量已经赋过一次初始零值,而在初始化阶段,则会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。初始化阶段就是执行类构造器
方法的过程:
()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序,静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问public class Test{
static{
i = 0; //给变量赋值可以正常编译通过
System.out.print(i); //这句编译器会提示“非法前向引用”
}
static int i = 1;
}
()
方法与类的构造器(即在虚拟机视角中的实例构造器()
方法)不同,它不需要显示的调用父类构造器,java虚拟机会保证在子类的()
方法执行之前,父类的()
方法已经被执行()
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作()
方法对于类或者接口,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器就不会给这个类生成()
方法()
方法。但是与类不同的是,执行接口的()
方法,不需要先执行父接口的()
方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化Java虚拟机必须保证一个类的()
方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只有一个线程会去执行这个类的()
方法,其他线程都会阻塞等待。这个过程中如果()
方法耗时很长,会导致多个线程阻塞,这种阻塞往往很隐蔽。总结初始化的发生时机:
1、会导致初始化的情况
2、不会导致初始化的情况
典型应用-编写懒惰初始化单例模式:
package jvm.classloading;
public class LazySingletonTest {
public static void main(String[] args) {
// Singleton singleton = new Singleton(); //不能直接new
Singleton singleton = Singleton.getInstance(); //通过懒惰单例获取对象
singleton.test();
}
}
class Singleton{
//私有化构造函数实现单例
private Singleton(){}
private static class LazyHolder{
private static final Singleton singleton = new Singleton();
static {
System.out.println(" lazy holder init!");
}
}
public static Singleton getInstance(){
//只有当调用此方法时,才会触发LazyHolder的初始化,从而创建Singleton对象
return LazyHolder.singleton;
}
public void test(){
System.out.println("test");
}
}
以上单例模式的特点
()
方法的线程安全“通过一个类的全限定类名来获取到描述该类的二进制字节流”,实现这个动作的代码就被称为类加载器。
对于任意一个类,都必须由加载它的类加载和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的命名空间。简单来说,就是即使两个类来自同一个Class文件,被统一虚拟机加载,只要它们的类加载器不同,那么这两个类必定不同。
介绍双亲委派之前,先来介绍以下三层类加载器:
\lib
目录,或者被-Xbootclasspath
参数所指定路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,如果名字不符合的类库即使放入lib目录中也不会被加载)类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器去处理,直接使用null代理即可\lib\ext
目录中,或者被java.ext.dirs系统变量指派的路径中所有的类库。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件上面图中展示的各种类加载器之间的层次关系被称为类加载器的双亲委派模型。其中类加载器之间的父子关系使用的是组合,而不是继承来实现的
双亲委派模型的工作原理:如果一个类加载器收到了类加载请求,他首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一次类加载器都是如此,因此所有的加载请求最后都应该传送到最顶层的启动类加载器中。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子类才会去尝试自己完成加载。
双亲委派模型的好处是Java中的类随着他的类加载器一起具备了一种带有优先级的层次关系,很好的解决了各个类加载器协作时基础类的一致性问题(越基础的类由越上层的加载器进行加载),防止了核心API库被随意篡改。
双亲委派模型的实现原理:双亲委派的实现代码只有10余行,全部集中在java.lang.ClassLoader的loadClass()方法中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检测这个类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//如果由上级,则委派给上级加载
c = parent.loadClass(name, false);
} else {
//如果没有上级(ExtClassLoader),则委派给BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类加载器抛出ClassNotFoundException异常
//说明父亲加载器无法完成加载请求
}
if (c == null) {
//在父类加载器无法加载时
//再调用本身的findClass方法进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上面代码的整个流程是:先检查请求加载的类型是否已经被加载过,若没有则调用父类加载器的loadClass请求,若父类加载器为空则默认使用启动加载器作为父加载器。假如父类加载器加载失败,抛出lassNotFoundException异常,才调用调用自己的findClass方法尝试进行加载。
1.第一次破坏
由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。
2.第二次破坏
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美。
如果基础类又要调用回用户的代码,那该么办?
一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务, 它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
3.第三次破坏
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。 OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。
JDBC例子
什么时候需要自定义类加载器:
步骤:
示例:
public class Load7 {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> c1 = classLoader.loadClass("MapImpl1");
Class<?> c2 = classLoader.loadClass("MapImpl1");
System.out.println(c1 == c2); //true
MyClassLoader classLoader2 = new MyClassLoader();
Class<?> c3 = classLoader2.loadClass("MapImpl1");
System.out.println(c1 == c3); //fasle
c1.newInstance();
}
}
//自定义类加载器
class MyClassLoader extends ClassLoader {
@Override // name 就是类名称
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "e:\\myclasspath\\" + name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}
FoundException {
String path = “e:\myclasspath\” + name + “.class”;
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}