Java ClassLoader深入研究
Java为了提供平台无关性,在操作系统之上加入了一层JVM来隔离操作系统特定实现,使所编写的java代码在任何平台都能运行,但是JVM是特定于某一操作系统的
一、当JVM启动时,由三个类加载器对类进行加载:
1.bootstrap classloader
2.extension classloader
3.system classloader
(1)bootstrap classloader[ 引导类加载器] 是由JVM实现的,不是java.lang.ClassLoader的子类 ,它负责加载Java的核心类,其加载的类由 sun.boot.class.path指定,或者在 执行java命令时使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值
说明:
-Dproperty_name=property_value 指定属性的值;
-Xbootclasspath 改变虚拟机装载缺省系统运行包rt.jar,而从-Xbootclasspath中设定的搜索路径中装载系统运行类
输出加载的核心类库:
- public class TestBootstrapClassLoader {
- public static void main(String[] args) {
- URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
- for (int i = 0; i < urls.length; i++) {
- System.out.println(urls[i].toExternalForm());
- }
- }
- }
输出结果:
file:/E:/MyEclipse%205.1.1%20GA/jre/lib/rt.jar
file:/E:/MyEclipse%205.1.1%20GA/jre/lib/i18n.jar
file:/E:/MyEclipse%205.1.1%20GA/jre/lib/sunrsasign.jar
file:/E:/MyEclipse%205.1.1%20GA/jre/lib/jsse.jar
file:/E:/MyEclipse%205.1.1%20GA/jre/lib/jce.jar
file:/E:/MyEclipse%205.1.1%20GA/jre/lib/charsets.jar
file:/E:/MyEclipse%205.1.1%20GA/jre/classes
(2) extension classloader[扩展类加载器] ,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包。这为引入除Java核心类以外的新功能提供了一个标准机制。在这个实例上调用方法getParent()总是返回空值null,因为引导加载器bootstrap classloader不是一个真正的ClassLoader实例。
- class TestExtensionClassLoader {
- public static void main(String[] args) {
- System.out.println(System.getProperty("java.ext.dirs"));
- ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();
- System.out.println("the parent of extension classloader : "+extensionClassloader.getParent());
- }
- }
输出结果:
E:/MyEclipse 5.1.1 GA/jre/lib/ext
the parent of extension classloader : null
以上这段代码也表明了下列父子关系或加载顺序:
bootstrap classloader(因其不是ClassLoader的子类,null)-->extension classloader-->system classloader
这也表明了jvm在加载类的顺序,当加载一个类时(假设其未加载),先找到最顶层的classloader,如果其可以加载这个类(或者已经加载了这个类),则返回这个类;如果其不能加载类(换个说法:在相应路径中搜索不到相应类),则用其子classloader加载,直到这个类被加载或者抛出相应的异常
这个顺序保证了越重要的类,越先加载;因为一个类只被加载一次(cache),所以如java.lang.System这个类,不能被用户替换(因为是按照bootstrap-->extension-->system的顺序,当要加载java.lang.System类时,其首先从bootstrap的搜索路径中找类)
(3)system/application classloader [系统(应用)类加载器] ,加载来自-classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。
a.可以通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。
b.如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。可以从源代码看出这一点:
- protected ClassLoader() {
- SecurityManager security = System.getSecurityManager();
- if (security != null) {
- security.checkCreateClassLoader();
- }
- this.parent = getSystemClassLoader();
- initialized = true;
- }
classpath路径:
- class TestSystemClassLoader {
- public static void main(String[] args) {
- System.out.println(System.getProperty("java.class.path"));
- }
- }
结果为:
E:/dev/java/eclipse/TestClassLoader/classes;E:/dev/java/lib/commons-logging-1.1.jar
重要:
classloader加载类用的是全盘负责委托机制。
a.全盘负责,即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入 ;
b.委托机制则是先让parent类加载器 (而不是super,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。此外类加载还采用了cache机制,也就是如果 cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。
二、ClassLoader加载Class的过程
1.检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2
2.如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4
3.请求parent classloader载入,如果成功到8,不成功到5
4.请求jvm从bootstrap classloader中载入,如果成功到8
5.寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.
6.从文件中载入Class,到8.
7.抛出ClassNotFoundException.
8.返回Class.
源代码如下:
- ClassLoader类中
- protected synchronized Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- // First, check if the class has already been loaded
- Class c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClass0(name);
- }
- } catch (ClassNotFoundException e) {
- // If still not found, then invoke findClass in order
- // to find the class.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
可以看到当在parent链classloader中和bootstrap classloader中都找不到相应的类时,会调用findClass方法,因此ClassLoader的子类可以重写这个方法,定义自己的找到类的方法
以下是一些类的ClassLoader示例:
- class TestClassLoader {
- public static void main(String[] args) {
- System.out.println(System.class.getClassLoader());
- System.out.println(javax.swing.JButton.class.getClassLoader());
- System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
- System.out.println(org.apache.commons.logging.Log.class.getClassLoader());
- }
- }
输出:
null 其路径在sun.boot.class.path中,由bootstrap classloader加载,所以返回为null(因其不是ClassLoader的子类)
null 其路径在sun.boot.class.path中,由bootstrap classloader加载
sun.misc.Launcher$ExtClassLoader@7259da 路径在java.ext.dirs中,由extension classloader加载
sun.misc.Launcher$AppClassLoader@197d257 由classpath指定,由system classloader加载
查看sun.misc.Launcher的源代码
- public Launcher() {
- // Create the extension class loader
- ClassLoader extcl;
- try {
- extcl = ExtClassLoader.getExtClassLoader();
- } catch (IOException e) {
- throw new InternalError(
- "Could not create extension class loader");
- }
- // 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");
- }
- // Also set the context class loader for the primordial thread.
- Thread.currentThread().setContextClassLoader(loader);
- // 其他
- }
可以看到是由Launcher这个类初始化ExtClassLoader和AppClassLoader类的
ExtClassLoader无parent,而AppClassLoader的parent为 ExtClassLoader
Launcher的getClassLoader()方法
- public ClassLoader getClassLoader() {
- return loader;
- }
- 只返回AppClassLoader
ExtClassLoader
- static class ExtClassLoader extends URLClassLoader {
- private File[] dirs;
- /**
- * create an ExtClassLoader. The ExtClassLoader is created
- * within a context that limits which files it can read
- */
- public static ExtClassLoader getExtClassLoader() throws IOException
- {
- final File[] dirs = getExtDirs();
- try {
- // Prior implementations of this doPrivileged() block supplied
- // aa synthesized ACC via a call to the private method
- // ExtClassLoader.getContext().
- return (ExtClassLoader) AccessController.doPrivileged(
- new PrivilegedExceptionAction() {
- public Object run() throws IOException {
- int len = dirs.length;
- for (int i = 0; i < len; i++) {
- MetaIndex.registerDirectory(dirs[i]);
- }
- return new ExtClassLoader(dirs);
- }
- });
- } catch (java.security.PrivilegedActionException e) {
- throw (IOException) e.getException();
- }
- }
- ......
- private static File[] getExtDirs() {
- String s = System.getProperty("java.ext.dirs");
- File[] dirs;
- if (s != null) {
- StringTokenizer st =
- new StringTokenizer(s, File.pathSeparator);
- int count = st.countTokens();
- dirs = new File[count];
- for (int i = 0; i < count; i++) {
- dirs[i] = new File(st.nextToken());
- }
- } else {
- dirs = new File[0];
- }
- return dirs;
- }
AppClassLoader
- static class AppClassLoader extends URLClassLoader {
- public static ClassLoader getAppClassLoader(final ClassLoader extcl)
- throws IOException
- {
- final String s = System.getProperty("java.class.path");
- final File[] path = (s == null) ? new File[0] : getClassPath(s);
- // Note: on bugid 4256530
- // Prior implementations of this doPrivileged() block supplied
- // a rather restrictive ACC via a call to the private method
- // AppClassLoader.getContext(). This proved overly restrictive
- // when loading classes. Specifically it prevent
- // accessClassInPackage.sun.* grants from being honored.
- //
- return (AppClassLoader)
- AccessController.doPrivileged(new PrivilegedAction() {
- public Object run() {
- URL[] urls =
- (s == null) ? new URL[0] : pathToURLs(path);
- return new AppClassLoader(urls, extcl);
- }
- });
- }
- ......
Launcher类的getBootstrapClassPath()方法
- public static URLClassPath getBootstrapClassPath() {
- String prop = (String)AccessController.doPrivileged(new GetPropertyAction("sun.boot.class.path"));
- URL[] urls;
- if (prop != null) {
- final String path = prop;
- urls = (URL[])AccessController.doPrivileged(
- new PrivilegedAction() {
- public Object run() {
- File[] classPath = getClassPath(path);
- int len = classPath.length;
- Set seenDirs = new HashSet();
- for (int i = 0; i < len; i++) {
- File curEntry = classPath[i];
- // Negative test used to properly handle
- // nonexistent jars on boot class path
- if (!curEntry.isDirectory()) {
- curEntry = curEntry.getParentFile();
- }
- if (curEntry != null && seenDirs.add(curEntry)) {
- MetaIndex.registerDirectory(curEntry);
- }
- }
- return pathToURLs(classPath);
- }
- }
- );
- } else {
- urls = new URL[0];
- }
- return new URLClassPath(urls, factory);
- }
从以上代码中可以看到bootstrap classloader使用sun.boot.class.path来加载类,extension classloader使用java.ext.dirs来加载类,而system classloader使用java.class.path来加载类
运行下列程序:
- class Test1 {
- public static void main(String[] args) {
- System.out.println(sun.misc.Launcher.getLauncher().getClass().getClassLoader());
- }
- }
结果为:
null
表明Launcher是由bootstrap classloader来加载的
三、关于Context ClassLoader
在上面Launcher的构造函数中有这么一句: Thread.currentThread().setContextClassLoader(loader); 这句是设置当前线程的classloader,默认是使用的AppClassLoader
这个有什么作用呢?
当线程需要用到某个类,contextClassLoader被请求来载入该类
注意:
(1)Class.forName(String name)载入的是在系统中已经加载入sun.boot.class.path、 java.ext.dirs、java.class.path路径中的类,而在这几个路径中未加入的类不能载入(报异常)
(2)Class.forName(String name, boolean initialize, ClassLoader loader)可以载入上述三个路径中没有的类,只要指定你的classloader即可
(3)利用ClassLoader可以载入在上述三个路径中没有的类
示例:
- 自定义ClassLoader,从c根目录中读class
- package Test;
- public class TestClassLoader extends ClassLoader {
- protected Map<String, Class> cache = new HashMap<String, Class>();
- public TestClassLoader() {
- super();
- }
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- if (cache.get(name) != null) {
- return cache.get(name);
- }
- try {
- String tname = name.replace('.', '/');
- File file = new File("c://" + tname + ".class");
- FileInputStream in = new FileInputStream(file);
- BufferedInputStream bufIn = new BufferedInputStream(in);
- ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
- BufferedOutputStream bufOut = new BufferedOutputStream(byteOut);
- byte[] buffer = new byte[4096];
- int len = -1;
- while((len = bufIn.read(buffer)) != -1) {
- bufOut.write(buffer, 0, len);
- }
- bufOut.flush();
- byteOut.flush();
- byte[] data = byteOut.toByteArray();
- Class cls = defineClass(name, data, 0, data.length);
- cache.put(name, cls);
- return cls; } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
- }
- class TestLoader {
- public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
- ClassLoader cl = Thread.currentThread().getContextClassLoader();
- System.out.println(Thread.currentThread().toString() + " " + cl.toString());
- new Thread() {
- public void run() {
- ClassLoader cl = Thread.currentThread().getContextClassLoader();
- System.out.println(Thread.currentThread().toString() + " " + cl.toString());
- TestClassLoader loader = new TestClassLoader();
- Thread.currentThread().setContextClassLoader(loader);
- new Thread() {
- public void run() {
- try {
- ClassLoader cl = Thread.currentThread().getContextClassLoader();
- System.out.println(Thread.currentThread().toString() + " " + cl.toString());
- //ClassLoader
- AbstractTestA test = (AbstractTestA) cl.loadClass("Test.TestAImp1").newInstance();
- //Class.forName
- //AbstractTestA test = (AbstractTestA) Class.forName("Test.TestAImp1", true, cl).newInstance(); test.test();
- } catch (InstantiationException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (ClassNotFoundException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }.start();
- }
- }.start();
- }
- }
- //抽象类
- package Test;
- public abstract class AbstractTestA {
- public String a;
- public void print() {
- System.out.println("a=" + a);
- }
- abstract public void test();
- }
- //编译后放入c根目录
- package Test;
- public class TestAImp1 extends AbstractTestA {
- public void test() {
- System.out.println(Thread.currentThread().toString());
- System.out.println("Test");
- }
- }
运行结果为:
Thread[main,5,main] sun.misc.Launcher$AppClassLoader@197d257
Thread[Thread-0,5,main] sun.misc.Launcher$AppClassLoader@197d257
Thread[Thread-1,5,main] Test.TestClassLoader@1004901
Thread[Thread-1,5,main]
Test
这也表明了线程间ContextClassLoader的继承性:
(1)main线程默认的 ContextClassLoader为AppClassLoader
(2)新启动线程从原线程处继承 ContextClassLoader
四、关于ClassLoader和Package
其中:String(int offset, int len, char[] arr)为包访问权限
- package java.lang;
- public class TestPackage {
- public static void main(String[] args) {
- char[] c = "1234567890".toCharArray();
- String s = new String(0, 10, c);
- }
- }
此代码可以编译通过,但是运行时出现下列错误:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(Unknown Source)
at java.lang.ClassLoader.defineClass(Unknown Source)
at java.security.SecureClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.access$100(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
这表明:
Java语言规定,在同一个包中的class,如果没有修饰符,默认为Package权限,包内的class都可以访问。但是这还不够准确。确切的说,只有由同一个ClassLoader装载的class才具有以上的Package权限。比如Bootstrap classloader装载了java.lang.String,AppClassLoader装载了我们自己写的java.lang.TestPackage,它们不能互相访问对方具有Package权限的方法。这样就阻止了恶意代码访问核心类的Package权限方法。
五、关于两个ClassLoader载入同一个类
注意:由两个不同的ClassLoader载入的同一个类,其是不同类型的,因此如果进行赋值会报ClassCastException
示例:
- package Test;
- import java.io.*;
- import java.util.*;
- public class TestClassLoader extends ClassLoader {
- protected Map<String, Class> cache = new HashMap<String, Class>();
- public TestClassLoader() {
- super();
- }
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- if (cache.get(name) != null) {
- return cache.get(name);
- }
- try {
- String tname = name.replace('.', '/');
- File file = new File("c://" + tname + ".class");
- FileInputStream in = new FileInputStream(file);
- BufferedInputStream bufIn = new BufferedInputStream(in);
- ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
- BufferedOutputStream bufOut = new BufferedOutputStream(byteOut);
- byte[] buffer = new byte[4096];
- int len = -1;
- while((len = bufIn.read(buffer)) != -1) {
- bufOut.write(buffer, 0, len);
- }
- bufOut.flush();
- byteOut.flush();
- byte[] data = byteOut.toByteArray();
- Class cls = defineClass(name, data, 0, data.length);
- cache.put(name, cls);
- return cls;
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
- }
- class TestLoaderA {
- public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
- TestClassLoader loader = new TestClassLoader();
- System.out.println(TestAImp1.class.getClassLoader());
- System.out.println(loader.findClass("Test.TestAImp1").getClassLoader());
- TestAImp1 test = (TestAImp1)loader.findClass("Test.TestAImp1").newInstance();
- test.test();
- }
- }
- public class TestAImp1 extends AbstractTestA {
- public void test() {
- System.out.println(Thread.currentThread().toString());
- System.out.println("Test");
- }
- }
运行以上代码结果:
sun.misc.Launcher$AppClassLoader@197d257
Test.TestClassLoader@1b90b39
Exception in thread "main" java.lang.ClassCastException: Test.TestAImp1
at Test.TestLoaderA.main(TestClassLoader.java:84)