从jdk源码角度理解jvm类加载机制

        关于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
 * */
@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.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的类加载会有更深入的理解。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值