JVM学习专栏:JVM学习
第7章 虚拟机类加载机制
这一章花费了我好久的时间才整理出来的,看书和看视频结合起来一起整理,所以内容和干货有点多,如果有不正确的地方欢迎各位指正!
目录
(五)线程上下文类加载器(Context ClassLoader)
线程上下文类加载器(Context ClassLoader)
(一)概述
代码编译的结果从本地机器码转变为字节码,一个Java源程序,要先经过javac编译,获得.class文件,之后在用java,使用JVM,将类加载到内存,并处理后才可执行。
Java虚拟机把描述类的数据从Class文件加载到内存,并进行连接和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
区分:类加载和类的加载(类初始化和类的初始化)
类加载和类初始化是一样的,都是指类加载机制的整个过程。
而类的加载和类的初始化都是类加载(类初始化)中的某阶段
Java中,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销, 但是却为Java应用提供了极高的扩展性和灵活性,Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
当系统出现以下几种情况时,JVM进程将被终止
1、程序运行到了最后正常结束
2、程序运行到使用System.exit() 或 Runtime.getRuntime.exit() 代码处结束程序
3、程序执行过程中遇到未捕获的异常或错误而结束
4、程序所在的平台强制结束JVM进程(比如操作系统出现错误导致JVM停止)
Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期为:
- 加载 (Loading)
- 连接(Linking)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化 (Initialization)
- 使用(Using)
- 卸载(Unloading)
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,不能改变,但是解析就不一定,它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定 / 晚期绑定 / 迟绑定)。
上面说的顺序确定,不是说上一阶段完全执行完才可以进行下一阶段,而是说阶段的开始时间是有顺序的。阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
(二)类加载的几个过程
1、加载
通过一个类的全限定名来获取定义此类的二进制字节流(class文件),将class文件字节码加载内存中,并让这些静态数据转换成方法区中运行时的数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的数据的访问入口。
加载的class文件来源:
- 本地文件系统加载class文件(最常见)
- zip、jar包加载class文件(JDBC导入的jar包……)
- 通过网络加载class文件
- 动态代理,运行期才生成,编译时不生成
- 如:ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
- 由其他文件生成(JSP动态编译生成class文件)
- 从数据库、机密文件中读取
- ……
类加载器通常无须等到“首次使用”该类时才加载该类,JVM允许系统预先加载某些类(但是不会进行相关的初始化操作,而且即使加载的这个类存在错误或缺失,也只有等到这个类被程序首次主动使用的时候---即初始化,才会报告错误LinkageError)
类的加载的过程由类加载器完成,(除了数组类之外,因为数组类的class对象是JVM根据需要自动在运行期创建的)。
对于数组类型的加载:
- 如果数组类型是引用类型,则先加载此引用类型。此数组将被标识在加载该引用类型的类加载器的名称空间上(类与类加载器一起确定Class对象的唯一性)
- 如果数组类型是基本数据类型,Java虚拟机将会把数组标记为与引导类加载器(根类加载器)关联。
数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的 可访问性将默认为public
2、连接
连接阶段负责把类的二进制数据合并到JVM的运行状态之中。分为以下三阶段:
- 验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致
- 文件格式验证、元数据验证、字节码验证、符号引用验证
- 准备:为类的静态变量分配内存,并设置默认初始值(并不是初始化)
- int的初始值为0,boolean的初始值为false……
- 解析:将常量池中类的二进制数据中的符号引用替换成直接引用
- 符号引用:
- 一组用来来描述所引用的目标的符号
- 符号引用与虚拟机实现的内存布局无关
- 引用的目标并不一定已经加载到内存中
- 直接引用:
- 是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
- 直接引用与虚拟机实现的内存布局相关
- 引用的目标已经在内存中
- 符号引用:
3、初始化
初始化阶段是执行类构造器<clinit>()方法的过程。主要是对静态变量进行初始化(如若没有指定初始值,将采用默认初始值)。
注意:不是静态变量的赋值,并不是在初始化期间实现赋值,而是初始化后的操作。
<clinit>()并不是在Java代码中直接编写的构造器,它是Javac编译器的自动生成物。是由编译器自动收集类中的所有静态变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,顺序和源文件的顺序一致。
//静态语句块中只能访问到 定义 在静态语句块 之前 的变量,
//定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
public class Test {
static {
i = 0; // 给变量复制可以正常编译通过
System.out.print(i); // 这句编译器会报错
}
static int i = 1;
}
如果一个类或接口中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
JVM会保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,多线程去初始化一个类,只有一个线程的<clinit>()方法执行,其他的阻塞。
特别注意:其他线程虽然会被阻塞,但是当一个线程执行完毕<clinit>后,其他线程唤醒,却不会再次进入<clinit>()方法。因为同一个类加载器下,一个类型只会被初始化一次。即:初始化语句只执行一次,再次调用的时候不再执行初始化语句
(1)类的初始化的时机
当程序要主动使用某个类的时候,如果该类还没有加载到内存中(即首次主动使用这个类),虚拟机就会对这个类进行初始化(当然首先要完成加载和连接)
类只需初始化一次,一但类初始化过了,之后若在引用该类,则直接调用内存中的Class对象。
只有类主动引用才会进行类的初始化,类的被动引用不会发生类的初始化。
①类的初始化的几种时机(类的主动引用):(此类还没初始化过,即首次主动使用)
- 使用new操作符调用类的构造器来创建类的实例
- 调用类的类方法(静态方法)
- 读取或设置类或者接口的类变量(静态变量)
- 使用反射的方式来强制创建类或者接口对应的java.lang.Class对象。
- 如:Class.forName("A")。如果系统还未初始化A类,A类将会被初始化,并返回对应的Class对象。
- 子类初始化,其父类也会被初始化(且父类优先初始化)
- 注意:此规则不适用于接口。在下面(3)中说明
- JVM启动时,优先初始化主类(包含main方法的那个类)
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化
②类的被动引用不会触发类的初始化,下面举几个例子
- 通过子类引用父类的静态变量,不会导致子类被初始化
public class ClassLoad {
public static void main(String[] args) {
//注意是通过子类.父类类变量,不是直接父类.类变量(虽然结果一样,不过原理不同)
System.out.println(Child.pStr);
}
}
class Child extends Parent {
public static String cStr = "child";
static {
System.out.println("Child类初始化");
}
}
class Parent{
public static String pStr = "parent";
static {
System.out.println("Parent类初始化");
}
}
//结果
Parent类初始化
parent
- 通过数组定义类引用类,不会导致该类的初始化(使用时会加载,上面加载部分有说到)
- 数组的类型是JVM在运行期动态生成的,一维数组表示为:[L类的权限定名。 二维:[[L类的权限定名
public class NotInitialization {
public static void main(String[] args) {
MyClass[] sca = new MyClass[10];
}
}
public MyClass{
static {
System.out.println("MyClass init!");
}
}
//不会初始化MyClass这个类(即不会打印MyClass init!)
//类型为:[Lcom.test.MyClass
- 引用常量(即final型的静态变量)--以a为例
- 如果a的值在编译时就可以确定下来的,那么a相当于“宏变量”,在编译阶段就已经存入调用a的方法所在类(假设为B)的常量池中,本质上没有直接引用到定义此常量的类(假设为A)。即使程序使用了a,也不会导致该类被初始化。
public static final String pStr = "123";
- 神奇事情:如果类B编译完成后,常量a就存放到B的常量池中。此时如果删除掉A编译后的class文件,再运行B,依旧可以获取到常量a
- 如果不能在编译时确定下来,此时如果通过访问a,会导致定义a的类A被初始化。(相当于主动引用类A)
public static final String pStr = UUID.randomUUID.toString();
- 如果a的值在编译时就可以确定下来的,那么a相当于“宏变量”,在编译阶段就已经存入调用a的方法所在类(假设为B)的常量池中,本质上没有直接引用到定义此常量的类(假设为A)。即使程序使用了a,也不会导致该类被初始化。
这里说的被动引用的类不会被初始化,并不是指类初始化全过程,而是指类的初始化。也就是说此类不会被初始化,但是有可能会被加载和连接。(不同虚拟机处理方式不同). ---这里可以证明上面说在加载过程中提到的 “类加载器通常无须等到首次使用该类时才加载该类”。可以通过VM参数设置观察是否会被加载:
-XX:+TraceClassLoading:追踪打印类的加载信息
//上面子类引用父类的程序实验,打印结果说明,子类不会被初始化,但会被加载到内存
//打印信息如下
……
[Loaded jvm.classLoad.Parent from file:/E:/workspace_idea/jvm/out/production/jvm/]
[Loaded jvm.classLoad.Child from file:/E:/workspace_idea/jvm/out/production/jvm/]
Parent类初始化
parent
……
(2)JVM初始化一个类的步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 此规则不适用于接口,在下面(3)中说明
- 假如类中有初始化语句,则系统从上到下依次执行这些初始化语句。
按照上面的步骤可以推出:JVM最先初始化的总是java.lang.Object类。
(3)接口与类的初始化区别
接口中不能使 用“static{}”语句块,但编译器仍然会为接口生成“<clinit>()”类构造器,用于初始化接口中所定义的成员变量。
接口中定义的成员变量其实都是常量(默认被public static final修饰,至于是编译期常量还是运行期常量,则都有可能)
接口与类真正有所区别的是:
- 当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化父类。
- 同时,接口的实现类在初始化时也不会执行接口的<clinit>()方法,即接口也不会被初始化。
- 除了上面的两种,其他情况接口和类是相同的。
//子接口初始化,父接口不会初始化
public class ClassLoad {
public static void main(String[] args) {
System.out.println(Child.CHILDTHREAD);
}
}
interface Parent{
//由于接口不能定义static块,所以通过这种方法确定接口是否初始化
//其中publid static final是默认的,可以不写,这里为了清楚展示就写了
public static final Thread PARENTTHREAD = new Thread(){
{
System.out.println("父接口初始化");
}
};
}
interface Child extends Parent{
public static final Thread CHILDTHREAD = new Thread(){
{
System.out.println("子接口初始化");
}
};
}
//结果
//子接口初始化
//实现类初始化,但是接口不初始化
public class ClassLoad {
public static void main(String[] args) {
System.out.println(MyImpl.i);
}
}
interface MyInterface{
//由于接口不能定义static块,所以通过这种方法确定接口是否初始化
//其中publid static final是默认的,可以不写,这里为了清楚展示就写了
public static final Thread PARENTTHREAD = new Thread(){
{
System.out.println("接口初始化!");
}
};
}
class MyImpl implements MyInterface {
public static int i = 1;
static {
System.out.println("实现类初始化");
}
}
//结果
实现类初始化
1
(三)类加载器
在上面类的加载阶段,就需要类加载器来进行加载。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
名称空间中包含着该类加载器和其所有的父加载器所加载的类组成。(子类加载器名称空间包含父类加载器的名称空间)
在一个命名空间中的类,是看不到其子加载器中所加载的类的(父加载器看不到子加载器所加载的类,但是子加载器可以看到父加载器所加载的类),同样的也看不到没有直接父子关系的其他加载器所加载的类。也就是说在一个命名空间中的类,获取不了不在其命名空间中的其他类(xxx.class、xxx.getClass都会报错)
时
一个被加载的Class对象的名称:(包名、类名、类加载器实例名)(具有唯一性)
1、类加载器有下面两种:
- Java虚拟机自带的类加载器
- 根类加载器Bootstrap classLoader(引导类加载器、启动器类加载器)
- 扩展类加载器Extension ClassLoader
- 应用程序类加载器Application ClassLoader(系统类加载器System ClassLoader)
- 用户自己定义的类加载器User ClassLoader
2、类加载器的层次结构(树状结构):(双亲委派模型)
- 引导类加载器 / 根类加载器 / 启动器类加载器
- 扩展类加载器
- 应用程序类加载器 / 系统类加载器
- 自定义类加载器
- 应用程序类加载器 / 系统类加载器
- 扩展类加载器
注:
- 类加载器之间有父子关系,但不是通过继承来实现的,而是通过组合的形式。
- 组合其实就是把“父类”作为子类的全局变量组合到子类中,子类再进行操作
- 除根类加载器外,其他类加载器都有且只有一个父加载器(双亲委派模型要求)
- 除根类加载器外,其他类加载器都是继承java.lang.ClassLoader。
- 根类加载器是用C++来写的,且存在于JVM内部,其他的都是用java写的,存在于JVM外部
- 除了根类加载器外,其他类加载器其实也是一个java类,他们都是由根类加载器加载的,而根类加载器则是JVM内部自带的。
//自定义类加载器是由根类加载器加载的
ClassLoader.class.getClassLoader();
//扩展和程序类加载器也是由根类加载器加载的
//这两种类加载器是Launcher的静态内部类,但是不是public,不能直接访问
//但是Launcher如果是根类加载器加载,也可以说明情况
Launcher.class.getClassLoader();
(打印结果均为null --根类加载器)
3、各种类加载器
- 根类加载器:
- 负责加载Java核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路径下的内容)。按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载
- 是由原生代码也就是由JVM自身实现的,并不继承java.lang.ClassLoader。
- 可以加载扩展类和引用程序类加载器 ,并指定他们的父类加载器
- 扩展类加载器:
- 负责加载jre的扩展目录(%JAVA_HOME%/jre/lib/ext 或者 Java.ext.dirs系统属性指定的目录)中jar包的类(注意一定要是打包在jar包里面的class文件才会被加载,其他的类加载器则不需要)
- 只要把自己开发的类打包成jar文件,然后放入正确路径即可(如:d:/Java/jdk1.8.0_05/jre/lib/ext)
- 由sun.misc.Launcher$ExtClassLoader实现
- 其父类加载器为根类加载器
- 应用程序类加载器:
- 负责加载来自java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径。
- 一般来说,Java应用的类都是由它来完成加载的
- 由sun.misc.Launcher$AppClassLoader实现
- 可以通过class对象.getClassLoader()获取此类加载器
- 其父类加载器为扩展类加载器
- 自定义类加载器:
- 用户可以制定类的加载方式,需要继承java.lang.ClassLoader
- 如果没有特别指定,其父类加载器是应用程序类加载器
- 如果要让自定义的类加载器运作,不仅需要继承和重写父类方法,还要注意class文件所放的位置。比如如果默认父类加载器是程序类加载器,那么就不能把class文件放在程序类加载器所查找class文件的路径下,否则由于双亲委派机制,程序类加载器在其路径下找到了相应的class文件,就直接加载了,自定义的类加载器就不会起作用了。
- 定义加载器时,重写findClass方法,方法内调用了loadClassData方法和defineClass方法,返回class对象
- loadClassData方法是自定义的,实现将指定的class文件读取到byte数组中并返回
- defineClass方法可以直接引用父类的方法,实现将byte数组转换成Class对象
- 使用此加载器时,先new出此自定义类加载器对象,再调用loadClass方法,传入要加载的Class文件的全限定名,就可以返回相应的Class对象
- loadClass(String name):加载名称为name的类,返回类的class对象。此方法内部依次调用了下面几个方法:
- findLoadedClass:如果是已加载的类,就直接返回此类的class对象
- loadClass:调用父类的loadClass方法(递归)
- findClass:找到相应的class类对象(在此方法中进行类的加载)。
- loadClass(String name):加载名称为name的类,返回类的class对象。此方法内部依次调用了下面几个方法:
4、两种机制
- 类缓存机制
- 一旦某个类被加载到类加载器中,他将维持加载(缓存)一点时间,且同一个类不会再次被载入。(JVM垃圾收集器可以回收这些Class对象)
- 怎样才算是“同一个类”?一个加载到JVM的类有唯一的标识:(类名、包名、类加载器实例名)。比如在pg包中有一个Person类被ClassLoader的实例cl负责加载,则对应的CLass对象在JVM中表示为(Person、pg、cl)。这意味着:同一个类被不同加载器实例加载,生成的CLass对象是不同的、是互不兼容的。
- 双亲委派机制 (双亲委托机制)
- 一种代理模式,就是某个类加载器收到加载类的请求时,将加载任务一层一层向上委托,直到根类加载器。在从根类加载器判断能否加载,能的话就交给它来加载,不能则向下传达,如果都不行,则最后才有这个类加载器加载类。(注意一定是先从根类加载器开始尝试加载)
- 双亲委派机制是为了保证Java核心库rt.jar的类型安全,保证不会出现用户修改核心类,比如java.lang.Object类,即使是自己定义了Object类也不会被加载。因为无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
- 注意:并不是所有的类加载器都采用双亲委派机制。(tomcat服务器中的类加载恰好和双亲委派机制相反,是先尝试加载,不行才向上委托)
(四)获取ClassLoader的途径
1、获取当前类的ClassLoader
//clazz为类的class对象(obj.getClass()可获取)
clazz.getClassLoader();
2、获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader(); //结果是appClassLoader
3、获取系统ClassLoader
ClassLoader.getSystemClassLoader();
4、获取调用者的ClassLoader
DriverManager.getCallerClassLoader();
5、获取父类加载器
loader对象.getParent();
(五)线程上下文类加载器(Context ClassLoader)
//小区分一下:
Thread.currentThread().getContextClassLoader(); //结果是appClassLoader
Thread.class.getClassLoader(); //结果是null(bootstrap),Thread类在rt.jar中
当前类加载器(Current ClassLoader)
每个类都会使用加载自身的类加载器去加载它所依赖的类,前提是所依赖的类尚未被加载。(比如:ClassA中引用了ClassB,而ClassB还未被加载,则加载A的类加载器就会去加载B)
线程上下文类加载器(Context ClassLoader)
在JDK1.2引入, 在线程运行中的代码可以通过该类加载器来加载类与资源。
Thread类中:
- setContextClassLoader(ClassLoader cl):设置上下文类加载器
- 如果没有调用此方法设置,则线程将继承其父线程的上下文类加载器
- Java应用运行时的初始线程的上下文类加载器是系统类加载器(App…)
- 如果有设置成了其他类加载器,则需要在使用过后将其还原成原本的loader
//一般使用流程:获取--(设置)--使用--还原
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
Thread.currentThread().setContextClassLoader(myClassLoader);
myMethod(); //其中调用了getContextClassLoader()方法,再进行相关类加载操作
} finally{
Thread.currentThread().setContextClassLoader(classLoader);
}
- getContextClassLoader():获取上下文类加载器
作用(重要性、引入原因):
对于SPI(Service Provider Interface,服务提供者接口),是面向接口编程的,服务规则由JDK规定,在JRE的核心API中提供了各服务相应的接口和规范,而接口的具体实现则交由给厂商自己开发。
这样一来,依靠双亲委派机制有着致命的缺陷:服务接口是由根类加载器(BootStrap)加载的,而接口的SPI实现类通过外部jar包引入,是被系统类加载器(App)加载的。服务接口需要引用到SPI实现类,但接口被BootStrap加载,无法再向上进行委派,所以就只能尝试由BootStrap加载,自然地,SPI实现类的jar包不在它的加载路径下(而在App类加载器能够加载的路径下),所以加载失败。
而且又由于双亲委派机制的单向委派出现了名称空间不同的问题,BootStrap加载的服务接口无法看到被App加载的SPI实现类,同样地引用不了SPI实现类。传统的双亲委托机制模型大失败。
比如:引入jdbc
其中的Connection和Statement是rt.jar中java.sql类的两个重要接口,他们都是由根类加载器进行加载的。
而他们的实现类有不同厂商各自实现,需要引入相应的jdbc的jar包,这些jar包则交由给系统类加载器加载。
结果就是接口看不到实现类,实现类可以看到接口。导致实现类无法使用。
这时候就需要引入上下文类加载器了。父类加载器(在上面的例子就是BootStrap)可以通过Thread.currentThread().getContextClassLoader()得到上下文加载器(在上面例子则是得到App)。这样,父类加载器可以通过这个上下文类加载器,去加载所需要的SPI实现类(这样也符合jar包实现类交给App加载)。
当然了,不仅适合有父子关系的类加载器实现 “父类加载器加载子类加载器所加载的类”。没有直接父子关系的类加载器之间也可以。
实际上就是打破了双亲委派模型,逆向使用类加载器。
在JDK6中提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,才算给SPI的加载提供了一种相对合理的解决方案。(《深入理解Java虚拟机》中认为直接打破的方式不太优雅……)
(六)java.util.ServiceLoader介绍
JDK1.6的时候才引入的,是针对SPI设计用来加载服务地具体实现类的。
在服务提供者(即SPI实现类jar包内)在META-INF/services路径下,存放着配置了相应类的全限定类名列表的配置文件,在这个配置文件中的类,都会被ServiceLoader扫描到,进行加载。加载后会放在Iterator中,可通过iterator()方法获取。
//JDBC测试,先导入jdbc的连接jar包
public class Test{
public static void main(String[] args) {
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = loader.iterator();
while(iterator.hasNext()){
Driver driver = iterator.next();
System.out.println("driver: " + driver.getClass()
+ " loader: " + driver.getClass().getClassLoader());
}
System.out.println("当前线程上下文类加载器:" + Thread.currentThread().getContextClassLoader());
System.out.println("ServiceLoader的类加载器:" + loader.getClass().getClassLoader());
}
}
(七)类的卸载
一个类何时结束生命周期,取决于代表他的Class对象何时结束生命周期
注意:
由JVM自带的类加载器(根类、扩展类、系统类)所加载的类,在虚拟机的生命周期中,始终不会被卸载。
由用户自定义的类加载器所加载的类是可以被卸载的。