从jdk源码角度理解jvm类加载机制
关于jvm类加载机制,个人感觉还是挺有深度的,可能一般写代码关注业务居多,对jvm的一些机制关注太少,只知其表,而不然其因,实在肤浅。这样写代码估计也写不出优雅的代码来。
网络上关于jvm类加载机制的文章实在是太多,但是从jdk源码角度来理解的确实比较少,之前也看到一篇优秀的博客:深入浅出ClassLoader,非常有深度地讲解了类加载机制。这里关注的是从jdk源码角度来理解。
一. 委派机制(delegation model)
如果你看过sun.misc.Launcher、java.lang.ClassLoader源码的话,可能对”委派机制“并不陌生,下面来讲讲jdk是如何去做的。
先看一张jvm类加载器类的关系图,这是jdk源码体现关系图:
image.png 从这张图,可以看出,所有的ClassLoader都是继承于java.lang.ClassLoader来实现的,当然jvm 中底层C++实现的Bootstrap ClassLoader除外,这个类加载器,等下再说。
再来看看另一张图,这是类加载委派关系图:
image.png
这里说的”委派“在jdk源码中主要是这样体现的:
1)在java.lang.ClassLoader中有一个属性”parent“,其解释如下
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added after it.
private final ClassLoader parent;
2)在java.lang.ClassLoader中有一个protected 方法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;
}
}
ClassLoader loadClass函数是用来加载class的,当前ClassLoader去加载class时,首先判断其“parent”属性的类加载器,如果不为null,则首先让“parent”类加载器去加载,这样按照“类加载委派关系图”一层层往上推;如果,其委派层次上面的“parent”类加载器加载失败,最后由当前的类加载器去加载。这里需要注意的是,虽然OtherClassLoader的“parent”属性指向AppClassLoder,AppClassLoder的“parent”属性指向ExtClassLoder,但是ExtClassLoder的“parent”属性并不是指向Bootstrap ClassLoder,而是为null,当然Bootstrap ClassLoder的“parent”也为null。请看源码:
1)ExtClassLoader的构造函数,第二个参数为null,即赋值给“parent”的值为null:
/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
2)AppClassLoader的构造函数,第二个参数为extcl,这个参数实际上指的是ExtClassLoader,会赋值为“parent”属性:
// 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");
}
当然,很多人会有疑问,Bootstrap ClassLoder、ExtClassLoader、AppClassLoader这么多ClassLoader,它们是从哪里加载class的,这个问题jdk源码中sun.misc.Launcher已经给出回答:Bootstrap ClassLoder加载的是System.getProperty("sun.boot.class.path");、ExtClassLoader加载的是System.getProperty("java.ext.dirs")、AppClassLoader加载的是System.getProperty("java.class.path"),以最简单的java工程,一个main方法,一条简单语句,运行环境为例说明这些路径下到底有哪些jar:
1)sun.boot.class.path = C:\Program Files (x86)\Java\jre7\lib\resources.jar;C:\Program Files (x86)\Java\jre7\lib\rt.jar;C:\Program Files (x86)\Java\jre7\lib\jsse.jar;C:\Program Files (x86)\Java\jre7\lib\jce.jar;C:\Program Files (x86)\Java\jre7\lib\charsets.jar;C:\Program Files (x86)\Java\jre7\lib\jfr.jar
看到了把,都是jre lib(注意这里说的jre是java路径下的,不是jdk路径下的jre,下同)下面的jar,都是java中最基本的jar,例如rt.jar、resources.jar等;
2)java.ext.dirs = C:\Program Files (x86)\Java\jre7\lib\ext;C:\Windows\Sun\Java\lib\ext,lib下面的ext路径;
3)java.class.path = E:\java_web\Test\bin;当前工程编译后的bin路径
这样,相信委派机制应该说的很清楚了。
二. 如何实现自己的ClassLoader
这个问题其实比较深奥,为什么这么说,因为类加载在一个java系统中占有非常重要的地位,它是class进入jvm的一个入口,如果入口都有问题,那这个系统应该没有什么意义。业界比较有名的类加载机制有:委派机制的典型代表“tomcat类加载机制”、颠覆委派机制的“osgi类加载”,有兴趣的话,可以自行研究,这里只说说简单的用法。
在java.lang.ClassLoader的loadClass注释中有这么一段话:* Subclasses of ClassLoader are encouraged to override #findClass(String), rather than this method.因为loadClass函数中调用了findClass函数,loadClass函数已经实现了“委派机制”,你只要去实现findClass就可以了,所以jdk是建议实现findClass就可以了,注意,这只是一个建议而已,当然如果你不想要jdk的“委派机制”,也可以自行写loadClass,所以这就为osgi的类加载留下了发展的空间。至于说到底“委派机制”、osgi类加载,哪个更优,只能说各有各的优缺点,只有你的项目需求才能给出答案,这里不做深入讨论。只是谈谈“委派机制”的一般常用用法:
1)实现findClass的类加载:
/**
- 实现“委派机制”中的findClass
- @param name the binary name of the class, eg.org.test.ClassLoaderTest
- */
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = null;
try {
b = loadLocalClass(name);
} catch (URISyntaxException e) {
e.printStackTrace();
}
if(b!=null) {
// 将class bin转为Class object
return defineClass(name, b, 0, b.length);
}
return null;
}
/**
- 读取class bin文件,这里是以读取E:\下的class为例
- */
private byte[] loadLocalClass(String name) throws URISyntaxException {
DataInputStream dis = null;
try {
int index = name.lastIndexOf(".");
String className = name.substring(index+1);
String path = "E:\"+className+".class";
File file = new File(path).getCanonicalFile();
dis = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
byte[] tmpArr = new byte[1024];
int readLen = 0;
readLen = dis.read(tmpArr);
byte[] byteArr = new byte[0];
while(readLen>0) {
byteArr = mergeArray(byteArr,tmpArr,readLen);
readLen = dis.read(tmpArr);
}
return byteArr;
} catch (SecurityException se) {
se.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流
if(dis!=null) {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private byte[] mergeArray(byte[] byteArr1, byte[] byteArr2, int len) {
int size = byteArr1.length+len;
byte[] byteArr = new byte[size];
System.arraycopy(byteArr1, 0, byteArr, 0, byteArr1.length);
System.arraycopy(byteArr2, 0, byteArr, byteArr1.length, len);
return byteArr;
}
2)用java.net.URLClassLoader实现
在“jdk源码体现关系图”中也看到了,ExtClassLoader、AppClassLoader都是继承于URLClassLoader的,所以可以直接用URLClassLoder指明URL就可以了,如下:
URLClassLoader loaderTest = null;
try {
loaderTest = new URLClassLoader(new URL[]{new File("E:\process.jar").toURI().toURL()});
} catch (MalformedURLException e3) {
// TODO Auto-generated catch block
e3.printStackTrace();
}
try {
// 测试加载E:\下的Process1.class
Class<?> pro = loaderTest.loadClass("org.test.Process1");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
需要注意的是:URLClassLoader支持两种file形式的资源,一种是jar文件,一种是directory,以process.jar为例说明:
在process.jar中有一个类是org.test.Process1,简单写得,其中org.test是包名;那传给File的参数就可以直接是"E:\process.jar";另一种方式传递路径,即给File的参数是"E:\",这里就需要自行在E:\下放org文件夹,org里面在放test文件夹,test里面在放Process1.class文件,其实就是copy编译后的带包名的路径。
三. 模拟“委派类加载机制”的行为
1)验证“委派机制”委派行为
这里主要是写一个类,在构造函数中,输出不同的结果,放在不同的路径下,例如,放在jre\lib\ext下(ExtClassLoader来加载),当然java工程本地的bin路径下会有编译后的.class文件,如下:
package org.test;
public class Process1 {
public Process1() {
System.out.println("ExtClassLoad load Process1.class");
}
public static void main(String[] args) {
Process1 pro = new Process1();
}
}
将待输出“ExtClassLoad load Process1.class”结果的Process1.java导出jar包, 放在jre\lib\ext路径下;再修改Process1.java输出结果,改为System.out.println("AppClassLoad load Process1.class");,编译java工程,这样在本地工程的bin路径下会有输出"AppClassLoad load Process1.class"的Process1.class,运行Process1.java,Console会输出“ExtClassLoad load Process1.class”而不是"AppClassLoad load Process1.class",因为ExtClassLoader会先加载Process1.class,把jre\lib\ext\路径下关于Process1的jar删掉,在运行Process1.java,控制台就会输出"AppClassLoad load Process1.class",这个时候就是AppClassLoader来加载。
当然,ExtClassLoader在加载jre\lib\ext时,也支持directory方式,与URLClassLoader不同的是,需要先ext下新建一层文件夹,然后在这个文件夹下放置带包名的.class文件。
2)模拟类加载异常:ClassNotFoundException、NoSuchMethodException、ClassCastException、NoClassDefFoundError
以下模拟是接着上面的类及包名。
增加一个类ClassLoaderTest:
public class ClassloaderTest extends URLClassLoader {
public ClassloaderTest(URL[] urls) {
super(urls);
}
static {
ClassloaderTest.registerAsParallelCapable();
}
public static void main(String[] args) {
Process1 pro = new Process1();
}
}
(1)模拟ClassNotFoundException:
将本地工程bin路径下的Process1.class文件删除,在运行Process1.java就会出现,这个简单。
(2)模拟NoSuchMethodException:
这里需要在ClassloaderTest的main函数中用到反射:
public static void main(String[] args) {
ClassloaderTest loaderTest = null;
try {
loaderTest = new ClassloaderTest(new URL[]{new File("E:\process.jar").toURI().toURL()});
} catch (MalformedURLException e3) {
e3.printStackTrace();
}
try {
Class<?> pro = loaderTest.loadClass("org.test.Process1");
Method method = null;
try {
method = pro.getDeclaredMethod("getStr");
} catch (NoSuchMethodException | SecurityException e) {
e.printStackTrace();
}
Object probj = null;
try {
probj = pro.newInstance();
} catch (InstantiationException | IllegalAccessException e1) {
e1.printStackTrace();
}
try {
String str = (String) method.invoke(probj);
} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}catch(ClassNotFoundException e) {
e.printStackTrace();
}
}
这里假设Process1.java中有一个getStr函数,然后在调用pro.getDeclaredMethod("getStr");会出现NoSuchMethodException
(3)模拟NoClassDefFoundError、ClassCastException:
这里主要是用两个类加载器加载同一个类来说明。这里首先把ClassloaderTest的main调整为:
public static void main(String[] args) {
ClassloaderTest loaderTest = null;
try {
loaderTest = new ClassloaderTest(new URL[]{new File("E:\process.jar").toURI().toURL()});
} catch (MalformedURLException e3) {
e3.printStackTrace();
}
try {
Class<?> pro = loaderTest.loadClass("org.test.Process1");
pro.newInstance();
}catch(ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
注意这里涉及到ClassloaderTest、Process1两个类,这两个类都是需要加载的。
ClassloaderTest类,由AppClassLoder加载,其本身是一个继承于URLClassLoader的类加载器,它要去加载其他类,首先自己要被加载到jvm中,ClassloaderTest是java工程中的一个类,编译后会在本地工程的bin目录下ClassloaderTest.class文件,而bin下面的class文件是由AppClassLoder加载的,所以ClassloaderTest由AppClassLoder加载。所以看一个类由哪个类加载器加载,就看该类的class文件处于什么类加载器加载的路径。另外,ClassloaderTest虽然继承于URLClassLoader,但是它的“parent”属性是AppClassLoader(因为URLClassLoader默认的parent属性是AppClassLoader),也就是向上委派的类加载器是AppClassLoader,这是用于ClassloaderTest加载其他类的,和ClassloaderTest被加载没有什么关系。可以通过“jdk源码体现关系图“、”类加载委派关系图“两个维度来了解类加载器类。
Process1类可以由AppClassLoder或ClassloaderTest来加载。如果本地工程的bin下有Process1.class,那毫无疑问是AppClassLoder加载;如果删除本地工程的bin下Process1.class,在E:\路径下放置process.jar,那Process1会由ClassloaderTest加载。
一般在一个类中所引用到的其他类,由被引用的类所被加载的类加载器加载。
public class A {
void doTest() {
B b = new B();
b.test();
}
}
也就是说,A如果被AppClassLoader加载,那么A所引用的类B也一般由AppClassLoader加载,这是一般情况,但最正确的是看B.class在哪个类加载器加载的路径下。
a)首先看看NoClassDefFoundError:
在ClassloaderTest的main中,用Process1 probj = (Process1)pro.newInstance();替换pro.newInstance();语句;然后编译java工程,再删除bin路径下的Process1.class;再运行ClassloaderTest,到Process1 probj = (Process1)pro.newInstance();会出现如下NoClassDefFoundError异常:
Exception in thread "main" java.lang.NoClassDefFoundError: org/test/Process1
at org.test.ClassloaderTest.main(ClassloaderTest.java:102)
Caused by: java.lang.ClassNotFoundException: org.test.Process1
at java.net.URLClassLoader2.run(URLClassLoader.java:1)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
... 1 more
这里解释一下。Process1 probj = (Process1)pro.newInstance();,这条语句实际上有三个动作:1.pro.newInstance();2.加载Process1.class;3.将1中的实例赋值给2中的引用。(1)在模拟过程中,做了一个动作,就是将bin下的Process1.class删除,这样”E:\“中的Process1.class会由ClassloaderTest加载,所以1中的instance是ClassloaderTest加载的Process1所产生的;(2)从打出来的Exception Stack Trace,可以看出NoClassDefFoundError是由ClassNotFoundException所引起的,为什么会出现ClassNotFoundException,这是因为ClassloaderTest类是由AppClassLoder加载的,所以ClassloaderTest中引用的Process1也应该由AppClassLoder加载,这个前面已经讲过,而AppClassLoder在当前java工程的bin路径没有找到Process1.class,所以就出现ClassNotFoundException: org.test.Process1;(3)为什么会出现NoClassDefFoundError?这是因为第3步的赋值过程,要知道在jvm中判断是否为同一个类由两个因素决定:是否由同一个类加载器加载,是否从相同内容的.class加载。
b)ClassCastException
(1)首先编译java工程;(2)在Eclipse环境中Process1 probj = (Process1)pro.newInstance();处设置断点;(3)剪切bin路径下的Process1.class到其他盘;(4)启动ClassloaderTest调试,到设置断点处;(5)将(3)中剪切的Process1.class文件在拷贝到bin路径下;(6)继续运行完当前进程,会出现ClassNotFoundException异常:
Exception in thread "main" java.lang.ClassCastException: org.test.Process1 cannot be cast to org.test.Process1
at org.test.ClassloaderTest.main(ClassloaderTest.java:101)
和NoClassDefFoundError异常模拟动作不同的是,本次模拟中,在ClassloaderTest加载"E:"路径下的Process1.class后,会将之前(3)中剪切的Process1.class文件再拷贝到bin路径下,这样AppClassLoader就能加载到ClassloaderTest中所引用到的Process1,这个时候将ClassloaderTest加载的Process1所产生的实例赋值给AppClassLoader加载的Process1引用就会出现ClassCastException。
这样从jdk源码角度理解”委派机制“,通过实际应用且模拟类加载相关的异常,相信对jvm的类加载会有更深入的理解。
!