从jdk源码角度理解jvm类加载机制
来源:互联网 发布:手机直播系统源码 编辑:程序博客网 时间:2024/05/29 03:13
关于jvm类加载机制,个人感觉还是挺有深度的,可能一般写代码关注业务居多,对jvm的一些机制关注太少,只知其表,而不然其因,实在肤浅。这样写代码估计也写不出优雅的代码来。
网络上关于jvm类加载机制的文章实在是太多,但是从jdk源码角度来理解的确实比较少,之前也看到一篇优秀的博客:深入浅出ClassLoader,非常有深度地讲解了类加载机制。这里关注的是从jdk源码角度来理解。
一. 委派机制(delegation model)
如果你看过sun.misc.Launcher、java.lang.ClassLoader源码的话,可能对”委派机制“并不陌生,下面来讲讲jdk是如何去做的。
先看一张jvm类加载器类的关系图,这是jdk源码体现关系图:
从这张图,可以看出,所有的ClassLoader都是继承于java.lang.ClassLoader来实现的,当然jvm 中底层C++实现的Bootstrap ClassLoader除外,这个类加载器,等下再说。
再来看看另一张图,这是类加载委派关系图:
这里说的”委派“在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 * */@Overridepublic Class<?> findClass(String name) throws ClassNotFoundException {byte[] b = null;try {b = loadLocalClass(name);} catch (URISyntaxException e) {e.printStackTrace();}if(b!=null) {// 将class bin转为Class objectreturn 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 blocke3.printStackTrace();}try {// 测试加载E:\下的Process1.classClass<?> 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.URLClassLoader$2.run(URLClassLoader.java:366)
at java.net.URLClassLoader$2.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的类加载会有更深入的理解。
- 从jdk源码角度理解jvm类加载机制
- 【JVM】从广度、深度,二维理解“类加载机制”
- Jvm类加载机制理解
- 从JVM角度理解线程
- 从源码的角度理解Android消息处理机制
- 【深入理解JVM】:类加载机制
- 理解jvm类加载机制(基础)
- JVM类加载机制(ClassLoader)源码解析
- 从字节码角度理解JVM异常处理机制的原理
- 从JDK源码角度看Boolean
- 从JDK源码角度看Object
- 从JDK源码角度看Byte
- 从JDK源码角度看Short
- 从JDK源码角度看Integer
- 从JDK源码角度看Long
- 从JDK源码角度看Long
- 从JDK源码角度看Float
- 从JDK源码角度看Float
- tomcat编码格式与jsp编码格式不一致,导致页面标题乱码
- P11 (*) 游程编码改
- ubuntu下解决WebStorm找不到chromium路径
- 三层小总结
- 【GLSL教程】(五)卡通着色
- 从jdk源码角度理解jvm类加载机制
- 填充ListView(继承BaseAdapter)
- 数据结构实验之查找七:线性之哈希表 151 268
- Universal-Image-Loader
- 039_硬币问题(贪心)
- DHCP源码分析-系统概述
- Spring声明式事务异常回滚机制
- Linux开启ssh服务
- VMware vSphere6.0 安装记录