从事java研发必然少不了对java类加载机制的涉及,本文结合例子讲述java classloader工作机制。
一 jvm 类加载机制
1)jvm位置:java是运行在java虚拟机上的程式,java虚拟机物理层面上来讲,就是我们安装在电脑上的jre目录/lib/jvm.dll(版本不同,可能存在于jre目录/lib/client/jvm.dll,jre目录/lib/server/jvm.dll),这是java字节码运行的基础,它不是由java语言编写,所以我们阅读jdk源码时遇到native函数,基本上就是调用jvm相关的代码。
2)jdk和jre关系:从oracle官网上下载java环境,可以选择jdk或者jre进行安装,他们的关系可以理解为子集的概念,jdk是jre运行环境再加上一些java开发的工具集,查看jdk目录结构如下(例子为jdk1.6.37版本)
D:. ├─bin │ └─server ├─include │ └─win32 ├─jre │ ├─bin │ │ ├─dtplugin │ │ ├─plugin2 │ │ └─server │ └─lib │ ├─amd64 │ ├─applet │ ├─audio │ ├─cmm │ ├─deploy │ ├─ext │ ├─fonts │ ├─im │ ├─images │ │ └─cursors │ ├─management │ ├─security │ ├─servicetag │ └─zi │ ├─Africa │ ├─America │ │ ├─Argentina │ │ ├─Indiana │ │ ├─Kentucky │ │ └─North_Dakota │ ├─Antarctica │ ├─Asia │ ├─Atlantic │ ├─Australia │ ├─Etc │ ├─Europe │ ├─Indian │ ├─Pacific │ └─SystemV └─lib └─visualvm ├─etc ├─platform │ ├─config │ │ ├─ModuleAutoDeps │ │ └─Modules │ ├─core │ │ └─locale │ ├─docs │ ├─lib │ │ └─locale │ ├─modules │ │ ├─ext │ │ │ └─locale │ │ └─locale │ └─update_tracking ├─profiler │ ├─config │ │ └─Modules │ ├─lib │ │ ├─deployed │ │ │ ├─jdk15 │ │ │ │ └─windows-amd64 │ │ │ └─jdk16 │ │ │ └─windows-amd64 │ │ └─locale │ ├─modules │ │ └─locale │ └─update_tracking └─visualvm ├─config │ └─Modules ├─core │ └─locale ├─modules │ └─locale └─update_tracking
java官方文档描述jre和jdk关系如图:(链接http://docs.oracle.com/javase/7/docs/)
在安装jdk时可以选择是否同时安装jre,如果选择安装,那么系统中就存在两份jre,具体程序运行时会执行哪个jre,windows系统默认搜索规则是:
1. 当前目录下有沒有 JRE子目录
2. 父目录下 JRE 子目录
3.查 詢 Window Registry(HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\)
注意:安装环境会建议建立JAVA_HOME环境变量并将其加入path中,这样可以避免因为默认搜索规则出的结果造成混淆。如果没有加,可以搜索下自己系统中有几个java.exe,本人系统中有三个,分别在c:/windows/system32/java.ext;
d:/program files/java/jdk_1_6_37/bin/java.exe;
d:/program files/java/jre/bin/java.exe
因为没有将JAVA_HOME路径加入到path,path路径是c:/windows/system32;......所以在命令行下执行java Main系统默认执行的是c:/windows/system32/java.exe(除非命令行在其他两个java.exe所在目录),这一点可以通过分别修改三个路径下java.exe文件到新名字java1.exe来验证到底执行的是哪个目录
3)java类加载机制:jdk带有三个系统类加载器:bootstrap加载器;扩展加载器;系统加载器,他们的关系如下表
类加载器 |
被加载加载器 | parent | 父类 | 类型 | 默认加载目录/文件 | 备注 |
bootstrap加载器 |
sun.boot.class.path系统属性所指路径,指向jre下/lib,如rt.jar
|
虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的 |
||||
扩展加载器 |
bootstrap加载器 |
bootstrap加载器(因为此加载器由非java语言编写,在jvm中标识为null,所以一个加载器的parent为null表示它是由bootstrap加载器加载) |
java.lang.ClassLoader->java.security.SecurClassLoader->java.net.URLClassLoader | sun.misc.Launcher$ExtClassLoader | java.ext.dirs属性所指路径,指向java.exe所在jre下/lib/ext子目录,可以将自己的class文件放入这个目录,交由扩展加载器加载,可以通过–Djava.ext.dirs=xxx 改变 | jvm中只存在一份,一旦建立,再通过System.setProperty()修改系统属性不会起作用 |
系统加载器 |
bootstrap加载器 |
扩展加载器 |
java.lang.ClassLoader->java.security.SecurClassLoader->java.net.URLClassLoader | sun.misc.Launcher$AppClassLoader | 默认为.目录 再取java.class.path属性所指路径,可以通过java -cp xxx 来改变 最后取环境变量CLASSPATH下的class文件和jar文件 |
jvm中只存在一份,一旦建立,再通过System.setProperty()修改系统属性不会起作用 |
在 Java 之中,每个类都是由某个类型加载器(ClassLoader 的实体)来载入,因此,Class 类型的实体中,都会有记录载入它的ClassLoader 的实体(注意:如果值是null,不代表它不是由类加载器载入,而是代表这个类別是由(bootstrap loader,也有人称root loader)所载入,只不过这个类型加载器不由java书写,所以逻辑上没有实体 )
二 自定义类加载器
加载类到内存中分两种方式:1)预加载 ;2)显示加载。预加载是虚拟机在启动的时候将rt.jar中的类一次加载到内存,因为这些类都是基础类,会被频繁使用到,预加载可以减少运行时IO开销,显示加载可以:1)使用new()操作符 2)java.lang.Class 裡的forName() 3)java.lang.ClassLoader 裡的loadClass()
要查看类加载详情,可以使用java -verbose:class xxx来输出。看下面一段代码:
public class Main { public static void main(String args[]) { A a1 = new A() ; a1.print() ; B b1 = new B() ; b1.print() ; } } public class A //与Main在同一个路径下 { public void print() { System.out.println("Using Class A") ; } } public class B //与Main在同一个路径下 { public void print() { System.out.println("Using Class B") ; } }到Main所在目录执行javac *.java,查看生成了三个calss文件,再执行java -verbose:class Main > load.log ,查看load.log内容如下:
[Opened D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar] [Loaded java.lang.Object from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar] [Loaded java.io.Serializable from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar] [Loaded java.lang.Comparable from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar] ... ... [Loaded java.security.Principal from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar] [Loaded Main from file:/D:/deep_java/] [Loaded A from file:/D:/deep_java/] Using Class A [Loaded B from file:/D:/deep_java/] Using Class B [Loaded java.lang.Shutdown from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar] [Loaded java.lang.Shutdown$Lock from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]由此可见class类加载顺序。
也可以使用以下方式加载类:
import java.net.* ; public class Test { public static void main(String args[]) throws Exception { Class c = Class.forName(args[0]) ; //第一种载入class对象方法 Object o = c.newInstance() ; //Class c = Class.forName(args[0],true,off.getClass().getClassLoader()) ;//true参数表示载入同时进行初始化,这个参数在SPI接口和实现类加载中非常有用 Test off = new Test() ; System.out.println("类型准备载入") ; //ClassLoader loader = off.getClass().getClassLoader() ;//第二种载入class对象方法,使用了对象引用Class的classloader //Class c = loader.loadClass(args[0]) ; System.out.println("类型准备实例化") ; Object o = c.newInstance() ; Object o2 = c.newInstance() ; } }
了解了默认类加载机制后,可以手工打造一个加载器,ExtClassLoader和AppClassLoader都是继承URLClassLoader,自己的加载器也可以继承自这个类:
import java.net.* ; public class Test { public static void main(String args[]) throws Exception { URL u = new URL("file:/D:/deep_java/test/lib/") ; URLClassLoader ucl = new URLClassLoader(new URL[]{ u }) ; Class c = ucl.loadClass(args[0]) ; Assembly asm = (Assembly) c.newInstance() ; asm.start() ; URL u1 = new URL("file:/D:/deep_java/test/lib/") ; URLClassLoader ucl1 = new URLClassLoader(new URL[]{ u1 }) ; Class c1 = ucl1.loadClass(args[0]) ; Assembly asm1 = (Assembly) c1.newInstance() ; asm1.start() ; System.out.println(Test.class.getClassLoader()) ; System.out.println(u.getClass().getClassLoader()) ; System.out.println(ucl.getClass().getClassLoader()) ; System.out.println(c.getClassLoader()) ; System.out.println(asm.getClass().getClassLoader()) ; System.out.println(u1.getClass().getClassLoader()) ; System.out.println(ucl1.getClass().getClassLoader()) ; System.out.println(c1.getClassLoader()) ; System.out.println(asm1.getClass().getClassLoader()) ; System.out.println(Assembly.class.getClassLoader()) ; }deep_java/test/ 目录结构如下:
├─Test.class ├─Assembly.class ├─lib │ ├─ClassA.class │ ├─ClassB.class │ ├─ClassC.classAssembly 是一个接口,ClassA ClassB ClassC都实现了这个接口,Test主程序在运行时将参数名作为Class名动态加载。命令行输入java -verbose:class Test ClassA 执行结果如下: