本内容仅涉及把类文件(字节流)加载到虚拟机的过程
- 基于java version "1.8.0_45"
- 基于Java(TM) SE Runtime Environment
- 基于Java HotSpot(TM) 64-Bit Server VM
概述
学习过Java的都知道,编写的Java代码是要被javac编译成class文件,才能被Java虚拟机执行的。虚拟机执行class文件,首先得先将其读入内存,然后分析里面的字节码,才能最终执行。
那么类加载这个过程,就是class文件被虚拟机读取和解析的过程,这是开始执行java程序的第一步。
在总结之前,先写下几个想知道的问题:
- JVM怎么知道要加载哪些类呢?
- 类加载有哪几步?
- 可以手动在运行中加载类吗?
- JVM是怎么加载的类?有什么机制?
- 了解了加载机制有什么用?
类加载过程
虚拟机只会加载程序执行时需要的类文件。
例如我们编写了这样一个类:
package jdk.test.classLoader;
public class Test {
public Test() {
}
public void hello() {
System.out.println("hello world");
}
public static void main(String[] args) {
Test t = new Test();
t.hello();
}
}
然后追加JVM参数-XX:+TraceClassLoading
跟踪一下类加载的轨迹,运行结果:
[Opened G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
[Loaded java.lang.Object from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
// 此处省略438行
[Loaded jdk.test.classLoader.Test from file:/D:/workspace/Test/bin/]
[Loaded sun.launcher.LauncherHelper$FXHelper from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
[Loaded java.lang.Void from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
hello world
[Loaded java.lang.Shutdown from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
轨迹很长没有完全贴出来,从轨迹中可以大致看出类加载到虚拟机的主要的步骤:
首先打开rt.jar(这个jar里面存的是java的系统类)[Opened G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
;
然后第一步是[Loaded java.lang.Object from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
;
可见首先加载的是所有类的父类java.lang.Object
;(加载先加载父类)
随后加载了rt.jar中的400多个类;
然后加载上面我们写的这个类[Loaded jdk.test.classLoader.Test from file:/D:/workspace/Test/bin/]
;
最后加载退出虚拟机的动作需要的类。
类加载器
那么问题来了,上面轨迹中的这些类怎么加载到JVM中的呢?JVM载入他们这个过程是自动完成的,那载入的过程,这个把class文件拿到内存中的“手”是什么?
————它的名字叫类加载器(ClassLoader)。
再去看一下类加载的轨迹,找一找classloader关键字。
仔细一看发现了15个和classLoader有关的加载轨迹,列出主要的几个:
12:[Loaded java.lang.ClassLoader from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
22:[Loaded java.security.SecureClassLoader from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
60:[Loaded sun.reflect.DelegatingClassLoader from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
89:[Loaded java.net.URLClassLoader from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
93:[Loaded sun.misc.Launcher$AppClassLoader from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
94:[Loaded sun.misc.Launcher$ExtClassLoader from G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar]
那么加载类加载器的加载器又是啥呢?感觉追根溯源变成先有鸡还是先有蛋的问题了。实际上顶层的类加载器是c++写的,所以这个轨迹里面是看不到的,能看到的只有APPClassLoader和ExtClassLoader。顶层的加载器叫bootStrap类加载器(引导类加载器)。
实际上每个java程序,至少有三个加载器:
- 引导类加载器
- 扩展类加载器
- 应用类加载器(或者叫系统类加载器)
为什么说至少呢,因为我们可以按照规则定义自己的类加载器。但是以上三种是固定的,JVM自带的。
引导类加载器负责加载系统的类(在rt.jar中),它没有对应的Java类对象,因为它不是用Java实现的,所以在系统类上调用
getClassLoader()
的返回结果是null。
扩展类加载器负责加载目录jre/lib/ext下的java标准扩展,它加载类不使用classpath这个环境变量。
应用类加载器负责加载应用类,即我们自己编写的类。它在由CLASSPATH环境变量或者-cp设置的路径或zip/jar包中查找要加载的类。
扩展类加载器和应用类加载器都是URLClassLoader的实例。
类加载器除了加载类到虚拟机中这个作用之外,还有一个重要的作用:一个类在虚拟机中的唯一性,由类本身和加载该类的类加载器共同决定的。也就是说,即使你两个类的字节码一模一样,但是不是由同一个类加载器加载的,那么在虚拟机中它们就不是同一个类。
如下我们自定义一个类加载器,读取磁盘上的一个类文件并加载到虚拟机中:
package jdk.test.classLoader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class TestCL {
public static void main(String[] args) throws ClassNotFoundException {
String basedir = "D://classes";
ClassLoader diskcl = new ClassLoader() {
private String dir = basedir;
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
byte[] in = Files
.readAllBytes(Paths.get(dir, name.substring(name.lastIndexOf(".") + 1) + ".class"));
return super.defineClass(name, in, 0, in.length);
} catch (IOException e) {
e.printStackTrace();
return super.loadClass(name);
}
}
};
// 把当前目录下的jdk.test.classLoader.Test的class文件复制到了D://classes目录下,使用自定义的加载器加载
Class c = diskcl.loadClass("jdk.test.classLoader.Test");
Class c_auto_load = Test.class;
System.out.println(c == c_auto_load);
System.out.println(c.getClassLoader());
System.out.println(c_auto_load.getClassLoader());
}
}
结果:
false
jdk.test.classLoader.TestCL$1@6d06d69c
sun.misc.Launcher$AppClassLoader@73d16e93
类加载机制
1.层次结构
类加载器有个parent属性,可以通过getParent()取得,一般自定义类加载器没有指定父加载器,则默认是APPClassLoader,上述的自定义的类加载层次结构可以这样查看:
System.out.println(diskcl.getParent());
System.out.println(diskcl.getParent().getParent());
System.out.println(diskcl.getParent().getParent().getParent());
/*
* sun.misc.Launcher$AppClassLoader@73d16e93
* sun.misc.Launcher$ExtClassLoader@70dea4e
* null 实际上是BootStrapClassLoader
*/
要注意的是,这里的父子关系不是基于继承而是基于组合的关系。
2.双亲委派
看一下ClassLoader#loadClass方法的源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//先查看是否有父加载器,有就让父加载器去加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 没有父加载器就让bootStrap加载器去加载,实际上没有父加载器就是父加载器是引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果父加载器没有找到(没有加载成功)
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
从loadClass就能了解双亲委派机制的原理:
类加载器在加载一个类时,首先让父加载器去加载,父加载器自然也是委托给自己的父加载器,这样一层一层往上层委托,如果父加载器加载失败,会把机会给子加载器,子加载器如果也失败,就再往下委托回子加载器,这样一层一层往下,直至加载成功或者抛出异常(大家都束手无策)。
这种加载机制可以防止类被重复加载,因为每次都是往上检查是否加载,且优先让父加载器加载(前面说过类的唯一性是由加载器和类自身确定的)。
有一点需要注意的是,父加载器加载的类对子加载器加载的类是可见的,反过来就不是。所以如果基础类(父加载器加载的类)想要调用后面子加载器加载的类,就需要破坏双亲委派机制。一般的解决方法是使用线程上下文加载器来加载需要访问的不可见的类。
Thread t = Thread.currentThread();
t.setContextClassLoader(cl);
t.getContextClassLoader();
参考文献:
《深入理解Java虚拟机》
《Java核心技术 卷2》
The end.
网友评论