Java虚拟机 ——类加载器
首发于公众号: DSGtalk1989
我们通过第一章节粗略的过了一下虚拟机将java代码变成可被机器阅读的机器码过程。
整个过程中最最重要的三个机制分别是
- java源码编译机制
- 类加载机制
- 类执行机制
源码编译机制实际上是交给源码编译器解决,跟虚拟机没有直接关系。JVM处理的是类的加载和类的执行。
一个类从加载到虚拟机内存到被卸载出内存,整个的生命周期要经历如下的7个阶段。分别是加载,验证,准备,解析,初始化,使用,卸载。验证,准备,解析这三个部分又被统称为链接。
java类的生命周期这里要说明一点,我们通常都会说到这个方法或者这个类,一般在使用的时候才会开始初始化,这就是java的运行时绑定,白话了说就是它到底是个什么要等到初始化了之后才知道,所以解析可能在初始化之前可能在初始化之后。这个点到为止。
2.1 类的加载器
我们先从类的加载(loading)开始,类的加载由ClassLoader及其子类完成,总共有如下四个ClassLoader。
-
Bootstrap ClassLoader
引导类加载器:加载最为基础,最为重要的类,比如
jre/lib
目录下的rt.jar
文件,以及虚拟机参数-Xbootclasspath
路径下的类。 -
Extension ClassLoader
扩展类加载器:如其名,加载java平台中扩展功能的一些jar包,比如
jre/lib
目录下的其他jar
文件,以及系统变量java.ext.dirs
路径下的类。 -
Application ClassLoader
应用类加载器:开始真正加载我们自己的类
-
Custom ClassLoader
自定义类加载器:我们需要自己家在一些其他的类文件或者做一些其他操作。
维基百科是这么描述的:
JVM中有3个默认的类加载器:[[3]](https://zh.wikipedia.org/wiki/Java类加载器#cite_note-3)[4]
- 引导(Bootstrap)类加载器。由原生代码(如C语言)编写,不继承自
java.lang.ClassLoader
。负责加载核心Java库[5],存储在<JAVA_HOME>/jre/lib
目录中。- 扩展(Extensions)类加载器。用来在
<JAVA_HOME>/jre/lib/ext
,[6]或java.ext.dirs
中指明的目录中加载 Java的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。该类由sun.misc.Launcher$ExtClassLoader
实现。- Apps类加载器(也称系统类加载器)。根据 Java应用程序的类路径(
java.class.path
或CLASSPATH环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。该类由sun.misc.Launcher$AppClassLoader
实现。
总结一下上面没有提到的:
引导类加载器是用原生代码编写的,(实际由C++编写),扩展类加载器由sun.misc.Launcher$ExtClassLoader
实现,应用类加载器由sun.misc.Launcher$AppClassLoader
实现。
如果只到这边为止,那网络上类似的会有很多,所以我们打开源码,探个究竟,要开始引起不适了,做好准备。
我们找到Launcher
类,先来看扩展类加载器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();
}
}
...
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;
}
...
}
通过静态方法,直接获取ExtClassLoader
对象,过程中通过getExtDirs
获取java.ext.dirs
路径下的所有文件并进行注册registerDirectory
。
同样的,应用类加载器基本一样。
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);
}
});
}
...
}
获取java.class.path
下所有类。
其实你还能在源码中发现这样一行。
private static String bootClassPath = System.getProperty("sun.boot.class.path");
所以我们的前面三个ClassLoader
分别会查找sun.boot.class.path
java.ext.dirs
java.class.path
来加载相应类。
接着,我们写一个Demo 来验证看看什么类用什么加载器加载出来,是否与我们上面描述的一致。
public static void main(String[] args){
ClassLoader c = ArrayTest.class.getClassLoader();
System.out.println("ClassLoader is " + c.toString());
}
代码中的ArrayTest
是一个我们自定义的类,打印的结果为
ClassLoader is sun.misc.Launcher$AppClassLoader@14dad5dc
没有问题,我们所有自定义的类走的加载器为AppClassLoader
。
好的,我们再来接着看,我们尝试着去拿一下int
的ClassLoader
public static void main(String[] args){
ClassLoader c = int.class.getClassLoader();
System.out.println("ClassLoader is " + c.toString());
}
结果报错了
Exception in thread "main" java.lang.NullPointerException
at salamanca.Vmtest.main(Vmtest.java:6)
什么意思?找不到int
类的ClassLoader
,那int
是怎么加载出来的呀?
针对ArrayTest
类的ClassLoader
我们再加一些打印,来看下他的父类情况。
public static void main(String[] args){
ClassLoader c = ArrayTest.class.getClassLoader();
System.out.println("ClassLoader is " + c.toString());
System.out.println("ClassLoader is " + c.getParent().toString());
System.out.println("ClassLoader is " + c.getParent().getParent().toString());
}
打印结果为
ClassLoader is sun.misc.Launcher$AppClassLoader@14dad5dc
ClassLoader is sun.misc.Launcher$ExtClassLoader@2f0e140b
Exception in thread "main" java.lang.NullPointerException
at salamanca.Vmtest.main(Vmtest.java:8)
也就是说AppClassLoader
的父加载器是ExtClassLoader
,ExtClassLoader
没有父加载器。这里要注意,我们说的父加载器并不是通常的继承,而只是parent
。根据最开始的维基百科介绍,猜测一下,之所以ExtClassLoader
没有父加载器,是因为Bootstrap ClassLoader
是由native代码实现的,因此直接在java层面是拿不到的。
我们抱着这些问题再回过头去啃一啃ClassLoader
的源码。首先找到parent
拿的到底是啥
public final ClassLoader getParent() {
if (parent == null)
return null;
...
return parent;
}
返回的是parent
,一步一步找到它的定义和它的赋值。
private final ClassLoader parent;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
...
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
即构造ClassLoader
传入一个parent ClassLoader
,分别去到AppClassLoader
,和ExtClassLoader
看下传的parent
都是谁。首先他俩都继承自URLClassLoader
->SecureClassLoader
->ClassLoader
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
public URLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
...
}
URLClassLoader(URL[] urls, ClassLoader parent,
AccessControlContext acc) {
super(parent);
...
}
protected SecureClassLoader(ClassLoader parent) {
super(parent);
...
}
是不是已经很明显了,AppClassLoader
传的是var2
,ExtClassLoader
传的就是null
。找一下AppClassLoader
定义的地方。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
...
}
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
的parent
就是ExtClassLoader
,ExtClassLoader
的parent
是null
。解释了我们前面的打印空指针现象,但是跟我们理解的AppClassLoader -> ExtClassLoader -> Bootstrap ClassLoader
有点不一致,Bootstrap ClassLoader
无缘无故就消失了。
先就此打住,我们先来看一下大家一直听到的双亲委托机制,然后再回过头来看,就豁然开朗了。
双亲委托
大家了解的双亲委托应该都是这么讲的
双亲委托子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类
简单来说就是以下的白话流程
- 来了一个
Class
加载需求,先去Custom Classlodaer
看是否加载过,如果没有加载过就问AppClassloader
是否加载过,如此循环直到Bootstrap ClassLoader
。 - 当中任意环节加载过就直接返回加载的字节码
- 如果直到
Bootstrap ClassLoader
发现都没有加载过,那么开始去自己的路径下面即sun.mic.boot.class
找是否有这个类,找到就开始加载,如果没有找到就往子加载器抛,如此循环直到抛到Custom ClassLoader
- 如果
Custom ClassLoader
的路径中都没有出现这个类,那么就抛错ClassNotFoundException
。
那么结合代码的话,如何去理解上面说的双亲委托和加载流程呢?
准备好了么?一切的谜题就要解开了!
我们去到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 {
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;
}
}
-
Class<?> c = findLoadedClass(name);
第一步,看下是否在自己这边已经被加载过了 -
if (c == null)
如果自己这儿没有加载过 -
在
try
中if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); }
有父加载器,就调父加载器的
loadClass
方法,递归。如归没有定义父加载器parent
比如ExtClassLoader
,就走findBootstrapClassOrNull
,清爽不清爽!我们先不急着兴奋,接着往下把流程看完 -
如果依然没有找到就调
findClass
方法去找。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(); }
已经很明显了,这个很有可能就是我们说的自上而下的加载。
/** * 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>. * ... */ protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
果不其然,这个方法就是去找寻具体的类,并且由其子类实现,我们最终可以在两个加载器的父类
URLClassLoader
中可以找到findClass
的方法实现protected Class<?> findClass(final String name) throws ClassNotFoundException { ... String path = name.replace('.', '/').concat(".class"); Resource res = ucp.getResource(path, false); ... }
而这个
ucp
就是我们说的资源路径/* The search path for classes and resources */ private final URLClassPath ucp;
你只要略微的找一下就能找到在
AppClassLoader
和ExtClassLoader
中,分别传入了pathjava.ext.dirs
和java.class.path
。
补充
有个技巧告诉大家,我们按照如下步骤操作IDE,便可以在控制台看到类加载的过程
-
首先找到IDEA的工具栏的
1Run -> Edit Configrations
-
在
2VM options
中添加配置-XX:+TraceClassLoading
就可以跟踪打印类的初始化流程了。 -
我们在结果中就能看到类的加载流程了
3
加载器介绍到此为止。