类加载机制#
类加载器深入解析与阶段分解#
- 在Java代码中,类型的加载、连接与初始化过程中都是在程序 运行期间完成的。
- 提供了大量的灵活性、增加了更多的可能性。
- Java虚拟机与程序的生命周期。
- 在如下几种情况下,Java虚拟机将结束生命周期
- 执行了
System.exit()
方法 - 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 执行了
上述出现的名词概念详解
- 类型:表示的类型(类 class)的本身,而并不是通过类型new出来的对象。
- 运行期间:RunTime。
- 加载:最常见的-将已经存在与磁盘上的Class文件,加载到内存上面。
- 连接:确定好类与类之间的关联关系,引用之间的转换,是在这一步完成的。
- 初始化:类型中的静态的变量赋值。是在初始化阶段完的。
- 可能性:为有创意的开发人员,提供了扩展。
- 类和类加载器:类全部是由类加载器加载到JVM的。
类的加载、连接与初始化过程详解#
-
加载:查找并加载类的二进制数据
-
连接
-
验证阶段:确保被加载的类的正确性
-
准备阶段:为类的静态变量分配内存,并将其初始化为默认值
-
解析阶段:把类中的符号引用转换为直接引用。符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
-
-
初始化:为类的静态变量赋予正确的初始值。初始化阶段就是执行类构造器方法<clinit>()的过程。此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。构造器方法中的指令语句按源文件中出现的顺序执行。<clinit>()不同于类的构造器。(构造器是虚拟机视角下<init>())。若该类具有父类,jvm保证子类的clinit>()执行前,父类的clinit>()yi j
-
使用:我们平时使用的对象,操作,方法调用,等等都是使用阶段
-
卸载:类在卸载之后,就不能够继续new对象,平时开发很少接触到这个卸载阶段。比如-OSGI技术会使用到卸载
代码1:从代码理解概念
class Test{ public static int a = 1; } //我们程序中给定的是 public static int a = 1; //但是在加载过程中的步骤如下: 1. 加载阶段 编译文件为class文件,然后通过类加载,加载到JVM 2. 连接阶段 第一步(验证):确保Class类文件没问题 第二步(准备):先初始化为 a=0。(因为你int类型的初始值为0) 第三步(解析):将引用转换为直接引用 3. 初始化阶段: 通过此解析阶段,把1赋值为变量a 4. 使用阶段 我们平时使用的对象,操作,方法调用,等等都是使用阶段 5. 卸载阶段 类在卸载之后,就不能够继续new对象,平时开发很少接触到这个卸载阶段。比如-OSGI技术会使用到卸载
图解
常量的本质含义与反编译及助记符详解#
-
Java程序对类的使用方式分为两种
-
主动使用(七种)
-
创建类的实例-(new一个对象)
-
访问某个类或接口的静态变量,或者对该静态变量赋值-(对静态变量的赋值和取值)
-
调用类的静态方法 (和静态变量的含义相同,因为class的助记符是一样的 invoke static)
-
反射(如Class.forName("com.test.Test"))
-
初始化一个类的子类
Class Parent{} Class Child extends Parent() //初始化子类,也算主动使用 父类。
-
Java虚拟机启动时被标明为启动类的类(Java Test),等包含
main
方法的类 -
JDK1.7开始提供的动态语言支持
java.lang.invoke.MethodHandle实例的解析结果 REF_getStatic REF_putStatic REF_invokeStatic 三个句柄对应的类没有初始化,则初始化
-
-
被动使用
- 除了以上七种情况,其他使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化
-
-
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
-
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再内存中创建一个
java.lang.Class
对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构。- 反射的原理, -
通过Class获取类的全量信息就是如此。
-
加载
.class
文件的方式- 从本地系统中直接加载
- 通过网络下载
.class
文件 - 从zip,jar等归档文件中加载
.class
文件 - 从专有数据库中提取
.class
文件 - 将Java源文件动态编译为.class文件
- (在动态代理的时候会用到)运行期的时候回生成。在编译的时候不会用到
.jsp
,web开发的时候,jsp文件转换成servlet文件
上述出现的名词概念详解
- 助记符
- getstatic
- putstatic
- invokestatic
代码2:
从程序角度,去了解什么是对类的 主动使用,什么是被动使用
class MyParent1{
public static String str = "hello world";
static { //静态代码块,在初始化的时候回运行
System.out.println("MyParent1 static block");
}
}
class MyChild1 extends MyParent1{
public static String str2 = "welcome";
static { //静态代码块,在初始化的时候回运行
System.out.println("MyChild1 static block");
}
}
两个测试案例:对于初始化阶段的主动使用和被动使用的测试。
public class MyTest {
public static void main(String[] args) {
System.out.println(MyChild1.str);
}
}
//运行结果是: MyParent1 static block hello world
//为什么会是这种情况:
1. 对于静态字段来说,只有直接定义了该字段的类才会被初始化。
2. 当调用 MyChild1.str的时候,是对MyParent1这个类的主动使用。 并没有主动使用MyChild1。
3. 也应了前面的:所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
public class MyTest {
public static void main(String[] args) {
System.out.println(MyChild1.str2);
}
}
//运行结果是: MyParent1 static block MyChild1 static block welcome
//为什么会是这种情况?
1. 因为当一个类在初始化的时候,他全部的父类都要先进行初始化。
问题:MyChild1,没有被初始化,但是有没有被加载呢?
答案:即使没有完成类的初始化,也会去加载类。
使用以下虚拟机参数去观察日志。
-
虚拟机参数:
-XX:+TraceClassLoading
用于追踪类的加载信息,并打印出来。可以在VM options里面添加。运行的时候,会使用这个参数。
加载完之后的控制台信息如下: (如果参数写错的话,会提示错误。并给出相应正确的提示。)
从日志文件输出汇总,可以判断出:第一加载的类是
Object
类。 从日志上,看出:即使没有完成类的初始化,也会去加载类
并且其中
MyTest
类也被加载。因为被设定为启动类的类,会自动初始化。
JVM参数基本介绍
- JVM参数都是以
-XX:
开头的 -XX:+<option>
,表示开启Option选项-XX:-<option>
,表示关闭Option选项-XX:<option>=<value>
,表示将Option选项的值设置为value
使用代码去看,代码3:
package com.dawa.jvm.classloader;
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
class MyParent2{
public static final String str = "hello world";
//常亮 static
{
System.out.println("MyParent2 static block");
}
}
//输出结果为 hello world
//加 final 和 不加 final的区别很大。 静态代码块是否调用的区别。 解释:final,表示常量。是不可被改变的量。 编译阶段,常量会被存到调用常量的方法所在的类的常量池 当中。 本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。 这里指的是:将常量存放在了MyTest2的常量池中,之后Test2和MyParent2就没有任何关系了。 甚至,我们可以将MyParent2的class文件删除。
常量:在编译阶段,会被存入到调用常量的方法所在的类的常量池当中。
本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。
这里指的是:将常量存放在了MyTest2的常量池中,之后Test2和MyParent2就没有任何关系了。
甚至,我们可以将MyParent2的class文件删除。
反编译操作:javap
》javap -c com.dawa....MyTest2
反编译操作之后,会有助记符。
助记符
助记符取决于类型的不同, 助记符的实现在源码 :rk.jar 中有实现。Opcodes在
LDC.java
,ICONST.java
,SIPUSH.java
等都有相关的实现类
- ldc:表示将int,float或者是String类型的常量值从常量池中推送至栈顶
- bipush:表示将单字节(-128~127)的常量推送至栈顶
- Sipush:表示将一个段整形常量值(-32768-32767)推送至栈顶
- iconst_1:表示将int类型1推送至栈顶 (-1-5 都是 iconst_m1- iconst_5)
- anewawarry: 表示创建一个引用类型(如类,接口,数组)的数组,并将其引用值压至栈顶。
- newarray:表示创建一个指定的原始类型(如int,float,char等)的数组,并将其引用值压至栈顶。
课程代码保存
代码4:
结论:当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池当中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类初始化
package com.dawa.jvm.classloader;
import java.util.UUID;
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3 {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent3 static code ");
}
}
运行结果
MyParent3 static code
08c6212e-ea93-4f69-a238-644179b1a07d
说明:Str的值在编译期间,显然是不能够被识别出来的。所以:当只有在运行期才能确定的值的时候,这个类是会被调用的。
如果删除编译后的类-运行:会提示-类未找到异常。
代码5:
首次主动使用:静态代码块会被使用(被初始化)。第二次主动使用,就不会被调用
package com.dawa.jvm.classloader;
public class MyTest4 {
public static void main(String[] args) {
MyParent4 myParent4 = new MyParent4();
System.out.println("=====");
MyParent4 myParent5 = new MyParent4();
}
}
class MyParent4 {
static {
System.out.println("MyParent4 static Code");
}
}
运行结果
MyParent4 static Code
代码6:
不是主动使用的时候,不会初始化。类型数组不算 主动使用。
运行期间,JVM帮忙生成的[L... (数组)类型。
[[ 二维数组
[[[
数组类型的父类型为 Object
package com.dawa.jvm.classloader;
/**
* 对于数组实例来说,其类型就是由JVM在运行期间动态生成的. * 表示为[Lcom.dawa.jvm.classloader.MyParent4这种形式. * 动态生成的父类型为:Object. * * 对于数组来说,JavaDoc经常讲构成数组的元素为Component,实际上就是将数组降低一个维度后的类型.
*/
public class MyTest4 {
public static void main(String[] args) {
MyParent4[] myParent4s = new MyParent4[1];
System.out.println(myParent4s.getClass());
MyParent4[][] myParent4s1 = new MyParent4[1][1];
System.out.println(myParent4s1.getClass());
System.out.println(myParent4s.getClass().getSuperclass());
System.out.println(myParent4s1.getClass().getSuperclass());
}
}
class MyParent4 {
static {
System.out.println("MyParent4 static Code");
}
}
运行结果
class [Lcom.dawa.jvm.classloader.MyParent4;
class [[Lcom.dawa.jvm.classloader.MyParent4;
class java.lang.Object
class java.lang.Object
原始类型的数组类型
int为: class [I
char为:class[C
blooean为:class[Z
short为:class[S
byte为:class[B
父类为:Object
助记符:
anewawarry: 表示创建一个引用类型(如类,接口,数组)的数组,并将其引用值压至栈顶。
newarray:表示创建一个指定的原始类型(如int,float,char等)的数组,并将其引用值压至栈顶。
关于接口的一些特点
- 接口没有静态代码块
- 接口的初始化
- 接口的遍历,都是
public static final
删除编译好的文件,再运行。来完成测试。因为接口中没办法定义静态代码块。
得到结论:当一个接口在初始化时,并不要求其父接口都完成了初始化.但是只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会初始化。
代码7:
package com.dawa.jvm.classloader;
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(Singleton.counter1);
System.out.println(Singleton.counter2);
}
}
class Singleton {
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
输出结果为:
1
1
修改静态变量的位置
class Singleton {
public static int counter1;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
System.out.println(counter1);
System.out.println(counter2);
}
public static int counter2 = 0;
public static Singleton getInstance() {
return singleton;
}
}
输出结果为:
1
0
上述案例在开发的时候肯定不会用到,但是对于学习类的初始化,是非常有用的
准备阶段的重要意义。
初始化阶段的重要意义。
再来看看,类从磁盘上的字节码文件到内存,到销毁掉的过程。
加载过程1
加载过程2:
上面的一些种种案例,就是在验证上述图中描述的过程。
对象:内存是分配在堆上面的。
类的实例化过程和 常量的初始化过程是一样的。 不过类的初始化方法 统称为方法
类实例化
- 为新的对象分配内存
- 为实例变量赋默认值
- 为实例变量赋正确的初始值
- Java编译器为它编译的每一个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被成为“”,针对源代码中每一个类的构造方法,Java编译器都产生一个方法。
类加载器#
类加载器介绍#
-
类的加载的最终产品是内存中的Class对象**
-
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
(正因为如此,所以反射的原理,通过Class类加载器,获取方法区内的所有数据结构实现)
-
有两种类型的加载器
- Java虚拟机自带的加载器
- 根类加载器(BootStrap)
- 扩展类加载器(Extension)
- 系统(应用)类加载器(System)
- 用户自定义的类加载器
java.lang.ClassLoader
的子类。位于java.lang包下- 用户可以定制类的加载方式
- Java虚拟机自带的加载器
-
类加载器并不需要等到某个类被“首次主动使用”时再加载它。(代码1- 就能验证)
类被加载,但是未被初始化。(使用JVM参数,可以查看加载记录Log)
-
JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到的
.class
文件缺失或存在错误,类加载器必须在 程序首次主动使用该类时才报告错误。(错误) -
如果这个类一直没有被程序主动使用,那么 类加载器就不会报告错误
类的验证
- 类被加载后,就进入了连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
- 类的验证的内容(部分)
- 类文件的结构检查
- 语义检查
- 字节码验证
- 二进制兼容性的验证
类的准备阶段
类的初始化
类的初始化
- 类的初始化步骤
- 假如这个类还没有被加载和连接,那就先进行加载和连接
- 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
- 假如类中存在初始化预计,那就依次执行这些初始化语句
类的初始化时机
- 主动使用(七种,最上面有)
- 类的初始化实际
- 被动使用不会初始化
- 当Java虚拟机初始化一个类时,要求它的所有的父类都已经被初始化,但是这条规则并不适用于接口
- 在初始化一个类时,并不会先初始化它所实现的接口。
- 在初始化一个接口时,并不会先初始化它的父接口。
- 因此,一个父接口并不会因为它的子接口或者是实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
- 只有当程序访问的静态变量或静态方法确实在当前类或者当前接口中定义,才可以认为是对类或接口的主动使用。
- 调用
ClassLoader
类的loadClass
方法加载一个类,并不是对类的主动使用。不会导致类的初始化。
类加载器
双亲委托机制。 父加载器先加载,如下图内容解释。
Java虚拟机自带的几种加载器。
- 除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类
java.lang.ClassLoader
,所有用户自定义类加载器都应该继承ClassLoader
类。
几种加载器的结构
初始化对于类和接口的异同点深入分析#
之前的代码案例,不太能够说明问题。使用JVM参数,跟踪加载日志分析。
会发现,接口之间相互调用的时候,压根就没有被加载。
仅仅从加载的层次就能得到这个结论,更不用说初始化阶段了。
再切换成类,实现接口。 去打印类的遍历的时候,接口不会被主动加载和初始化。
语法快的解释:实例化的块。 和静态的代码块不一样。
实例化的块,在每一个实例被创建的时候,都会被调用了一次。
实例化的块,在构造方法的前面执行
class C {
//实例块,在每一次被实例化的时候都会被调用一次。 { }
// 静态代码块,在第一次初始化的时候会被调用一次。 static{ }
// 构造方法 public C(){ }
} // END - Class C
借助于 实例化块,来产生一个测试Demo去测试类和接口初始化的差距
package com.dawa.jvm.classloader;
public class MyTest7 {
public static void main(String[] args) {
System.out.println(MyChild7.b);
}
}
interface MyParent7 {
public static Thread thread = new Thread() {
{
System.out.println("MyParent7 invoked");
}
};
}
class MyChild7 implements MyParent7 {
public static int b = 5;
}
运行结果
5
好,这就说明问题了吧。当一个类被初始化的时候,接口是不会先被初始化的
类加载器的双亲委托机制详解#
Loader1 先去加载类,发现有父亲,向上。发现有父亲,向上。发现由父亲,向上。
由最父的 根类加载器先尝试加载。
失败之后,向下。失败之后向下,失败之后向下。
由于Sample类是用户自己编写的。根据下面的图,所以,系统类加载器能够读取。
所以就由Application ClassLoader类加载器来加载。
加载完成之后,将加载的Class文件,交给Loader1.】
上述就是:双亲委托机制。过程和概念和实现。
须知:Oracle公司的HotSpot虚拟机默认的就是这种双亲委托机制。并不是所有的JVM虚拟机都是这种机制。
其他类加载器:
-
若有一个类加载器能够成功加载Test类,那么这个类加载器被称为 定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器),都被称为 初始类加载器。
上述:
系统类加载器,为定义类加载器,也可以称为初始类加载器
Loader1被称为 初始类加载器
Class 类中 getClassLoader()的实现
@CallerSensitive
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl,
Reflection.getCallerClass());
}
return cl;
}
打印出来之后是一个内部类:sun.misc.Launcher&AppClassLoader@182sd21
自己编写的类加载器,都会直接或者间接的继承 ClassLoader
类
从代码角度,去理解上述概念
案例1: 常量-常量池的初始化和加载。 同时根据删除和不删除 class文件来判断一些概念。
判断类是运行时还是初始化时调用。
没有输出结果.FinalTest static block
package com.dawa.jvm.classloader;
class FinalTest {
public static final int x = 3; //public static int x = 3
static {
System.out.println("FinalTest static block");
}
}
public class MyTest8 {
public static void main(String[] args) {
System.out.println(FinalTest.x);
}
}
通过指令反编译 : javap -c com.dawa.jvm.classloader.MyTest8
运行结果如下:
classes javap -c java.main.com.dawa.jvm.classloader.MyTest8 警告: 二进制文件java.main.com.dawa.jvm.classloader.MyTest8包含com.dawa.jvm.classloader.MyTest8 Compiled from "MyTest8.java" public class com.dawa.jvm.classloader.MyTest8 { public com.dawa.jvm.classloader.MyTest8(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: iconst_3 4: invokevirtual #4 // Method java/io/PrintStream.println:(I)V 7: return }
如果不用静态变量,变为Random
package com.dawa.jvm.classloader;
import java.util.Random;
class FinalTest {
public static final int x = new Random().nextInt(3);
static {
System.out.println("FinalTest static block");
}
}
public class MyTest8 {
public static void main(String[] args) {
System.out.println(FinalTest.x);
}
}
反编译后的:
classes javap-c java.main.com.dawa.jvm.classloader.MyTest8 警告:二进制文件java.main.com.dawa.jvm.classloader.MyTest8包含com.dawa.jvm.classloader.MyTest8 Compiled from"MyTest8.java"
public class com.dawa.jvm.classloader.MyTest8{public com.dawa.jvm.classloader.MyTest8();Code:0:aload_0 1:invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: getstatic #3 // Field com/dawa/jvm/classloader/FinalTest.x:I 6: invokevirtual #4 // Method java/io/PrintStream.println:(I)V 9: return }
从3:iconst_3 变为 getstatic #3 // Field com/dawa/jvm/classloader/FinalTest.x:I
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
不同的类加载器作用与加载动作分析#
案例说明:使用代码,输出加载器和父类加载器。
输出结果:
有些JVM使用null,充当根类加载器。(源码DOC中都有说明。)
案例:通过字节码路径(全路径),获取字节码在什么位置上。
上下文 ContextClassLoader 类加载器,一般使用来加载应用的。然后我们使用这个加载器去说明一些问题。
这就拿到了给定的字节码文件所在的物理路径。
关于ClassLoader类的getResource()方法
/**
* Finds the resource with the given name. A resource is some data
* (images, audio, text, etc) that can be accessed by class code in a way
* that is independent of the location of the code.
*
* <p> The name of a resource is a '<tt>/</tt>'-separated path name that
* identifies the resource.
*
* <p> This method will first search the parent class loader for the
* resource; if the parent is <tt>null</tt> the path of the class loader
* built-in to the virtual machine is searched. That failing, this method
* will invoke {@link #findResource(String)} to find the resource. </p>
*
* @apiNote When overriding this method it is recommended that an
* implementation ensures that any delegation is consistent with the {@link
* #getResources(java.lang.String) getResources(String)} method.
*
* @param name
* The resource name
*
* @return A <tt>URL</tt> object for reading the resource, or
* <tt>null</tt> if the resource could not be found or the invoker
* doesn't have adequate privileges to get the resource.
*
* @since 1.1
*/
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
获取CLassLoader的途径
- 获取当前类的ClassLoader:
clazz.getClassLoader();
- 获取当前线程上线文的CLassLoader:
Thread.currentThread().getContextClassLoader()
- 获取系统的CLassLoader
ClassLoader.getSystemClassLoader()
- 获取调用者的CLassLoader
DriverManager.getCallerClassLoader()
案例:不同类的加载,使用上述方法去chauffeur
String类的类加载器是 null MyTest类的类加载器是 AppCLassLoader
再看一下下图把。
CLassLoader类的源码详解#
用到一个类,这个类又是重要的,常用的类,就去看源码Doc, 肯定是最权威的。
需要先自行花费时间去阅读一遍这个类。
/**
* A class loader is an object that is responsible for loading classes. The
* class <tt>ClassLoader</tt> is an abstract class. Given the <a
* href="#name">binary name</a> of a class, a class loader should attempt to
* locate or generate data that constitutes a definition for the class. A
* typical strategy is to transform the name into a file name and then read a
* "class file" of that name from a file system.
* 类装入器是负责装入类的对象。类类装入器是一个抽象类。
* 给定类的二进制名,类装入器应该尝试定位或生成构成类定义的数据。
* 一种典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“class文件”。
* 1. 定位 () 2.另外一种场景:从网络上获取类的名字
* <p> Every {@link Class <tt>Class</tt>} object contains a {@link
* Class#getClassLoader() reference} to the <tt>ClassLoader</tt> that defined
* it.
* 每一个类对象,包含一个定义这个class对象的CLassLoader类加载器对象。
*
* <p> <tt>Class</tt> objects for array classes are not created by class
* loaders, but are created automatically as required by the Java runtime.
* The class loader for an array class, as returned by {@link
* Class#getClassLoader()} is the same as the class loader for its element
* type; if the element type is a primitive type, then the array class has no
* class loader.
*
* 重要:对于数组类的对象来说,不是由类加载器来创建的。而是由运行时由JVM虚拟机来动态创建的。
* 换句话来说,其他类型都是由类加载器来创建的。
* 数组调用getclassloader()返回的类加载器和数组中的每个元素调用的类加载器是一样的。
* 如果类型是原生类型的话, 是没有类加载器的。
*
* <p> Applications implement subclasses of <tt>ClassLoader</tt> in order to
* extend the manner in which the Java virtual machine dynamically loads
* classes.
* 应用程序实现类装入器的子类,以扩展Java虚拟机动态装入类的方式。
*
* <p> Class loaders may typically be used by security managers to indicate
* security domains.
*
* //安全管理器通常会使用类装入器来指示安全域。 //类加载器的设计:是安全的
*
* <p> The <tt>ClassLoader</tt> class uses a delegation model to search for
* classes and resources. Each instance of <tt>ClassLoader</tt> has an
* associated parent class loader. When requested to find a class or
* resource, a <tt>ClassLoader</tt> instance will delegate the search for the
* class or resource to its parent class loader before attempting to find the
* class or resource itself. The virtual machine's built-in class loader,
* called the "bootstrap class loader", does not itself have a parent but may
* serve as the parent of a <tt>ClassLoader</tt> instance.
*
* 委托模型:双亲委托模型。
* 类加载器使用双亲委托模型。ClassLoader类使用委托模型来搜索类和资源。
* 类加载器的每个实例都有一个关联的父类加载器器。
* 当请求查找类或资源时,类加载器实例将把对类或资源的搜索委托给其父类加载器,然后再尝试查找类或资源本身。
* 虚拟机的内置类加载器称为“启动类加载器”,它本身没有父类加载器,但可以作为类加载器实例的父类。
*
* <p> Class loaders that support concurrent loading of classes are known as
* <em>parallel capable</em> class loaders and are required to register
* themselves at their class initialization time by invoking the
* {@link
* #registerAsParallelCapable <tt>ClassLoader.registerAsParallelCapable</tt>}
* method. Note that the <tt>ClassLoader</tt> class is registered as parallel
* capable by default. However, its subclasses still need to register themselves
* if they are parallel capable. <br>
* In environments in which the delegation model is not strictly
* hierarchical, class loaders need to be parallel capable, otherwise class
* loading can lead to deadlocks because the loader lock is held for the
* duration of the class loading process (see {@link #loadClass
* <tt>loadClass</tt>} methods).
*
* 支持类的并发加载的类加载器称为支持并行的类加载器,需要通过调用类加载器在类初始化时注册它们自己。
* registerAsParallelCapable方法。注意,默认情况下ClassLoader类被注册为支持并行的。
* 但是,它的子类仍然需要注册它们自己,如果它们是并行的。
*
* 在委托模型没有严格层次结构的环境中,类装入器需要具有并行能力,否则类装入可能会导致死锁,因为装入器锁在类装入过程期间一直持有(请参阅loadClass方法)。
*
* <p> Normally, the Java virtual machine loads classes from the local file
* system in a platform-dependent manner. For example, on UNIX systems, the
* virtual machine loads classes from the directory defined by the
* <tt>CLASSPATH</tt> environment variable.
*
* //通常,Java虚拟机以平台相关的方式从本地文件系统加载类。例如,在UNIX系统上,虚拟机从CLASSPATH环境变量定义的目录加载类。
*
* <p> However, some classes may not originate from a file; they may originate
* from other sources, such as the network, or they could be constructed by an
* application. The method {@link #defineClass(String, byte[], int, int)
* <tt>defineClass</tt>} converts an array of bytes into an instance of class
* <tt>Class</tt>. Instances of this newly defined class can be created using
* {@link Class#newInstance <tt>Class.newInstance</tt>}.
*
* //但是,有些类可能不是起源于文件;它们可能来自其他来源,例如网络,也可能由应用程序构造。
* //方法defineClass将字节数组转换为类的实例。 //可以使用Class.newinstance创建这个新定义的类的实例。
*
* <p> The methods and constructors of objects created by a class loader may
* reference other classes. To determine the class(es) referred to, the Java
* virtual machine invokes the {@link #loadClass <tt>loadClass</tt>} method of
* the class loader that originally created the class.
*
* //由类加载器创建的对象的方法和构造函数可以引用其他类。
* 为了确定引用的类,Java虚拟机调用最初创建类的类加载器的loadClass方法。去加载其他的类。
*
* <p> For example, an application could create a network class loader to
* download class files from a server. Sample code might look like:
*
* <blockquote><pre>
* ClassLoader loader = new NetworkClassLoader(host, port);
* Object main = loader.loadClass("Main", true).newInstance();
* . . .
* </pre></blockquote>
*
* //例如,应用程序可以创建一个网络类加载器来从服务器下载类文件。
* 示例代码可能如下: ClassLoader loader = new NetworkClassLoader(host, port); Object main = loader.loadClass("Main", true).newInstance(); . . .
*
* <p> The network class loader subclass must define the methods {@link
* #findClass <tt>findClass</tt>} and <tt>loadClassData</tt> to load a class
* from the network. Once it has downloaded the bytes that make up the class,
* it should use the method {@link #defineClass <tt>defineClass</tt>} to
* create a class instance. A sample implementation is:
*
* <blockquote><pre>
* class NetworkClassLoader extends ClassLoader {
* String host;
* int port;
*
* public Class findClass(String name) {
* byte[] b = loadClassData(name);
* return defineClass(name, b, 0, b.length);
* }
*
* private byte[] loadClassData(String name) {
* // load the class data from the connection
* . . .
* }
* }
* </pre></blockquote>
*
* <h3> <a name="name">Binary names</a> </h3>
*
* <p> Any class name provided as a {@link String} parameter to methods in
* <tt>ClassLoader</tt> must be a binary name as defined by
* <cite>The Java™ Language Specification</cite>.
*
* <p> Examples of valid class names include:
* <blockquote><pre>
* "java.lang.String"
* "javax.swing.JSpinner$DefaultEditor"
* "java.security.KeyStore$Builder$FileBuilder$1"
* "java.net.URLClassLoader$3$1"
* </pre></blockquote>
*
* @see #resolveClass(Class)
* @since 1.0
*/
从Doc文字摘取的重要知识点:
-
对于数组类的对象来说,不是由类加载器来创建的。而是由运行时由JVM虚拟机来动态创建的。(只有数组类型是特殊的,其他的都不特殊)
-
数组调用getclassloader()返回的类加载器和数组中的每个元素调用的类加载器是一样的。
如果类型是原生类型的话, 是没有类加载器的。String[]strings=new String[2]; System.out.println(strings.getClass().getClassLoader()); // 输出结果为:null (这个Null是根类加载器) MyTest1[] myTest1s = new MyTest1[2]; System.out.println(myTest1s.getClass().getClassLoader()); // 输出结果为:sun.misc.Launcher@appClassLoader@1823s1 int[] ints = new int[2]; System.out.println(myTest1s.getClass().getClassLoader()); // 输出结果为:null(这个null是指没有类加载器) 注意:第一个Null和第二个Null的意义不一样
-
有些类可能不是起源于文件;它们可能来自其他来源,例如网络,也可能由应用程序构造。方法defineClass将字节数组转换为类的实例。可以使用Class.newinstance创建这个新定义的类的实例。
自定义一个类加载器,去理解Doc提到的方法
package com.dawa.jvm.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyTest16 extends ClassLoader { //类加载的名字
private String classLoaderName;
//Class文件的后缀名
private final String fileExtension = ".class"; //两个构造方法.
public MyTest16(ClassLoader parent, String classLoaderName) {
super(parent);
//显示指定该类加载器的父加载器. 看源码构造方法就明白了
this.classLoaderName = classLoaderName;
}
public MyTest16(String classLoaderName) {
super();
//将系统类加载器当做该类加载器的父加载器. 看源码构造方法就明白了
this.classLoaderName = classLoaderName;
}
private byte[] loadCLassDate(String name) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream ba = null;
try {
this.classLoaderName = this.classLoaderName.replace(".", "/");
is = new FileInputStream(new File(name + this.fileExtension));
ba = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = is.read())) {
ba.write(ch);
}
data = ba.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
ba.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
} //重写父类的findClass方法,关键方法。 查看源码,查看父类方法的含义。 @Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] data = this.loadCLassDate(className); return this.defineClass(className, data, 0, data.length); } }
测试使用:
public static void test(ClassLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("com.dawa.jvm.classloader.MyTest");
Object object = clazz.newInstance();
System.out.println(object);
}
public static void main(String[] args) throws Exception {
MyTest16 loader1 = new MyTest16("loader1");
test(loader1);
}
Copy运行结果:
com.dawa.jvm.classloader.MyTest@7852e922
over,成功。
问题:loadclass()方法,好像不是用的自定义的。 自定义的加载数据的方法好像没有用法的。使用Debug不会进入Debug模式。
答案:好吧,上述案例真的有问题(后来补充),压根不会执行。因为双亲委托机制,被APP加载器给加载了。
因为自己使用的时候,没有指定父类加载器。所以会自动调用双亲委托机制默认的加载器。
如图概念所示:
系统类加载器,就是他的定义类加载器。
系统类加载器和自定义的MyTest16,就是他的初始类加载器。
改进后的自定义的类加载器和测试
package com.dawa.jvm.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyTest16 extends ClassLoader {
//类加载的名字
private String classLoaderName; //Class文件的后缀名
private final String fileExtension = ".class"; //文件路径
private String path;
public void setPath(String path) {
this.path = path;
}
//两个构造方法.
public MyTest16(ClassLoader parent, String classLoaderName) {
super(parent);
//显示指定该类加载器的父加载器.
this.classLoaderName = classLoaderName;
}
public MyTest16(String classLoaderName) {
super();
//将系统类加载器当做该类加载器的父加载器.
this.classLoaderName = classLoaderName;
}
private byte[] loadCLassDate(String className) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream ba = null;
className = className.replace(".", "/");
//Windows系统使用反斜杠,Mac使用正斜杠
try {
is = new FileInputStream(new File(this.path + className + this.fileExtension));
ba = new ByteArrayOutputStream();
int ch;
while (-1 != (ch = is.read())) {
ba.write(ch);
}
data = ba.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
ba.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
} //重写父类的findClass方法 @Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] data = this.loadCLassDate(className); return this.defineClass(className, data, 0, data.length); } public static void main(String[] args) throws Exception{ //创建自定义类加载器. 父类是:系统类加载器 MyTest16 loader1 = new MyTest16("loader1"); //设置绝对路径 loader1.setPath("/Users/shangyifeng/work/workspace/jvm_leature/build/classes/"); Class<?> clazz = loader1.loadClass("com.dawa.jvm.classloader.MyTest"); System.out.println("class: "+ clazz.hashCode()); Object object = clazz.newInstance(); System.out.println(object); } }
}
}
目前:还是由系统类加载器加载的。
再次改进:
Copy
➜ classes mkdir -p ~/Desktop/com/dawa/jvm/classloade ➜ classes cp java/main/com/dawa/jvm/classloader/MyTest.class ~/Desktop/com/dawa/jvm/classloade
代码上:
Copy
public static void main(String[] args) throws Exception{ //创建自定义类加载器. 父类是:系统类加载器 MyTest16 loader1 = new MyTest16("loader1"); //设置绝对路径 // loader1.setPath("/Users/shangyifeng/work/workspace/jvm_leature/build/classes/"); loader1.setPath("/Users/Desktop"); Class<?> clazz = loader1.loadClass("com.dawa.jvm.classloader.MyTest"); System.out.println("class: "+ clazz.hashCode()); Object object = clazz.newInstance(); System.out.println(object); }
注意:一个类只会被加载一次。
但是:还涉及类加载器的命名空间问题。
左边MyTest1删除的情况
左边MyTest1不删除的情况
Loader2的父类加载器为Loader1的情况
- 命名空间
- 每个类加载器都有自己的命名空间。命名空间由该加载器及所有父加载器所加载的类组成
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
在谈论类加载器的时候,要涉及命名空间的前提。
从代码中:说明加载顺序:loadClass()方法的Doc注释已经说明加载顺序。第一步是寻找已经加载的类。
类加载器的关系:并不是树形结构,而是一种包含关系。
因此可能让处于同一层次的类加载的关系为:父子关系。
自定义类加载器涉及的重要方法#
使用结果,去验证结论。不然只知道结论是没有用的。
我们要探究的三个方法
-
findClass() - 自定义一个类加载器,主要是重写这个方法
-
defineClass() - 自定义一个类加载器,调用了这个方法
-
loaderClass() - 自定义一个类加载器,调用了这个方法
-
CLassLoader类提供的构造方法。
Copy
private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; if (ParallelLoaders.isRegistered(this.getClass())) { parallelLockMap = new ConcurrentHashMap<>(); package2certs = new ConcurrentHashMap<>(); assertionLock = new Object(); } else { // no finer-grained lock; lock on the classloader instance parallelLockMap = null; package2certs = new Hashtable<>(); assertionLock = this; } } /** * Creates a new class loader using the specified parent class loader for * delegation. * * <p> If there is a security manager, its {@link * SecurityManager#checkCreateClassLoader() * <tt>checkCreateClassLoader</tt>} method is invoked. This may result in * a security exception. </p> * * @param parent * The parent class loader * * @throws SecurityException * If a security manager exists and its * <tt>checkCreateClassLoader</tt> method doesn't allow creation * of a new class loader. * * @since 1.2 */ protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); } /** * Creates a new class loader using the <tt>ClassLoader</tt> returned by * the method {@link #getSystemClassLoader() * <tt>getSystemClassLoader()</tt>} as the parent class loader. * * <p> If there is a security manager, its {@link * SecurityManager#checkCreateClassLoader() * <tt>checkCreateClassLoader</tt>} method is invoked. This may result in * a security exception. </p> * * @throws SecurityException * If a security manager exists and its * <tt>checkCreateClassLoader</tt> method doesn't allow creation * of a new class loader. */ protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } // -- Class -- /** * Loads the class with the specified <a href="#name">binary name</a>. * This method searches for classes in the same manner as the {@link * #loadClass(String, boolean)} method. It is invoked by the Java virtual * machine to resolve class references. Invoking this method is equivalent * to invoking {@link #loadClass(String, boolean) <tt>loadClass(name, * false)</tt>}. * * @param name * The <a href="#name">binary name</a> of the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class was not found */ public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
-
findClass()
Copy
/** * Finds the class with the specified <a href="#name">binary name</a>. * This method should be overridden by class loader implementations that * follow the delegation model for loading classes, and will be invoked by * the {@link #loadClass <tt>loadClass</tt>} method after checking the * parent class loader for the requested class. The default implementation * throws a <tt>ClassNotFoundException</tt>. * * @param name * The <a href="#name">binary name</a> of the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class could not be found * * @since 1.2 */ protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
因为父类中的方法,
findClass()
方法体只是抛出一个ClassNotFoundException
,所以子类必须重写方法体。自定义加载器-最重要的步骤,就是重写这个
findClass()
方法 -
classloader()
Copy
/** * Loads the class with the specified <a href="#name">binary name</a>. * This method searches for classes in the same manner as the {@link * #loadClass(String, boolean)} method. It is invoked by the Java virtual * machine to resolve class references. Invoking this method is equivalent * to invoking {@link #loadClass(String, boolean) <tt>loadClass(name, * false)</tt>}. * * @param name * The <a href="#name">binary name</a> of the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class was not found */ public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
通过字节流的形式,加载Class文件,返回Byte[]字节数组。
-
defineClass()
Copy
/** * Converts an array of bytes into an instance of class <tt>Class</tt>. * Before the <tt>Class</tt> can be used it must be resolved. * * <p> This method assigns a default {@link java.security.ProtectionDomain * <tt>ProtectionDomain</tt>} to the newly defined class. The * <tt>ProtectionDomain</tt> is effectively granted the same set of * permissions returned when {@link * java.security.Policy#getPermissions(java.security.CodeSource) * <tt>Policy.getPolicy().getPermissions(new CodeSource(null, null))</tt>} * is invoked. The default domain is created on the first invocation of * {@link #defineClass(String, byte[], int, int) <tt>defineClass</tt>}, * and re-used on subsequent invocations. * * <p> To assign a specific <tt>ProtectionDomain</tt> to the class, use * the {@link #defineClass(String, byte[], int, int, * java.security.ProtectionDomain) <tt>defineClass</tt>} method that takes a * <tt>ProtectionDomain</tt> as one of its arguments. </p> * * @param name * The expected <a href="#name">binary name</a> of the class, or * <tt>null</tt> if not known * * @param b * The bytes that make up the class data. The bytes in positions * <tt>off</tt> through <tt>off+len-1</tt> should have the format * of a valid class file as defined by * <cite>The Java™ Virtual Machine Specification</cite>. * * @param off * The start offset in <tt>b</tt> of the class data * * @param len * The length of the class data * * @return The <tt>Class</tt> object that was created from the specified * class data. * * @throws ClassFormatError * If the data did not contain a valid class * * @throws IndexOutOfBoundsException * If either <tt>off</tt> or <tt>len</tt> is negative, or if * <tt>off+len</tt> is greater than <tt>b.length</tt>. * * @throws SecurityException * If an attempt is made to add this class to a package that * contains classes that were signed by a different set of * certificates than this class (which is unsigned), or if * <tt>name</tt> begins with "<tt>java.</tt>". * * @see #loadClass(String, boolean) * @see #resolveClass(Class) * @see java.security.CodeSource * @see java.security.SecureClassLoader * * @since 1.1 */ protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null); }
再往上最底层的时候:方法的关键词为:native。
Copy
native 关键词,标记这个方法是本地方法,调用本地C语言的底层来实现。
类的卸载#
- 当一个MyTest类被加载、连接和初始化后,它的生命周期就开始了。当代表MyTest类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,MyTest类在方法区内的数据也会被卸载,从而结束Test类的生命周期。
- 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
-
由用户自定义的类加载器所加载的类是可以被卸载的
虚拟机自带的类加载器,所加载的类,是不可被卸载的。
由用户自定义的类加载器所加载的类是可以被加载的。
案例1:使用JVM 参数-卸载参数 和 System.gc(),来判断是否会被卸载。
JVM参数: -XX:+TraceClassUnloading
对象置空的时候,才会被GC垃圾回收机制回收。
案例2:使用jvisualvm工具来监视进程。 (让线程沉睡20秒)
已经被装在数量:1568
已经被卸载数量:1
自定义类加载器在复杂类加载情况下的运行分析#
用到的两个实体类:
Copy
public class MyCat{ public MyCat(){ sout("MyCat is loaded by: "+ this.getClass().getClassLoader()) } } public class MySample{ public MySample(){ sout("MySample is loaded by: "+ this.getClass().getClassLoader()) new MyCat(); //这就把两个类给关联起来的。 } }
有关的类加载的操作:
Copy
public class MyTest17{ psvm(){ MyTest16 loader1 = new MyTest16("loader1"); //父加载器为APP Class<?> clazz = loader1.loadClass("com.dawa.jvm.classLoader.MySample"); sout("class:"+class.hashCode()); //如果注释该行,那么并不会实例化MySample对象,即MySample构造方法不会被调用 //因此不会实例化MyCat对象。 也不会调用MyCat的构造方法。这里就不会加载MyCat Class. //但是 MyCat类,并不一样被加载。因为如果不主动使用。也是可能会被加载的。 //这里先使用 -XX:+TraceClassLoading 去查看记录: //我们发现,注释起来之后,没有被加载。但是在某些情况下,没用到,也是可能会被预加载的。 Obejct object = class.newInstance(); } }
运行结果:不会调用 MyTest16中的findClass()输出。
因为:都是用父类的 AppCLassLoader加载的。
上述案例,只删除MyCat,运行
默认是用当前类的(MySample)加载器来加载 MyCat。
上述案例,只删除MySample,运行。
正常运行。MyTest16 加载的桌面的文件。有。所以没报错。
MyCat: 也是被MyTest16 尝试去加载。(但是还是双亲委托机制)
改造程序:在MyCat中添加MySample的调用
Copy
public class MyCat{ public MyCat(){ sout("MyCat is loaded by: "+ this.getClass().getClassLoader()) //新增: sout("from MyCat:"+ MySample.class) } }
删除MySample: 运行出问题
问题原因:
- MySample是由自定义加载器加载的
- MyCat是由系统类加载器加载的
当MySample类找不到的时候,默认使用所在的MyCat类的列加载器去加载,加载不到。所以报错。
改动上述案例:在MySample中加如MyCat的类的调用
Copy
public class MySample{ public MySample(){ sout("MySample is loaded by: "+ this.getClass().getClassLoader()) new MyCat(); //这就把两个类给关联起来的。 sout(MyCat.class); } }
删除MyCat,运行
运行结果没问题:
因为自定义的命名空间为子类:子类加载器 可以加载父类 APP加载器能够加载的类。
关于命名空间的重要说明
- 子加载器所加载的类能够访问父加载器所加载的类
- 父加载器所加载的类无法访问子加载器所加载的类
关于不同类加载器,所加载的文件的路径
用代码,去打印出不同类加载器所加载的文件路径。
-
启动类加载器
Copy
sout(System.getProperty("sun.boot.class.path"));
-
扩展类加载器
Copy
sout(System.getProperty("java.ext.dirs"))
-
系统类加载器(IDEA配置的有路径,所以能够直接加载你编译的out文件夹下的目录Class文件)
Copy
sout(System.getProperty("java.class.path"))
如果自行用去模拟路径或者放入路径相同的目录,是能够被指定的符合条件的类加载器所调用的
拓展类加载器相关的案例:
使用 java -D参数,修改拓展类加载器的目录,就能说明一些问题。
案例:
定义一个Person类
调用
结果 为true
原因:
- Loader1加载,通过父类的系统类加载器加载成功。 MyPerson被成功加载
- Loader2加载,也是通过父类去加载,但是加载前会判断是否已经存在,因为已存在,所以直接放回。
- 所以:clazz1 = clazz2
通过反射去调用,不会有错。
invoke(arg1,arg2):arg1-代表在哪个类上调用这个方法。 Arg2 - 方法 参数
改造上述程序:1.给两个加载器都设定加载目录。2.将之前的class文件放入桌面。3.删除idea中的MyPerson文件。
结果:Flase + 异常
Flase原因:
- 命名空间的问题。因为两个Loader的名字不同,所以命名不同。
- 因为命名空间不同,所以不能算是已经加载过文件,所以需要重新加载。
- 所以导致的结果不同 = Flase
异常原因:MyPerson 无法转换 MyPerson。哈哈哈哈啊哈哈哈哈哈。
- 因为命名空间不同。嗯。根据上下文分析,他们是互不可见的
另外:
综上:命名空间在日常开发用到的很少,但是针对于框架的底层,或者是服务器开发,命名空间是很重要的。
类加载器的双亲委托模型的好处:总结#
- 可以确保Java核心库的类型安全:所有的Java应用都至少会应用java.lang.Object类。也就是说在运行期间,java.lang.Obejct类会被加载到Java虚拟机中,如果这个加载过程是由Java应用加载的类加载器所完成的。那么很可能就会在JVM中存在多个版本的java.lang.Obejct类,而且这些类之间还是不兼容的,相互不可见的(正式命名空间在发挥着作用)。借助于双亲委托机制,Java核心类库中的加载工作都是由启动类加载器来统一完成,从而确保了java应用所使用的都是同一版本的核心类库,他们之间都是相互兼容的。
- 可以确保Java核心类库所提供的类不会被自定义的类所替代。
- 不同的类加载器可以为相同名词(binary name)的类创建额外的命名空间。相同名词的类可以并存在Java虚拟机中,只需要用不同的类加载器加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用。
修改完扩展类加载器的目录为当前目录之后,为什么还是AppCLassLoader加载的?
因为啊:扩展类加载器:要求只能读取Jar包中的文件,不能直接加载.class文件。
我们继续,打包:然后,再运行。
结果发生了很大的变化。
案例:使用java命令运行
如果直接使用java命令运行的时候,AppCLassLoader加载的目录为当前目录:这也是我们最初学java的时候为什么要配置环境变量。
在运行期,一个Java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的,如果同样的名字(即相同的完全限定名)的类hi由两个不同的类加载器所加载,那么这些类就是不同的,即.class文件的字节码完全一样,并且从相同的位置加载也是如此。
案例:修改根加载器的 默认路径
在Oracle的HotSpot实现中,系统属性sun.boot.class.path如果修改错了,则运行会出错。出错内容如上图所示
数组:
只有数组是特殊的情况。不是由类加载器加载的。是在运行期间由JVM动态创建的。
问题:用Java语言写的类加载器,是由谁加载的? (先有鸡还是先有蛋)
答案:是由启动类加载器加载的。启动类加载器,不是java代码,是用C++ 语言写的,是归属为JVM的一部分。在JVM启动的时候就会实例化,创建启动类加载器的对象。
- 因为类加载器都是Launcher类的静态内部类,所以,只用判断Launcher是由哪些加载器加载的,就可以简介的推断内部的静态类是由哪个加载器加载的。
所谓的那些默认路径和默认值,都是在源码中配置好的。源码之前,了无秘密。
关于系统类加载器,默认值是未被定义的,所以默认指向AppCLassLoader。在下述Doc文档中描述的很清楚。
所以想要修改默认的系统加载器:使用java.system.loader
属性修改其默认值就行。
报错了:是因为没有满足上述的 单参数的构造方法。
如果属性被定义了,那么就必须定义一个单个参数为CLassLoader的构造器,来实现双亲委托机制的。如下添加指定的构造器
再运行:
自定义的类加载器,是由默认的类加载器加载的(即AppCLassLoader)
使用命令行运行的时候,打印出默认加载器就能打印出来自己配置的默认加载器的名字。
Copy
sout(ClassLoader.getSystemClassLoader)
有时间就自行查阅Doc文档。
CLassLoader类,getSystemCLassLoader()方法。 提到了系统类加载器。
- context class Loader
- 为什么一定要提供一个默认的 单参的构造方法
OPENJDK,开源的JDK版本。 获取源代码。
Openjdk.java.net : openjdk的官网
Oraclejdk 不开源
grepcode.com : 查看源码的网站 输入包名
网上的技术,文章,书,基本上都是来源于源码。
Launcher类的源码分析
Copy
/* * Copyright (c) 1998, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package sun.misc; import java.io.File; import java.io.IOException; import java.io.FilePermission; import java.net.*; import java.nio.file.Paths; import java.util.HashSet; import java.util.StringTokenizer; import java.util.Set; import java.util.Vector; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; import java.security.AccessControlContext; import java.security.PermissionCollection; import java.security.Permissions; import java.security.Permission; import java.security.ProtectionDomain; import java.security.CodeSource; import sun.security.util.SecurityConstants; import sun.net.www.ParseUtil; /** * This class is used by the system to launch the main application. */ public class Launcher { private static URLStreamHandlerFactory factory = new Factory(); private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path"); public static Launcher getLauncher() { return launcher; } private ClassLoader loader; public Launcher() { // Create the extension class loader ClassLoader extcl; try { extcl = ExtClassLoader.getExtClassLoader(); } catch (IOException e) { throw new InternalError( "Could not create extension class loader", e); } // Now create the class loader to use to launch the application try { loader = AppClassLoader.getAppClassLoader(extcl); } catch (IOException e) { throw new InternalError( "Could not create application class loader", e); } // Also set the context class loader for the primordial thread. Thread.currentThread().setContextClassLoader(loader); // Finally, install a security manager if requested String s = System.getProperty("java.security.manager"); if (s != null) { // init FileSystem machinery before SecurityManager installation sun.nio.fs.DefaultFileSystemProvider.create(); SecurityManager sm = null; if ("".equals(s) || "default".equals(s)) { sm = new java.lang.SecurityManager(); } else { try { sm = (SecurityManager)loader.loadClass(s).newInstance(); } catch (IllegalAccessException e) { } catch (InstantiationException e) { } catch (ClassNotFoundException e) { } catch (ClassCastException e) { } } if (sm != null) { System.setSecurityManager(sm); } else { throw new InternalError( "Could not create SecurityManager: " + s); } } } /* * Returns the class loader used to launch the main application. */ public ClassLoader getClassLoader() { return loader; } /* * The class loader used for loading installed extensions. */ static class ExtClassLoader extends URLClassLoader { static { ClassLoader.registerAsParallelCapable(); } private static volatile ExtClassLoader instance = null; /** * create an ExtClassLoader. The ExtClassLoader is created * within a context that limits which files it can read */ public static ExtClassLoader getExtClassLoader() throws IOException { if (instance == null) { synchronized(ExtClassLoader.class) { if (instance == null) { instance = createExtClassLoader(); } } } return instance; } private static ExtClassLoader createExtClassLoader() throws IOException { try { // Prior implementations of this doPrivileged() block supplied // aa synthesized ACC via a call to the private method // ExtClassLoader.getContext(). return AccessController.doPrivileged( new PrivilegedExceptionAction<ExtClassLoader>() { public ExtClassLoader run() throws IOException { final File[] dirs = getExtDirs(); int len = dirs.length; for (int i = 0; i < len; i++) { MetaIndex.registerDirectory(dirs[i]); } return new ExtClassLoader(dirs); } }); } catch (java.security.PrivilegedActionException e) { throw (IOException) e.getException(); } } void addExtURL(URL url) { super.addURL(url); } /* * Creates a new ExtClassLoader for the specified directories. */ public ExtClassLoader(File[] dirs) throws IOException { super(getExtURLs(dirs), null, factory); SharedSecrets.getJavaNetAccess(). getURLClassPath(this).initLookupCache(this); } private static File[] getExtDirs() { String s = System.getProperty("java.ext.dirs"); File[] dirs; if (s != null) { StringTokenizer st = new StringTokenizer(s, File.pathSeparator); int count = st.countTokens(); dirs = new File[count]; for (int i = 0; i < count; i++) { dirs[i] = new File(st.nextToken()); } } else { dirs = new File[0]; } return dirs; } private static URL[] getExtURLs(File[] dirs) throws IOException { Vector<URL> urls = new Vector<URL>(); for (int i = 0; i < dirs.length; i++) { String[] files = dirs[i].list(); if (files != null) { for (int j = 0; j < files.length; j++) { if (!files[j].equals("meta-index")) { File f = new File(dirs[i], files[j]); urls.add(getFileURL(f)); } } } } URL[] ua = new URL[urls.size()]; urls.copyInto(ua); return ua; } /* * Searches the installed extension directories for the specified * library name. For each extension directory, we first look for * the native library in the subdirectory whose name is the value * of the system property <code>os.arch</code>. Failing that, we * look in the extension directory itself. */ public String findLibrary(String name) { name = System.mapLibraryName(name); URL[] urls = super.getURLs(); File prevDir = null; for (int i = 0; i < urls.length; i++) { // Get the ext directory from the URL; convert to // URI first, so the URL will be decoded. URI uri; try { uri = urls[i].toURI(); } catch (URISyntaxException ue) { // skip this URL if cannot convert it to URI continue; } // Use the Paths.get(uri) call in order to handle // UNC based file name conversion correctly. File dir = Paths.get(uri).toFile().getParentFile(); if (dir != null && !dir.equals(prevDir)) { // Look in architecture-specific subdirectory first // Read from the saved system properties to avoid deadlock String arch = VM.getSavedProperty("os.arch"); if (arch != null) { File file = new File(new File(dir, arch), name); if (file.exists()) { return file.getAbsolutePath(); } } // Then check the extension directory File file = new File(dir, name); if (file.exists()) { return file.getAbsolutePath(); } } prevDir = dir; } return null; } private static AccessControlContext getContext(File[] dirs) throws IOException { PathPermissions perms = new PathPermissions(dirs); ProtectionDomain domain = new ProtectionDomain( new CodeSource(perms.getCodeBase(), (java.security.cert.Certificate[]) null), perms); AccessControlContext acc = new AccessControlContext(new ProtectionDomain[] { domain }); return acc; } } /** * The class loader used for loading from java.class.path. * runs in a restricted security context. */ static class AppClassLoader extends URLClassLoader { static { ClassLoader.registerAsParallelCapable(); } public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException { final String s = System.getProperty("java.class.path"); final File[] path = (s == null) ? new File[0] : getClassPath(s); // Note: on bugid 4256530 // Prior implementations of this doPrivileged() block supplied // a rather restrictive ACC via a call to the private method // AppClassLoader.getContext(). This proved overly restrictive // when loading classes. Specifically it prevent // accessClassInPackage.sun.* grants from being honored. // return AccessController.doPrivileged( new PrivilegedAction<AppClassLoader>() { public AppClassLoader run() { URL[] urls = (s == null) ? new URL[0] : pathToURLs(path); return new AppClassLoader(urls, extcl); } }); } final URLClassPath ucp; /* * Creates a new AppClassLoader */ AppClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent, factory); ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this); ucp.initLookupCache(this); } /** * Override loadClass so we can checkPackageAccess. */ public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { int i = name.lastIndexOf('.'); if (i != -1) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPackageAccess(name.substring(0, i)); } } if (ucp.knownToNotExist(name)) { // The class of the given name is not found in the parent // class loader as well as its local URLClassPath. // Check if this class has already been defined dynamically; // if so, return the loaded class; otherwise, skip the parent // delegation and findClass. Class<?> c = findLoadedClass(name); if (c != null) { if (resolve) { resolveClass(c); } return c; } throw new ClassNotFoundException(name); } return (super.loadClass(name, resolve)); } /** * allow any classes loaded from classpath to exit the VM. */ protected PermissionCollection getPermissions(CodeSource codesource) { PermissionCollection perms = super.getPermissions(codesource); perms.add(new RuntimePermission("exitVM")); return perms; } /** * This class loader supports dynamic additions to the class path * at runtime. * * @see java.lang.instrument.Instrumentation#appendToSystemClassPathSearch */ private void appendToClassPathForInstrumentation(String path) { assert(Thread.holdsLock(this)); // addURL is a no-op if path already contains the URL super.addURL( getFileURL(new File(path)) ); } /** * create a context that can read any directories (recursively) * mentioned in the class path. In the case of a jar, it has to * be the directory containing the jar, not just the jar, as jar * files might refer to other jar files. */ private static AccessControlContext getContext(File[] cp) throws java.net.MalformedURLException { PathPermissions perms = new PathPermissions(cp); ProtectionDomain domain = new ProtectionDomain(new CodeSource(perms.getCodeBase(), (java.security.cert.Certificate[]) null), perms); AccessControlContext acc = new AccessControlContext(new ProtectionDomain[] { domain }); return acc; } } private static class BootClassPathHolder { static final URLClassPath bcp; static { URL[] urls; if (bootClassPath != null) { urls = AccessController.doPrivileged( new PrivilegedAction<URL[]>() { public URL[] run() { File[] classPath = getClassPath(bootClassPath); int len = classPath.length; Set<File> seenDirs = new HashSet<File>(); for (int i = 0; i < len; i++) { File curEntry = classPath[i]; // Negative test used to properly handle // nonexistent jars on boot class path if (!curEntry.isDirectory()) { curEntry = curEntry.getParentFile(); } if (curEntry != null && seenDirs.add(curEntry)) { MetaIndex.registerDirectory(curEntry); } } return pathToURLs(classPath); } } ); } else { urls = new URL[0]; } bcp = new URLClassPath(urls, factory, null); bcp.initLookupCache(null); } } public static URLClassPath getBootstrapClassPath() { return BootClassPathHolder.bcp; } private static URL[] pathToURLs(File[] path) { URL[] urls = new URL[path.length]; for (int i = 0; i < path.length; i++) { urls[i] = getFileURL(path[i]); } // DEBUG //for (int i = 0; i < urls.length; i++) { // System.out.println("urls[" + i + "] = " + '"' + urls[i] + '"'); //} return urls; } private static File[] getClassPath(String cp) { File[] path; if (cp != null) { int count = 0, maxCount = 1; int pos = 0, lastPos = 0; // Count the number of separators first while ((pos = cp.indexOf(File.pathSeparator, lastPos)) != -1) { maxCount++; lastPos = pos + 1; } path = new File[maxCount]; lastPos = pos = 0; // Now scan for each path component while ((pos = cp.indexOf(File.pathSeparator, lastPos)) != -1) { if (pos - lastPos > 0) { path[count++] = new File(cp.substring(lastPos, pos)); } else { // empty path component translates to "." path[count++] = new File("."); } lastPos = pos + 1; } // Make sure we include the last path component if (lastPos < cp.length()) { path[count++] = new File(cp.substring(lastPos)); } else { path[count++] = new File("."); } // Trim array to correct size if (count != maxCount) { File[] tmp = new File[count]; System.arraycopy(path, 0, tmp, 0, count); path = tmp; } } else { path = new File[0]; } // DEBUG //for (int i = 0; i < path.length; i++) { // System.out.println("path[" + i + "] = " + '"' + path[i] + '"'); //} return path; } private static URLStreamHandler fileHandler; static URL getFileURL(File file) { try { file = file.getCanonicalFile(); } catch (IOException e) {} try { return ParseUtil.fileToEncodedURL(file); } catch (MalformedURLException e) { // Should never happen since we specify the protocol... throw new InternalError(e); } } /* * The stream handler factory for loading system protocol handlers. */ private static class Factory implements URLStreamHandlerFactory { private static String PREFIX = "sun.net.www.protocol"; public URLStreamHandler createURLStreamHandler(String protocol) { String name = PREFIX + "." + protocol + ".Handler"; try { Class<?> c = Class.forName(name); return (URLStreamHandler)c.newInstance(); } catch (ReflectiveOperationException e) { throw new InternalError("could not load " + protocol + "system protocol handler", e); } } } } class PathPermissions extends PermissionCollection { // use serialVersionUID from JDK 1.2.2 for interoperability private static final long serialVersionUID = 8133287259134945693L; private File path[]; private Permissions perms; URL codeBase; PathPermissions(File path[]) { this.path = path; this.perms = null; this.codeBase = null; } URL getCodeBase() { return codeBase; } public void add(java.security.Permission permission) { throw new SecurityException("attempt to add a permission"); } private synchronized void init() { if (perms != null) return; perms = new Permissions(); // this is needed to be able to create the classloader itself! perms.add(SecurityConstants.CREATE_CLASSLOADER_PERMISSION); // add permission to read any "java.*" property perms.add(new java.util.PropertyPermission("java.*", SecurityConstants.PROPERTY_READ_ACTION)); AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { for (int i=0; i < path.length; i++) { File f = path[i]; String path; try { path = f.getCanonicalPath(); } catch (IOException ioe) { path = f.getAbsolutePath(); } if (i == 0) { codeBase = Launcher.getFileURL(new File(path)); } if (f.isDirectory()) { if (path.endsWith(File.separator)) { perms.add(new FilePermission(path+"-", SecurityConstants.FILE_READ_ACTION)); } else { perms.add(new FilePermission( path + File.separator+"-", SecurityConstants.FILE_READ_ACTION)); } } else { int endIndex = path.lastIndexOf(File.separatorChar); if (endIndex != -1) { path = path.substring(0, endIndex+1) + "-"; perms.add(new FilePermission(path, SecurityConstants.FILE_READ_ACTION)); } else { // XXX? } } } return null; } }); } public boolean implies(java.security.Permission permission) { if (perms == null) init(); return perms.implies(permission); } public java.util.Enumeration<Permission> elements() { if (perms == null) init(); synchronized (perms) { return perms.elements(); } } public String toString() { if (perms == null) init(); return perms.toString(); } }
- 从源码中能够解释 几种类加载器的双亲调用原理。
- 当前线程设置上下文的类加载器。AppCLassLoader
看懂上图,就能知道。CLassLoader类中的run()方法
- 系统属性的设置,导致控制系统类加载器的变更。
- 为什么自定义的类加载器,要提供一个单参为CLassLoader对象的构造方法。
- 并且默认的,自定义的类加载器是由系统类加载器加载的。
forName()方法底层剖析
Copy
/** * Returns the {@code Class} object associated with the class or * interface with the given string name, using the given class loader. * Given the fully qualified name for a class or interface (in the same * format returned by {@code getName}) this method attempts to * locate, load, and link the class or interface. The specified class * loader is used to load the class or interface. If the parameter * {@code loader} is null, the class is loaded through the bootstrap * class loader. The class is initialized only if the * {@code initialize} parameter is {@code true} and if it has * not been initialized earlier. * * <p> If {@code name} denotes a primitive type or void, an attempt * will be made to locate a user-defined class in the unnamed package whose * name is {@code name}. Therefore, this method cannot be used to * obtain any of the {@code Class} objects representing primitive * types or void. * * <p> If {@code name} denotes an array class, the component type of * the array class is loaded but not initialized. * * <p> For example, in an instance method the expression: * * <blockquote> * {@code Class.forName("Foo")} * </blockquote> * * is equivalent to: * * <blockquote> * {@code Class.forName("Foo", true, this.getClass().getClassLoader())} * </blockquote> * * Note that this method throws errors related to loading, linking or * initializing as specified in Sections 12.2, 12.3 and 12.4 of <em>The * Java Language Specification</em>. * Note that this method does not check whether the requested class * is accessible to its caller. * * <p> If the {@code loader} is {@code null}, and a security * manager is present, and the caller's class loader is not null, then this * method calls the security manager's {@code checkPermission} method * with a {@code RuntimePermission("getClassLoader")} permission to * ensure it's ok to access the bootstrap class loader. * * @param name fully qualified name of the desired class //类名 * @param initialize if {@code true} the class will be initialized. * See Section 12.4 of <em>The Java Language Specification</em>. //是否初始化 * @param loader class loader from which the class must be loaded //类加载器 * @return class object representing the desired class * * @exception LinkageError if the linkage fails * @exception ExceptionInInitializerError if the initialization provoked * by this method fails * @exception ClassNotFoundException if the class cannot be located by * the specified class loader * * @see java.lang.Class#forName(String) * @see java.lang.ClassLoader * @since 1.2 */ @CallerSensitive public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException { Class<?> caller = null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { // Reflective call to get caller class is only needed if a security manager // is present. Avoid the overhead of making this call otherwise. caller = Reflection.getCallerClass();//这里是重点。获取到调用forName()方法的对象 if (sun.misc.VM.isSystemDomainLoader(loader)) { ClassLoader ccl = ClassLoader.getClassLoader(caller);//获取到这个对象的加载器 if (!sun.misc.VM.isSystemDomainLoader(ccl)) { sm.checkPermission( SecurityConstants.GET_CLASSLOADER_PERMISSION); } } } return forName0(name, initialize, loader, caller); }
线程上下文类加载#
作用:为了解决双亲委托模型无法解决的问题。
Copy
Thread.currentThread().setContextClassLoader(sys);
Thread类中有一个 getContextClassLoader()方法。是抽象方法,必须由线程调用。
运行结果:
AppCLassLoader
null
线程上下文类加载器意味着什么?
当前类加载器(Current CLassLoader):用于加载当前类的类加载器
每一个类都会尝试去使用他自己的类加载器(即加载自身的类加载器)去加载他所依赖的其他的类。如果ClassX引用了ClassY,那么Class类的类加载器就会去加载ClassY(前提是ClassY尚未被加载)。
线程上下文类加载器 (Context CLassLoader)
线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextCLassLoader()
与setContextClassLoader(CLassLoader c1)
分别用来获取和设置上下文类加载器。
如果没有通过SetContextCLassLoader(CLassLoader c1)进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行时的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源。
线程上下文类加载器的重要性
Copy
伪代码:JDBC Class.forNmae("com.mysql.dirver.Driver"); Connection conn = Driver.getConnection(); Statement st = conn.getStatement(); jdk提供接口,如 Connection,Statement 厂商提供实现类
SPI(Service Provider Interface) 服务提供者接口。
把厂商实现的具体的jar包放在Classpath下面就行了。但是父加载器加载的类,根本无法看到子加载器加载的类。所以线程上下文类加载器的重要性就体现出来了。
父CLassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的CLassLoader加载的类。这就改变了父CLassLoader或是其他没有直接父子关系的CLassLoader加载的类的情况。
即改变了双亲委托模型。
线程上下文类加载器就是当前线程的Current CLassLoader。
在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是java核心库所提供的,而java核心库是由启动类加载器加载的,而这些接口的实现却是来自不同的jar包(厂商提供,如JDBC),java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托机制就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。
日常开发用到的估计很少,但是对于框架的开发,服务器的开发。
再次补充说明:JDK提供的标准接口,肯定会调用厂商实现的对象,加载厂商提供的类,来进行对象的实例化。
JDK的启动类加载器,父类,无法加载子类,所以要用到线程类加载器。
除了JDBC,JNDI,JAXP等等的SPI,都肯定是 接口要调用具体实现的。 默认的双亲委托机制是无法加载子类的。(之前就是自己可能无法深入理解过这样的实现,因为被JDK完全很好的做到了。)这也是双亲委托机制存在的缺陷,也正好是线程类加载器存在的原因和意义。
还有就是Tomcat的具体的实现:完全打破了传统的双亲委托模型。Tomcat是先自己加载,自己加载不了之后再委托父类的加载器去加载。传统的是父类先加载,父类加载不了之后再加载。(学习完现有知识之后的拓展。)
用实例来说明一些问题。
运行结果:
AppCLassLoader
ExtCLassLoader
说明:对于当前线程来说:他的类加载器就是当前的类加载器。
因为:(初始线程的上下文类加载器是系统类加载器)
因为:源码面前,没有秘密:源码中设置过当前线程的类加载器。
线程上下文类加载器的一般使用模式(获取-使用-还原)
框架中经常使用。
Copy
//伪代码说明 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();//获取 try{ Thread.currentThread().setContextClassLoader(targetTcc1);//将你想要使用的类加载器,放入 myMethod();//方法中,调用getContextClassLoader(targetTcc1)使用 }finally{ Thread.currentThread().setContextClassLoader(classLoader); //还原 } myMythod里面则调用了Thread.currentThread().getContextClassLoader();获取当前线程的上下文类加载器 //用完之后一定要还原。
-
如果一个类是由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载的(如果该依赖类之前没有被加载过的话)。所以:ContextCLassLoader 的作用就是为了破坏Java的类加载委托机制。
-
当高层提供了统一的接口让低层去实现,同时又要在高层加载(或者实例化)底层的类时,就必须要通过线程上下文类加载来帮助高层的CLassLoader来到并加载该类。
-
所以不管当前类处于任何环境目录下(在运行的时候都能用系统类加载器去加载当前类)。java代码都是在线程的情况下实现的。让你随时随地的使用当前线程的类加载器去加载你需要加载的类。这种实现是非常巧妙的。
可以找一些框架,查看他们的上下文类加载器的实现原理,基本上都是通过这种方式实现的。
扩展:ThreadLocal的实现原理:用空间换时间。 有多少个线程就创建多少个副本,就不用使用同步锁,就实现了数据同步。
ServiceLoader在SPI中的重要作用分析
案例:基于MySQL的驱动去测试。
Copy
//加载MySQL驱动。
输出结果:
因为只提供了一个Driver接口,就能找到所有实现的驱动,一定是在某个地方,隐藏着默认的配置。到某个地方就能找到相应的设置。厂商提供者就是按照这样的默认配置去开发的子类的实现。
想要知道上述原因,就要阅读以下ServiceLoader类的源码
Copy
/** * A simple service-provider loading facility. 一个简单的,服务提供者的加载设施 * * <p> A <i>service</i> is a well-known set of interfaces and (usually * abstract) classes. A <i>service provider</i> is a specific implementation * of a service. The classes in a provider typically implement the interfaces * and subclass the classes defined in the service itself. Service providers * can be installed in an implementation of the Java platform in the form of * extensions, that is, jar files placed into any of the usual extension * directories. Providers can also be made available by adding them to the * application's class path or by some other platform-specific means. 一个service是一组已知的接口和(通常 抽象)类。一个<i>服务提供者</i>是一个特定的实现服务。 提供者中的类通常实现接口并子类化服务本身中定义的类。 服务提供商可以安装在Java平台的实现中,形式为扩展名,也就是说,jar文件被放置到任何常用的扩展名中目录。 提供程序也可以通过将它们添加到应用程序的类路径或其他特定于平台的方法。 * * <p> For the purpose of loading, a service is represented by a single type, * that is, a single interface or abstract class. (A concrete class can be * used, but this is not recommended.) A provider of a given service contains * one or more concrete classes that extend this <i>service type</i> with data * and code specific to the provider. The <i>provider class</i> is typically * not the entire provider itself but rather a proxy which contains enough * information to decide whether the provider is able to satisfy a particular * request together with code that can create the actual provider on demand. * The details of provider classes tend to be highly service-specific; no * single class or interface could possibly unify them, so no such type is * defined here. The only requirement enforced by this facility is that * provider classes must have a zero-argument constructor so that they can be * instantiated during loading. 为了加载,服务由单个类型表示,即单个接口或抽象类。(可以使用具体类,但不建议这样做。)给定服务的提供程序包含一个或多个具体类,这些类使用特定于提供程序的数据和代码扩展此服务类型。提供者类通常不是整个提供者本身,而是一个代理,它包含足够的信息来决定提供者是否能够满足特定的请求,以及能够根据需要创建实际提供者的代码。提供者类的细节往往是高度特定于服务的;没有一个类或接口可能统一它们,所以这里没有定义这样的类型。此功能强制的惟一要求是,提供程序类必须具有零参数构造函数,以便在加载期间实例化它们。 * 重点:服务提供者 通过某种方式,让JDK认识厂商提供的类。 * <p><a name="format"> A service provider is identified by placing a * <i>provider-configuration file</i> in the resource directory * <tt>META-INF/services</tt>.</a> The file's name is the fully-qualified <a * href="../lang/ClassLoader.html#name">binary name</a> of the service's type. * The file contains a list of fully-qualified binary names of concrete * provider classes, one per line. Space and tab characters surrounding each * name, as well as blank lines, are ignored. The comment character is * <tt>'#'</tt> (<tt>'\u0023'</tt>, * <font style="font-size:smaller;">NUMBER SIGN</font>); on * each line all characters following the first comment character are ignored. * The file must be encoded in UTF-8. 通过将提供程序配置文件放在资源目录META-INF/services中来标识服务提供程序。文件的名称是服务类型的完全限定二进制名称。该文件包含具体提供程序类的完全限定二进制名称列表,每行一个。每个名称周围的空格和制表符以及空行都将被忽略。注释字符是'#' ('\u0023',数字符号);在每一行中,第一个注释字符后面的所有字符都被忽略。该文件必须用UTF-8编码。 * * <p> If a particular concrete provider class is named in more than one * configuration file, or is named in the same configuration file more than * once, then the duplicates are ignored. The configuration file naming a * particular provider need not be in the same jar file or other distribution * unit as the provider itself. The provider must be accessible from the same * class loader that was initially queried to locate the configuration file; * note that this is not necessarily the class loader from which the file was * actually loaded. 如果某个特定的具体提供程序类在多个配置文件中命名,或者在同一配置文件中多次命名,则会忽略重复项。命名特定提供程序的配置文件不必与提供程序本身位于相同的jar文件或其他分发单元中。提供程序必须可以从最初查询以定位配置文件的类装入器访问;注意,这不一定是实际加载文件的类加载器。 * * <p> Providers are located and instantiated lazily, that is, on demand. A * service loader maintains a cache of the providers that have been loaded so * far. Each invocation of the {@link #iterator iterator} method returns an * iterator that first yields all of the elements of the cache, in * instantiation order, and then lazily locates and instantiates any remaining * providers, adding each one to the cache in turn. The cache can be cleared * via the {@link #reload reload} method. * * <p> Service loaders always execute in the security context of the caller. * Trusted system code should typically invoke the methods in this class, and * the methods of the iterators which they return, from within a privileged * security context. * * <p> Instances of this class are not safe for use by multiple concurrent * threads. * * <p> Unless otherwise specified, passing a <tt>null</tt> argument to any * method in this class will cause a {@link NullPointerException} to be thrown. * * * <p><span style="font-weight: bold; padding-right: 1em">Example</span> * Suppose we have a service type <tt>com.example.CodecSet</tt> which is * intended to represent sets of encoder/decoder pairs for some protocol. In * this case it is an abstract class with two abstract methods: * * <blockquote><pre> * public abstract Encoder getEncoder(String encodingName); * public abstract Decoder getDecoder(String encodingName);</pre></blockquote> * * Each method returns an appropriate object or <tt>null</tt> if the provider * does not support the given encoding. Typical providers support more than * one encoding. * * <p> If <tt>com.example.impl.StandardCodecs</tt> is an implementation of the * <tt>CodecSet</tt> service then its jar file also contains a file named * * <blockquote><pre> * META-INF/services/com.example.CodecSet</pre></blockquote> * * <p> This file contains the single line: * * <blockquote><pre> * com.example.impl.StandardCodecs # Standard codecs</pre></blockquote> * * <p> The <tt>CodecSet</tt> class creates and saves a single service instance * at initialization: * * <blockquote><pre> * private static ServiceLoader<CodecSet> codecSetLoader * = ServiceLoader.load(CodecSet.class);</pre></blockquote> * * <p> To locate an encoder for a given encoding name it defines a static * factory method which iterates through the known and available providers, * returning only when it has located a suitable encoder or has run out of * providers. * * <blockquote><pre> * public static Encoder getEncoder(String encodingName) { * for (CodecSet cp : codecSetLoader) { * Encoder enc = cp.getEncoder(encodingName); * if (enc != null) * return enc; * } * return null; * }</pre></blockquote> * * <p> A <tt>getDecoder</tt> method is defined similarly. * * * <p><span style="font-weight: bold; padding-right: 1em">Usage Note</span> If * the class path of a class loader that is used for provider loading includes * remote network URLs then those URLs will be dereferenced in the process of * searching for provider-configuration files. * * <p> This activity is normal, although it may cause puzzling entries to be * created in web-server logs. If a web server is not configured correctly, * however, then this activity may cause the provider-loading algorithm to fail * spuriously. * * <p> A web server should return an HTTP 404 (Not Found) response when a * requested resource does not exist. Sometimes, however, web servers are * erroneously configured to return an HTTP 200 (OK) response along with a * helpful HTML error page in such cases. This will cause a {@link * ServiceConfigurationError} to be thrown when this class attempts to parse * the HTML page as a provider-configuration file. The best solution to this * problem is to fix the misconfigured web server to return the correct * response code (HTTP 404) along with the HTML error page. * * @param <S> * The type of the service to be loaded by this loader * * @author Mark Reinhold * @since 1.6 */ public final class ServiceLoader<S> implements Iterable<S> { ... }
去MySQL驱动中:找JDK中说明的:META-INF/services/
目录里的指定的驱动的名字:
具体文件内容:
这两个结果就说明了上面程序输出结果的原因。
源码面前,了无秘密。
手动修改上下文加载器,然后运行。
就找不到了。因为没加载成功。拓展类加载器加载不到厂商提供的自定义的类。
同时:加上JVM打印追踪日志参数:
这就,就彻底理解了。
但是如果只告诉结论说上下文类加载的作用和概念,没有这些源码的追踪和案例的话,就不会理解的这么透彻了。
学完之后,应该如果以后别的同事或者面试官,问关于任何JVM类加载器的问题,不应该有任何的知识盲区了。
虽然没有将Tomcat等其他扩展程序,但是如果你去稍微的跟踪一下代码,就很容易的就理解了他们在这方面的实现的原理和具体实现。
通过JDBC驱动加载,深刻理解线程上下文类加载器。#
最后一个关于加载器的案例:通过JDBC驱动加载,深刻理解线程上下文类加载器。
要求:通过这两行代码,去追底层的实现原理。不要小看这两行。底层涉及了整个类加载器机制的全过程和之前讲过的所有的知识点,如初始化,加载,准备,类加载器等等。
Copy
关于:Class.forName("com.mysql.jdbc.Driver");
初始化java.sql.DriverManager
这个类的时候,会初始化里面的静态代码块。
loadInitialDriver()方法会被初始化。
自己DeBug,跟一下代码。 这一块的内容就到这里。
然后返回,最初的。初始化块。接下来的registerDriver()
这个registeredDrivers的变量是什么?
包装类不用细看,是已经注册的Driver的包装类。
到这里:第一行代码的重点基本上就到这里。然后主要是第二行代码的具体实现。
- DriverManager类(启动类加载器加载的JDK提供的类)
第二行,根本看不到MySQL具体的实现。那是怎么获取到MySQL的连接呢?
那就来看看这个类的这个方法的源码:
Copy
/** * Attempts to establish a connection to the given database URL. * The <code>DriverManager</code> attempts to select an appropriate driver from * the set of registered JDBC drivers. *<p> * <B>Note:</B> If the {@code user} or {@code password} property are * also specified as part of the {@code url}, it is * implementation-defined as to which value will take precedence. * For maximum portability, an application should only specify a * property once. * 试图建立到给定数据库URL的连接。 在DriverManager尝试选择从已注册的JDBC驱动程序的相应驱动程序。 注意:如果user或password财产也被指定为部分url ,它是实现定义哪个值将优先考虑。 为了最大的可移植性,应用程序应该只指定一次的属性。 * @param url a database url of the form * <code>jdbc:<em>subprotocol</em>:<em>subname</em></code> * @param user the database user on whose behalf the connection is being * made * @param password the user's password * @return a connection to the URL * @exception SQLException if a database access error occurs or the url is * {@code null} * @throws SQLTimeoutException when the driver has determined that the * timeout value specified by the {@code setLoginTimeout} method * has been exceeded and has at least tried to cancel the * current database connection attempt */ @CallerSensitive public static Connection getConnection(String url, String user, String password) throws SQLException { java.util.Properties info = new java.util.Properties(); if (user != null) { info.put("user", user); } if (password != null) { info.put("password", password); } return (getConnection(url, info, Reflection.getCallerClass())); }
试图建立到给定数据库URL的连接。 在DriverManager尝试选择从已注册的JDBC驱动程序的相应驱动程序。
这句话是重点。从已经注册了的JDBC驱动程序中选择。
重点在 另外一个.另外一个三参数的getConnection。
注意:Reflection.getCallerClass():获取到 调用这个方法的类的实例
Copy
// Worker method called by the public getConnection() methods. private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;//获取到的是系统类加载器 synchronized(DriverManager.class) { // synchronize loading of the correct classloader. if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } if(url == null) { throw new SQLException("The url cannot be null", "08001"); } println("DriverManager.getConnection(\"" + url + "\")"); // Walk through the loaded registeredDrivers attempting to make a connection. // Remember the first exception that gets raised so we can reraise it. //遍历已经注册的驱动。 通过Debug可以看到值。 SQLException reason = null; for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) {//特别重要的代码。 try { println(" trying " + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } // if we got here nobody could connect. if (reason != null) { println("getConnection failed: " + reason); throw reason; } println("getConnection: no suitable driver found for "+ url); throw new SQLException("No suitable driver found for "+ url, "08001"); }
通过Debug知道,获取到的是我们自己写的应用类。
现在Class.forName()都不用写了,会自己加载。因为符合SPI规范的都能够被自动加载。
用到的特别重要的方法:isDriverAllowed,判断是否为被允许的Driver
Copy
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) { boolean result = false; if(driver != null) { Class<?> aClass = null; try { //用系统类加载器,加载MySQL的驱动,加载的同时去初始化它。 //加载不是目的,目的是判断true或者false。 aClass = Class.forName(driver.getClass().getName(), true, classLoader); } catch (Exception ex) { result = false; } //加载不是目的,目的是判断true或者false。判断不同命名空间的类名相同,但是不是同一个类 result = ( aClass == driver.getClass() ) ? true : false; } return result; }
//加载不是目的,目的是判断true或者false。
判断不同命名空间的类名相同,但是,不是同一个类
也就是说,要确保被同一个类加载器加载。
Copy
result = ( aClass == driver.getClass() ) ? true : false;
就是为了避免命名空间不同
再往下:判断完之后,具体的获取驱动器连接 是接下来的代码;
再往下调用的话,就追究到 提供尝试提供的 Driver的实现了。
有没有自己这样跟代码? 问到你的内心灵魂深处。
如果你是自己追的代码,那么你就掌握了追源码的技能。
如果你没有自己追代码,那么你就只掌握了别人给你讲的那单一的一个技能点。
授人以鱼不如授人以渔。