类加载机制指虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的。
在运行期间的好处是更加灵活,增加了更多的可能性。如反射、动态代理等。
类的生命周期包括:加载、验证、准备、解析(Resolution)、初始化、使用和卸载 7 个阶段。
Class
文件)注意:并不意味着执行类的加载就一定会进行连接、初始化。
类的加载指的是将类的Class
文件中读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class
对象。
类加载的时机
Java
虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
对Class
文件的内容进行验证,需要其符合Java
的语法和要求。
准备阶段为类变量在方法区中分配内存并初始化。注意这里只是对类变量,不包含实例变量。对类变量的初始化是指初始化为类型默认值。
如果类变量为编译期常量,那么JVM
在准备阶段便可以直接赋值。
解析阶段指虚拟机将常量池内的符号引用替换为直接引用的过程,这样我们便可以直接通过对象或类调用其成员。
符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量。
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
类初始化阶段是类加载过程的最后一步,初始化阶段,才真正开始执行类中定义的Java程序代码.
public static int a = 1;
在准备阶段a
被复制为0,在初始化阶段,a
被赋值为1,所以初始化阶段才算真正执行按我们意愿所进行的初始化操作.
Java虚拟机对于类初始化的时机进行指定了严格的规定。所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
Java程序对类的使用分为两种:
final
修饰的编译器常量除外)main
方法的类java.lang.invoke.MethodHandle
实例的解析结果REF_getStatic
,REF_putStatic
,REF_invokeStatic
,REF_newInvokeSpecial
四种类型的方法句柄,并且句柄对应的类没有初始化,则初始化。default
注解修饰),那么其自其实例类被初始化之前初始化。除了上述7种情况下,其他使用Java类的方式都被看作是对类的被动使用(可能会加载、连接类也不可能不会),都不会导致类的初始化。
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 MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str);
}
}
输出:
MyParent1 static block
hello world
对于静态字段来说,只有直接定义了该字段的类才会被初始化,不管是谁调用,都只会导致该字段所在的类被初始化。 为什么MyChild1
没有被初始化呢?这是因为对MyChild1
的调用属于被动调用。
public class MyTest4 {
public static void main(String[] args) {
MyParent4[] myParent4s = new MyParent4[5];
System.out.println(myParent4s.getClass());
// class [Lcom.wangzhao.jvm.classloader.MyParent4;
MyParent4[][] myParent4s2 = new MyParent4[5][1];
System.out.println(myParent4s2.getClass());
// class [[Lcom.wangzhao.jvm.classloader.MyParent4;
System.out.println(myParent4s.getClass().getSuperclass());
// class java.lang.Object
System.out.println(myParent4s2.getClass().getSuperclass());
// class java.lang.Object
int[] array = new int[1];
System.out.println(array.getClass());
// class [I
System.out.println(array.getClass().getSuperclass());
// class java.lang.Object
}
}
class MyParent4{
static{
System.out.println("MyParent4 static block");
}
}
通过输出可以看到并没有输出 MyParent4 static block
,这是因为创建MyParent4
类型的数组,并不属于对MyParent4
的主动使用,在上述主动使用中并不包含这种情况.
对于数组实例来说,其类型是由JVM在运行期动态生成的。
我们更改被动使用示例一的main
方法,如下所示:
public class MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str2);
}
}
输出:
MyParent1 static block
MyChild1 static block
welcome
当一个类在初始化时,要求其父类全部都已经初始化完毕了.
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
}
}
interface MyParent5{
public static Thread thread = new Thread(){
{
//实例化代码块
System.out.println("MyParent 5 invoked ");
}
};
}
interface MyChild5 extends MyParent5{
public static final int b = new Random().nextInt(4);
}
输出:
0
在初始化一个接口时,并不会先初始化他的父接口
class MyParent2{
public static String str = "hello world";
static {
System.out.println("MyParent2 static block");
}
}
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
想必你能很快的猜出答案,这属于对MyParent2
的主动使用,所以输出:
MyParent2 static block
hello world
但是如果我们将str
改为常量会输出什么?
class MyParent2{
public static final String str = "hello world";
static {
System.out.println("MyParent2 static block");
}
}
输出:
hello world
这里并没有输出 MyParent2 static block
,没道理啊,MyParent2.str
属于对MyParent2
的主动使用啊。
这是因为常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。
可以明显看到,该Class
文件并没有MyParent2
的任何信息.
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
4ef5aa3a-6773-4678-858d-08e8e108d742
当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,导致这个类被初始化。
public class MyTest6 {
public static void main(String[] args) {
Singleton.getInstance();
System.out.println(Singleton.counter1);
System.out.println(Singleton.counter2);
}
}
class Singleton{
public static int counter1;
public static int counter2 = 0;
public static Singleton singleton = new Singleton();
private Singleton(){
counter1++;
counter2++;
System.out.println("静态代码块 counter1: " + counter1);
System.out.println("静态代码块 counter2: " + counter2);
}
public static Singleton getInstance(){
return singleton;
}
}
输出:
静态代码块 counter1: 1
静态代码块 counter2: 1
1
1
这是因为当调用 getInstance()
时,属于对Singleton
类的主动使用,所以会进行初始化。
在初始化前counter1
、counter2
、singleton
的值分别在准备阶段被赋值为0
、0
和null
。然后开始至上向下初始化,先对counter1
初始化,因为并没有进行赋值,所以使用默认值。然后对counter2
,这里将0
赋值给counter2
。当初始化singleton
时,调用构造方法,对初始化后的counter1
和counter2
进行递增操作。
如果我们更改counter2
的顺序,输出会如何?
class Singleton{
public static int counter1;
public static Singleton singleton = new Singleton();
private Singleton(){
counter1++;
counter2++;
System.out.println("静态代码块 counter1: " + counter1);
System.out.println("静态代码块 counter2: " + counter2);
}
public static int counter2 = 0;
public static Singleton getInstance(){
return singleton;
}
}
输出:
静态代码块 counter1: 1
静态代码块 counter2: 1
1
0
答案是不是有点出乎你的意料。同样的初始化阶段和之前一样,接着开始初始化阶段。counter1
依然使用准备阶段的默认值,接着是对singleton
的初始化(因为初始化是自上而下的)。进入其构造方法,首先执行counter1++
操作(此时counter1
使用的是初始化后的值),counter1 = 1
;接着进行counter2++
操作(注意这里使用的是counter2
准备阶段的值),counter2 = 1
。
注意构造方法结束并不意味着初始化阶段结束,因为counter2
还没有被初始化。
接着开始进行counter2
的初始化,counter2
由准备阶段的1
变为0
。
准备阶段的重要意义:如果类变量还未初始化,对类变量进行类似++
的操作,没有默认值是不是会报错。
通过一个类的全限定名来获取描述此类的二进制字节流,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块成为"类加载器"。
这个类加载器负责将存在在
目录中的,或者被-Xbootclasspath
参数所指定的路径,并且能够被虚拟机识别的类库(如rt.jar
)加载到虚拟机内存中。
这个类加载器由sun.misc.Launcher$ExtClassLoader
实现,负责加载
目录中的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库。
这个类加载器由 sun.misc.Launcher$AppClassLoader
实现,负责加载用户类路径(ClassPath
) 上所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是默认的类加载器。
双亲委派模型可以理解为类加载时,类加载被调用顺序一种机制。
上图所展示的类加载器之间的关系层次如上所示,该关系成为类加载器的双亲委派模型。需要注意的是,以上类加载器直接的关系并非继承。可观察继承图如下所示:
可以明显看到APPClassLoader
以及ExtClassLoader
都是继承自URLClassLoader
。同时我们也可以看到并没有BootstrapClassLoader
,这是因为BootstrapClassLoader
是由C++
编写而成的,与Java
的类并没有任何关系。同样的,我们也无法获取该类的实例。
获取BootstrapClassLoader
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("java.lang.String");
System.out.println(clazz);
System.out.println(clazz.getClassLoader());
}
输出如下所示
class java.lang.String
null
可以观察到,如果我们获取BootstrapClassLoader
时,返回的是null
。
获取AppClassLoader
class C{
public static void main(String[] args) throws ClassNotFoundException {
Class<?> c = Class.forName("com.wangzhao.jvm.classloader.C");
System.out.println(c);
System.out.println(c.getClassLoader());
}
}
输出如下所示
class com.jvm.classloader.C
sun.misc.Launcher$AppClassLoader@b4aac2
可以观察到,我们自定义类是使用AppClassLoader
进行加载的,其属于Launcher
类下的一个内部类。
如果一个类加载器收到了类加载的请求,它首先不会尝试加载这个类,而是将请求委托给其父类。直到没有父类为止,否则一直委托给父类。接着父类开始再其资源路径下尝试加载这个类,如果加载不到。那么回到子类去加载,如果最后的子类也不能加载到,那么程序抛出异常。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个类有没有被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果存在父加载器,则让父加载器去加载这个类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父加载器为null的话,则使用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果一直到启动类加载器,该类依然没有被加载
// 那么在该类加载器对应的路径下去寻找,如果找不到,再去子加载器的路径寻找
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
首先可以明显看出,类加载的过程与上面的时序图过程是相同的。
其次,我们在书写单例模式的时候,有如下一种方式
public class Singleton {
public static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
我们在书写单例模式的时候,都听过类加载的过程是线程安全的,但是不知道你有没有看过其源码,为什么是线程安全的?因为类加载的过程被synchronized
所修饰。
类加载器的作用都是对Class
文件进行加载,那么为什么不直接使用最底层那个类加载器去加载不就可以了吗?
一个显而易见的好处是,Java
类随着他的类加载器一起具备了一种带有优先级的层次关系。
以 java.lang.Object
为例,最终是由BootstrapClassLoader
进行的加载,所以Object
类在程序的各种类加载器都是同一个类。如果没有双亲委派机制的话,用户自己编写了一个称为java.lang.Object
的类,那么程序中将会出现多个不同的Object
类,程序将会很混乱。
如下代码所示
public class Object {
}
class ObjectClassLoaderTest{
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("java.lang.Object");
System.out.println(clazz);
}
}
程序运行后抛出异常信息如下所示:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
双亲委派机制保证了一个类在程序中是由一个类加载器进行加载,且这种加载是具有优先级的。
clazz.getClassLoader()
Thread.currentThread().getContextClassLoader()
ClassLoader.getSystemClassLoader()
public class MyTest8 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> clazz = loader.loadClass("com.wangzhao.jvm.classloader.CL");
System.out.println(clazz);
System.out.println("=============");
clazz = Class.forName("com.wangzhao.jvm.classloader.CL");
System.out.println(clazz);
}
}
输出
class com.wangzhao.jvm.classloader.CL
=============
CL static block
class com.wangzhao.jvm.classloader.CL
通过上面的输出可以看到,类加载器加载类并不是对类的主动使用,所以不会初始化类。
public class MyClassLoader extends ClassLoader{
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
System.out.println(fileName);
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
}
public class MyTest11 extends ClassLoader {
private String classLoaderName;
private static final String FILE_EXTENSION = ".class";
public MyTest11(String classLoaderName){
super(); // 将系统类加载器设置为父加载器
this.classLoaderName = classLoaderName;
}
public MyTest11(ClassLoader parent,String classLoaderName){
super(parent); // 显式指定父加载器
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return defineClass(name,data,0,data.length);
}
public byte[] loadClassData(String className) throws ClassNotFoundException {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
String fileName = className.replace(".","/")+FILE_EXTENSION;
is = new FileInputStream(new File(fileName));
int len = 0;
while((len = is.read())!=-1){
baos.write(len);
}
data = baos.toByteArray();
System.out.println(123);
return data;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public static void test(ClassLoader classLoader) throws Exception {
System.out.println(classLoader.getParent());
Class<?> clazz = classLoader.loadClass("com.wangzhao.jvm.classloader.MyTest1");
Object obj = clazz.newInstance();
System.out.println(obj);
}
public static void main(String[] args) throws Exception{
MyTest11 loader1 = new MyTest11("loader1");
test(loader1);
}
这里我们谈一下这两种自定义有何区别,当使用第一种时,我们直接调用我们重写后的loadClass()
方法。但是对于第二种来说,我们相当与重写了findClass()
,这种情况下,该方法会在ClassLoader
的loadClass()
中调用。如果其父加载器可以进行类的加载,那么就不会执行我们重写后的findClass()
,因为在前面说过,双亲委派模型并不是继承关系,这样的加载方式更符合双亲委派机制。
例如有如下一段代码(前提,该项目类路径下并没有这个Class
文件),那么这两个类加载器加载的这个类相等吗?
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz);
System.out.println(clazz.hashCode());
Object obj = clazz.newInstance();
MyTest11 loader2 = new MyTest11("loader2");
loader2.setPath("/home/wangzhao/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz2);
System.out.println(clazz2.hashCode());
Object obj2 = clazz2.newInstance();
System.out.println(obj2);
}
输出如下
findClass
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
findClass
24079912
com.wangzhao.jvm.classloader.MyTest11@14ae5a5
通过输出我们可以清楚看到这个类执行了我们自定义的findClass
方法,也就是说类加载器是我们的自定义类。但是可以明显看到这两个类的hashCode
不同,在类加载器不同的情况下,也就是说这两个类对象是不同的。
那么如果我们在项目的类路径下添加这个文件会输出什么呢?
10568834
sun.misc.Launcher$AppClassLoader@b4aac2
10568834
sun.misc.Launcher$AppClassLoader@b4aac2
可以看到,如果类加载器是同一个的话,那么类对象是相等的。
定义如下类
public class MyPerson {
private MyPerson myPerson;
public void setMyPerson(Object obj){
this.myPerson = (MyPerson)obj;
}
}
执行下面的代码
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
MyTest11 loader2 = new MyTest11("loader2");
Class<?> clazz1 =loader1.loadClass("com.wangzhao.jvm.classloader.MyPerson");
Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyPerson");
System.out.println(clazz1 == clazz2);
Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();
Method method = clazz1.getMethod("setMyPerson",Object.class);
method.invoke(obj1,obj2);
}
程序正常执行,这是因为clazz1
和clazz2
都是由AppClassLoader
加载。
对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java
虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即比较两个类是否“相等”,只有在这两个类是由同一个类加载器实例 加载的前提下才有意义,否则,即使这两个类来源于同一个Class
文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。
同样的在类路径下没有该class文件的前提下,运行如下代码
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz.hashCode());
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
MyTest11 loader2 = new MyTest11(loader1,"loader2");
loader2.setPath("/home/wangzhao/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz2.hashCode());
Object obj2 = clazz2.newInstance();
System.out.println(obj2.getClass().getClassLoader());
}
输出结果如下
findClass
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
通过输出可以看到,loader2
加载器并没有执行findClass()
,即MyTest1
被没有被loader2
加载。这是因为loader2
的父加载器loader1
已经加载了MyTest
1,通过双亲委托机制,当loader2
加载MyTest1
时,先让其父加载器加载。
存在下面这样一个类
public class MyPerson {
private MyPerson myPerson;
public void setMyPerson(Object obj){
this.myPerson = (MyPerson)obj;
}
}
运行下面的代码,前提类路径下存在MyPerson.class文件
public class MyTest14 {
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
MyTest11 loader2 = new MyTest11("loader2");
loader1.setPath("C:\\Users\\25519\\Desktop\\");
loader2.setPath("C:\\Users\\25519\\Desktop\\");
Class<?> clazz1 =loader1.loadClass("com.wangzhao.jvm.classloader.MyPerson");
Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyPerson");
System.out.println(clazz1 == clazz2);
Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();
Method method = clazz1.getMethod("setMyPerson",Object.class);
method.invoke(obj1,obj2);
}
输出如下
true
如果我们将类路径下的MyPerson文件删除后,放到桌面后。继续运行上面的程序,输出如下
findClass
findClass
false
Exception in thread "main" java.lang.reflect.InvocationTargetException
Caused by: java.lang.ClassCastException: com.wangzhao.jvm.classloader.MyPerson cannot be cast to com.wangzhao.jvm.classloader.MyPerson
我们分析一下为什么会出现这种情况,首先我们没有删除类路径下得MyPerson.class
文件时,MyTest11
的父加载器可以在类路径下找到Class
文件并加载,所以clazz1
和clazz2
是同一个对象。
而删除Class
文件后,是由我们自定义的类加载器所加载,但是loader1
和loader2
属于不同的实例,所以loader1
和loader2
的命名空间不同,所以clazz1
和clazz2
是不同的class对象。当对象实例属于不同得命名空间时,即使是由同一份Class
文件加载,当进行类型转换时(非继承、实现关系)一定会出现类型转换异常。
存在如下这些类
public class MyCat {
public MyCat(){
System.out.println("MyCat loaded by : " + this.getClass().getClassLoader());
}
}
public class MySample {
public MySample(){
System.out.println("MySample loaded by : "+this.getClass().getClassLoader());
new MyCat();
}
}
当我们执行如下的代码时
public class MyTest12 {
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MySample");
System.out.println("class: " + clazz.hashCode());
// 如果注释掉该行,那么并不会实例化MySample对象,即MySample构造方法不会被调用
// 因此不会实例化MyCat对象,即没有对MyCat进行主动使用,这里就不会加载MyCat Class
// 注意:没有对类进行主动使用,并不意味着一定不会加载Class文件
Object obj = clazz.newInstance();
}
}
输出如下
class: 21029277
MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
这是因为loader1
的父加载器AppCLassLoader
在类路径下可以找到MySample
的Class
文件,所以MySample
是由AppCLassLoader
所加载,而MyCat
位于MySample
的构造方法中,所以MyCat
是由MySanple
的类加载器或其父加载器去加载,因为AppClassLoader
可以在类路径下找到MyCat
的Class
文件,所以可以加载成功。
将MyCat.class
从项目路径下删除,放到桌面,保留MySample.class
在类路径下。执行下面的代码
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MySample");
System.out.println("class: " + clazz.hashCode());
Object obj = clazz.newInstance();
}
输出
class: 21029277
MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: com/wangzhao/jvm/classloader/MyCat
通过输出可以看到,MyCat
加载失败,这是因为MySample
由AppClassLoader
加载,所以MyCat也需要由AppClassLoader
加载,但是类路径下没有MyCat.class
,所以加载失败。
将MySample.class
从项目路径下删除,放到桌面,保留MyCat.class
在类路径下,继续执行上面的代码,输出
findClass
class: 21685669
MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
通过输出可以看到,当加载MySample
时,是由我们自己定义的类加载器加载的。这是因为我们自定义的类加载器的父加载器不可以加载MySample
,而我们自定义的类加载器可以加载,所以按照我们自定义类加载器的方式去加载。而加载MyCat
时,根据双亲委派机制,先由loader1
的父加载器去加载,而AppClassLoader
可以加载到MyCat
。
修改MyCat
的代码,如下所示
public class MyCat {
public MyCat() {
System.out.println("MyCat loaded by : " + this.getClass().getClassLoader());
System.out.println("from MyCat : " + MySample.class);
}
}
重新编译后,在类路径下保留MyCat.class
和MySample.class
后,输出如下
class: 21029277
MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
from MyCat : class com.wangzhao.jvm.classloader.MySample
如果将MySample.class
从类路径中删除,放到桌面后,输出如下
findClass
class: 21685669
MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: com/wangzhao/jvm/classloader/MySample
可以看到,获得MySample的class
对象失败这是因为MySample
由loader1
加载,而MyCat
是由其父加载器加载。在父加载器中看不到子加载器加载信息。
修改MySample和MyCat的代码如下所示
public class MyCat {
public MyCat() {
System.out.println("MyCat loaded by : " + this.getClass().getClassLoader());
//System.out.println("from MyCat : " + MySample.class);
}
}
public class MySample {
public MySample(){
System.out.println("MySample loaded by : "+this.getClass().getClassLoader());
// 由加载MySample的类加载器去加载MyCat
new MyCat();
System.out.println("from Sample : " + MyCat.class);
}
}
重新编译后运行,输出如下
class: 21029277
MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
from Sample : class com.wangzhao.jvm.classloader.MyCat
将MySample
从类路径下删除,放到桌面后,运行输出如下
findClass
class: 21685669
MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
from Sample : class com.wangzhao.jvm.classloader.MyCat
MySample
由loader1
加载,MyCat
由AppClassloader
加载。当在loader1
中获取MyCat
的class
对象时,子加载器可以访问到父加载器的加载信息,所以可以获取成功。
当某个类被加载、连接和初始化后,它的生命周期就开始了。当代表这个类的Class
对象不再被引用,即不可触及时,Class对象就会结束生命周期,这个类在方法区内的数据也会被卸载,从而结束该类的生命周期。
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java
虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java
虚拟机自身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class
对象,因此这些Class
对象始终是可触及的。
由用户自定义的类加载器所加载的类是可以被卸载的。
// -XX:+TraceClassUnloading输出有哪些类被卸载
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyTest11 loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz.hashCode());
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
// 去除引用,让垃圾回收器回收
loader1 = null;
clazz = null;
obj = null;
System.gc();
loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz.hashCode());
obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
}
输出如下
findClass
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
[Unloading class com.wangzhao.jvm.classloader.MyTest1 0xa43e4228]
findClass
24079912
com.wangzhao.jvm.classloader.MyTest11@14ae5a5
可以看到,findClass
输出两次,也就是说Class
对象被卸载了一次。
Java
核心库的类型安全。 所有的Java
应用都至少引用java.lang.Object
类,也就是说在运行期,java.lang.Object
这个类会被加载到 Java 虚拟机中;如果这个加载过程是由自定义的类加载器去完成的话,那么很可能就会在JVM
中存在多个版本的java.lang.Object
类,而且这些类之间还是不兼容的,相互之间不可见的(命名空间在发挥的作用)。
借助于双亲委托机制,Java
核心类库中的类的加载工作都是由启动类加载器来统一完成的,从而确保了Java
应用程序所使用的都是同一个版本的Java
核心类库,他们之间是相互兼容的。
相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了应用。
执行下面的代码
public class MyTest15 {
public static void main(String[] args) {
System.out.println(MyTest15.class.getClassLoader());
System.out.println(MyTest1.class.getClassLoader());
}
}
相信你能很快知道输出结果,输出如下
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
在上文中说到过,我们可以通过更改对应加载器的加载路径,可以改变类加载器。那么,如果我们将拓展类加载器的加载路径,改为我们当前的目录,输出结果会是什么
通过上面的输出可以看到,上面的两个类依然是由AppClassLoader加载。在上一篇文章中说过,如果扩展类加载器其类路径下的class文件,需要是类库的形式。所以先进行下面的打包
在Oracle
的Hotspot
实现中,系统属性sun.boot.class.path
如果修改错了,则运行会出错,提示如下信息
内建于JVM中
的启动类加载器会加载java.lang.ClassLoader
以及其他的Java
平台类,当JVM
启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstrap)。
启动类加载器并不是Java
类,而其他的加载器则是Java
类,启动类加载器时特定于平台的机器指令,它负责开启整个加载过程。
所有类加载器(除了启动类加载器)都被实现为Java
类。不过,总归由一个组件加载第一个Java
类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯Java类加载器就是启动类加载器的职责。
启动类加载器还会负责加载供JRE正常运行所需要的基本组件,包括java.unit
与java.lang
包中的类等。
public static void main(String[] args) {
System.out.println(ClassLoader.class.getClassLoader());
// 扩展类加载器与系统类加载器也是由启动类加载器所加载的
System.out.println(Launcher.class.getClassLoader());
System.out.println(ClassLoader.getSystemClassLoader());
}
输出如下
null
null
sun.misc.Launcher$AppClassLoader@18b4aac2
可以看到ClasssLoader
以及Launcher
的类加载器都是启动类加载器,因为扩展类加载器与系统类加载器属于Launcher
内部类,我们不能直接访问,而Launcher
的类加载会尝试加载其成员。同时可以看到系统类加载器默认是AppClassLoader
。
我们知道系统类加载器默认是AppClassLoader
,系统类加载器是默认的自定义加载器的父加载器。
运行下面的代码
public class MyTest16 {
public static void main(String[] args) {
System.out.println(System.getProperty("java.system.class.loader"));
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(new MyTest11("loader").getParent());
}
}
输出如下
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
将系统类加载器改为我们自己定义的加载器出错,这是因为我们需要在自定义类加载器中添加一个ClassLoader
的构造器,如下所示:
public MyTest11(ClassLoader parent){
super(parent);
}
可以看到自定义ClassLoader的类加载器的父加载器不再是AppClassLoader
。
在我们更改默认的系统类加载器后,其是由AppClassLoader
加载。
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
// 用于确认系统类加载器有没有被赋值
private static boolean sclSet;
// 系统类加载器
private static ClassLoader scl;
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
// 如果没有被赋值,但是系统类加载器却不为空,不符合逻辑,所以出错
throw new IllegalStateException("recursive invocation");
// 返回一个Launcer的实例
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 将AppClassLoader设置为系统类加载器
scl = l.getClassLoader();
try {
// 系统类加载器需不需要进行改变
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
// 表示系统类加载器设置完毕
sclSet = true;
}
}
获取Launcher
实例
public Launcher() {
// 扩展类加载器
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 获取AppClassLoader时,将ExtClassLoader传入,这是为了将loader的父加载器设置为ExtClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 将刚创建好的应用类加载器设置为当前线程的上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);
// 安全管理器相关设置
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
获取ExtClassLoader
实例
static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
获取系统的java.ext.dirs
路径下的所有文件
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
通过代码,我么便可以知道,为什么扩展类的加载路径是java.ext.dirs
。
在Launcher的构造器中创建好之后,紧接着开始创建AppClassLoader
。创建过程如下
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
通过代码,可以看到AppClassLoader
的加载路径同样读取java.class.path
下的所有文件。
class SystemClassLoaderAction
implements PrivilegedExceptionAction<ClassLoader> {
private ClassLoader parent;
SystemClassLoaderAction(ClassLoader parent) {
this.parent = parent;
}
public ClassLoader run() throws Exception {
String cls = System.getProperty("java.system.class.loader");
if (cls == null) {
// 如果我们没有设置过java.system.class.loader,那么系统类加载器为AppClassLoader
return parent;
}
// 通过反射调用一个带ClassLoader参数的构造方法
// 这就是为什么我们自定义类加载器设置为SystemClassLoader时,需要有一个ClassLoader参数的构造方法
Constructor<?> ctor = Class.forName(cls, true, parent)
.getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
// 系统类加载器的父加载器设置为parent
ClassLoader sys = (ClassLoader) ctor.newInstance(
new Object[] { parent });
// 将用户自定义的类加载器设置为上下文类加载器
Thread.currentThread().setContextClassLoader(sys);
return sys;
}
}
// name 全限定类名
// initialize 是否进行初始化
// loader 加载指定类的类加载器
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 获取调用forName()方法的类的class对象
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
// 获取caller的classLoader对象
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
该方法的作用是返回使用给定的类加载器加载给定字符串名称的类或接口的Class
对象。
Class.forName(“Foo”) = Class.forName(“Foo”, true, this.getClass().getClassLoader())
public class MyTest17 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getContextClassLoader());
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(Thread.class.getClassLoader());
// null
}
}
每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类),如果ClassX
引用了ClassY
,那么ClassX
的类加载器就会去加载ClassY
(前提是ClassY
尚未被加载)。
线程上下文类加载器从JDK1.2
开始引入的,类Thread
中的getContextClassLoader()
与setContextClassLoader(ClassLoader cl)
分别用来获取和设置上下文类加载器。
如果没有通过setContextClassLoader(cl)
设置的话,线程将继承其父线程的上下文类加载器。
Java
应用运行时的初始化线程的上下文类加载器就是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类于资源。
线程上下文类加载器的重要性:SPI(Service Provider Interface)
父ClassLoader
可以使用当前线程Thread.currentThread().getContextClassLoader()
所指定的classloader
加载的类。这就改变了父Classloader
不能使用子ClassLoader
或是其他没有直接父子关系的classLoader
加载的类的情况。即改变了双亲委托模型。
在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI
来说,有些接口时Java
核心库所提供的,而Java
核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar
包(厂商提供),Java
的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI
的要求(这样导致接是启动类加载,而实现是由系统类加载器或应用类加载器加载)。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。
以JDBC
为例,JDBC
的接口是启动类加载器加载,但是JDBC
的接口的实现却是由数据库厂商所提供。因为JDBC
的接口和其实现存在依赖关系,而其实现的jar包被我们放到了classpath
,这样便导致了启动类加载器无法加载其实现,从而我们无法使用JDBC
,所以传统的双亲委托机制便失效了。
public class MyTest18 implements Runnable{
private Thread thread;
public MyTest18(){
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
ClassLoader classLoader = this.thread.getContextClassLoader();
this.thread.setContextClassLoader(classLoader);
System.out.println("Class: " + classLoader.getClass());
System.out.println("Parent: " + classLoader.getParent().getClass());
}
public static void main(String[] args) {
new MyTest18();
}
}
该程序的输出通过前面的分析,想必很快能知道答案,输出如下
Class: class sun.misc.Launcher$AppClassLoader
Parent: class sun.misc.Launcher$ExtClassLoader
线程上下文类加载的一般使用模式 (获取 - 使用 - 还原)
伪代码如下
try{
// targetThreadContextClassLoader将要使用的ClassLoader设置为线程上下文类加载器
Thread.currentThread().setContextClassLoader(targetThreadContextClassLoader);
doSomething();
}finally{
Thread.currentThread().setClassLoader(classLoader);
}
doSomething()
里面调用了Thread.currentThread.getContextClassLoader()
,获取当前线程的上下文类加载器做某些事情。如果一个类由类加载器A
加载,那么这个类的类加载器会加载这个类的依赖类(前提依赖类没有被加载)
ContextClassLoader
的作用是为了破坏Java的类加载器委托机制。
当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader
找到并加载该类。
SPI