王森写《Java深度歷險》第二章《深入類別載入器》一文对Java类加载机制给出了很好的解释,读了以后有个啥印象呢?
预先加载还是按需加载?
什么时候JVM加载程序需要的类呢?
两种情况:
在系统启动的时候全部加载进来 or 当程序调用类之前加载
SUN的JVM(1.4为例)使用了上面的两种调用方式。
举个例子:
public class Main {
public static void main(String[] args) {
new ClassA().MethodA();
}
}
public class ClassA {
public void MethodA(){
System.out.println("Use ClassA");
}
}
java -verbose:class Main
用JDK5.0编译的结果
[Opened D:\Java\jdk1.5\jre\lib\rt.jar] (开始加载java基础类所在的包,可以看到JDK1.5就加载了 rt.jar jsse.jar jce.jar charsets.jar 几个jar包)
[Opened D:\Java\jdk1.5\jre\lib\jsse.jar]
[Opened D:\Java\jdk1.5\jre\lib\jce.jar]
[Opened D:\Java\jdk1.5\jre\lib\charsets.jar]
[Loaded java.lang.Object from D:\Java\jdk1.5\jre\lib\rt.jar] (开始加载jar包里面的类)
.
.
.
[Loaded java.security.cert.Certificate from D:\Java\jdk1.5\jre\lib\rt.jar]
[Loaded com.zte.classLoaderTest.Main from file:/D:/Eclipse/workspace3.2.2/ClassLoaderTest/bin/] (开始加载程序入口类Main)
[Loaded com.zte.classLoaderTest.ClassA from file:/D:/Eclipse/workspace3.2.2/ClassLoaderTest/bin/] (加载类ClassA)
Use ClassA
[Loaded java.lang.Shutdown from D:\Java\jdk1.5\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Java\jdk1.5\jre\lib\rt.jar] (看看SUN的注释 governing the virtual-machine shutdown sequence. 大家应该明白了吧?)
用JDK6.0编译的结果
[Loaded java.lang.Object from shared objects file] (看看JDK6.0是不是有点不一样啊?没有加载jar包的一步了,据说JDK6.0的JVM速度增加了,猜测是为了速度优化了)
[Loaded java.io.Serializable from shared objects file]
.
.
.
.
[Loaded java.security.Principal from shared objects file]
[Loaded java.security.cert.Certificate from shared objects file]
[Loaded com.zte.classLoaderTest.Main from file:/D:/Eclipse/workspace3.2.2/ClassLoaderTest/bin/] (同上)
[Loaded com.zte.classLoaderTest.ClassA from file:/D:/Eclipse/workspace3.2.2/ClassLoaderTest/bin/] (同上)
Use ClassA
[Loaded java.util.AbstractList$Itr from shared objects file] (从这里开始有点不一样了,这是在干什么?JDK6.0的shutdown吧!呵呵 )
[Loaded java.util.IdentityHashMap$KeySet from shared objects file]
[Loaded java.util.IdentityHashMap$IdentityHashMapIterator from shared objects file]
[Loaded java.util.IdentityHashMap$KeyIterator from shared objects file]
[Loaded java.io.DeleteOnExitHook from shared objects file]
[Loaded java.util.LinkedHashSet from shared objects file]
[Loaded java.util.HashMap$KeySet from shared objects file]
[Loaded java.util.LinkedHashMap$LinkedHashIterator from shared objects file]
[Loaded java.util.LinkedHashMap$KeyIterator from shared objects file]
从上面的例子可以很清除的看到在JVM刚启动的时候是使用预先加载(pre-loading)的机制将java基础类的jar包都加载进来。当基础类都加载结束以后,开始加载static main 所在的类。调用static main方法,方法中使用到了类ClassA 所以先加载ClassA,然后调用ClassA的方法。也就是说,当JVM执行完初始操作(加载基础类)以后,对待客户程序使用按需加载(load-on-demand)的方式,当程序需要的时候JVM执行加载操作。
Java程序的动态性
Java程序是具有动态性的,平时我们使用到Java的动态性比较少,new object()的时候我们没有意识到,但是Java的动态性开始启动了.......
Java有两种方式来达到动态性: 显示的和隐式的
两种方式在底层实现上来说都使用的同样的机制,差异在Java程序设计师使用的代码不同。
隐式的(implicit)
当你的java代码中出现new的时候,类的动态加载机制就开始了。
显示的(explicit)
由于隐式的类加载没有弹性,所以SUN提供了显示的方式来动态加载类。
显示加载的两个方法:
Class的forName方法 & ClassLoader的loadClass方法
还是让我们看一个例子吧:
显示动态加载的例子
public interface Assembly {
public void run();
}
public class Excel implements Assembly {
public void run() {
System.out.println("Excel is run");
}
}
public class Word implements Assembly {
public void run() {
System.out.println("word is run");
}
}
public class Main {
public static void main(String[] args) throws Exception {
Class c = Class.forName(args[0]);
Object o = c.newInstance();
Assembly a = (Assembly) o;
a.run();
}
}
有了代码就不多说了........
从上面的代码可以看到:
通过Class.forName(String s) 来获取类的Class类型 通过Class的newInstance()来加载类,获取实例。
还有一个方法Class forName(String s, boolean flag, ClassLoader classloader) s 表示需要加载的类的名称
flag表示加载类的时候是否先初始化类的静态区 classloader表示需要用到的类加载器
默认的一个参数的newInstance的flag是true,就是“载入类别+呼叫静态初始化块”
如果使用这种方法的话,需要有一个类加载器 由于ClassLoader.getCallerClassLoade()是私有的方法所以不能直接获取类加载器。所以我们使用的时候可以随便找一个类然后获取它的类加载器。
Test test = new Test();
ClassLoader c1 = test.getClass().getClassLoader();
或者
Class b = Test.Class;
ClassLoader c1 = b.getClassLoader();
这样就可以获取ClassLoader了。
然后使用Class.forName(String s).newInstance() 还是ClassLoader.loadClass()就自便了。
ClassLoader怎么获取是有一些说道的:
要想获取ClassLoader需要先有一个Class
在Java中每个类别的老祖宗都是Object,而Object有一个getClass的方法可以得到一个Class类别(Class.class)的实体。但这个方法是私有的,别想动。那么这个Class类别实体从何而来,是在.class载入JVM的时候生成的,以后我们再想得到一个类的实例的时候就需要通过这个Class.class代理来与JVM里面的.class来通讯了。
个人胡思乱想: Object里面的getClass肯定是交给JVM加载类的时候使用的;使用Class.class的时候如果该Class已经加载则OK,如果没有加载那么JVM会先加载它。
这个ClassLoader到底是何物,还不是很清楚,还是继续看下去吧!
自定义类加载器来加载类
这里我们使用到了Java的java.net.URLClassLoader
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
// 使用默认的newInstance方法
// Class c = Class.forName(args[0]);
// Object o = c.newInstance();
// 使用
// ClassLoader loader = Word.class.getClassLoader();
// Class c = loader.loadClass(args[0]);
// Object o = c.newInstance();
// 自定义URLClassLoader
URL url = new URL("file:/D:/Eclipse/workspace3.2.2/ClassLoaderTest/com");
URLClassLoader loader = new URLClassLoader(new URL[] { url });
Class c = loader.loadClass(args[0]);
Object o = c.newInstance();
Assembly a = (Assembly) o;
a.run();
URL url2 = new URL("file:/D:/Eclipse/workspace3.2.2/ClassLoaderTest/com");
URLClassLoader loader2 = new URLClassLoader(new URL[] { url2 });
Class c2 = loader2.loadClass(args[0]);
Object o2 = c2.newInstance();
Assembly a2 = (Assembly) o2;
a2.run();
}
}
上面的例子按照王森所说的应该是出现同一个类别加载两次的情况,但我屡试不行。暂且记下吧!
按王森所说一个类只能被同一个ClassLoader加载一次,但是可以被不同的ClassLoader同时加载。
JVM中应用类加载器的结构
编译后的.class是如何启动的呢?下面说一下这个过程:
当我们在命令行输入 java XX(.class)的时候,java.exe根据一定的规则找到JRE(规则见下文)。接着找到JRE之中的JVM.DLL(真正的java虚拟机),最后载入这个动态链接库启动java虚拟机。
虚拟机一启动先做一些初始化的动作,比如获取系统参数等。之后就产生第一个类加载器,即所谓的Bootstrap Loader。Bootstrap Loader是由C++编写的,这个Loader进行了一些初始化操作以后,最重要的是载入定义在sun.misc命名空间下的Launcher.java之中的ExtClassLoader(因为是inner class ,所以编译之后变成Launcher$ExtClassLoader.class),并设定parent为null,代表其父加载器为Bootsrap Loader。然后,Bootstrap Loader再载入Launcher.java之中的AppClassLoader(同理为Launcher$AppClassLoader.class)并设定parent为ExtClassLoader
如上的过程用代码测试一下:
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader loader = Main.class.getClassLoader();
System.out.println(loader);
ClassLoader loaderParent = loader.getParent();
System.out.println(loaderParent);
ClassLoader loaderParentParent = loaderParent.getParent();
System.out.println(loaderParentParent);
}
}
结果为:
sun.misc.Launcher$AppClassLoader@131f71a
sun.misc.Launcher$ExtClassLoader@15601ea
null
从上面的例子我们能够很清除的看出ClassLoader的层次关系。
【说明】Bootstrap Loader为null,是因为它是由C++写的没法表示
此外,大家需要注意的是AppClassLoader和ExtClassLoader都是URLClassLoader的子类别。
AppClassLoader搜索的路径是由java.class.path取出的路径决定的。
java.class.path值的设定是由-cp 或-classpath或path环境变量决定的。
System.out.println(System.getProperty("java.class.path"));
结果为:
D:\Eclipse\workspace3.2.2\ClassLoaderTest\bin
ExtClassLoader搜索的路径是由java.ext.dirs决定的。
System.out.println(System.getProperty("java.ext.dirs"));
结果为:
D:\Java\jdk1.5\jre\lib\ext
java.ext.dirs的内容是由java.exe选择的jre路径下面的 jre\lib\ext决定的。
最后一个是Bootstrap Loader了,它是由“sun.boot.class.path”决定的。
System.out.println(System.getProperty("sun.boot.class.path"));
结果为:
D:\Java\jdk1.5\jre\lib\rt.jar;D:\Java\jdk1.5\jre\lib\i18n.jar;D:\Java\jdk1.5\jre\lib\sunrsasign.jar;D:\Java\jdk1.5\jre\lib\jsse.jar;D:\Java\jdk1.5\jre\lib\jce.jar;D:\Java\jdk1.5\jre\lib\charsets.jar;D:\Java\jdk1.5\jre\classes
系统参数sun.boot.class.path需要在虚拟机启动前修改。作为java.exe的参数 -Dsun.boot.class.path = XXXXX
从名称可以看出 sun.boot.class.path sun.class.path 都是看到的路径,对该路径下面文件夹的东西就不管了。而sun.ext.dirs则告诉我们它会搜索dirs,也就是说如果指定目录没有搜索到就会搜索下层路径。
我们可以看出,使用dirs肯定是一个比较费时的操作,需要去逐层的搜索。
【说明】当JVM启动以后我们的这些参数是不能再修改的了,即使你使用System.setProperty方法也不行。因为在系统启动的时候读取以后就保存了一份。
前面说到了类加载器的结构,有这样一种结构有什么作用呢?下面来说明:
【类加载器可以看到parent加载器的所载入的所有类型,反之不行】
举例:加载Class的时候先是AppClassLoader被调用,AppClassLoader先请求它的parent加载器ExtClassLoader,ExtClassLoader也先请求它的parent加载器BootstrapClassLoader。如果BootstrapClassLoader能够加载就OK,以后都只能使用BootstrapClassLoader来加载,如果没有找到则返回ExtClassLoader来加载,如果没有找到就到AppClassLoader去搜索。
需要说明的是,如果你的Main所在的类是由ExtClassLoader加载的,如果后续加载的类不在ExtClassLoader和BootstrapClassLoader所在的路径下则会出现找不到类的异常,即使AppClassLoader能够搜索到也不行。
比方说:JDBC API的核心类都是使用BootstrapClassLoader来加载的,但是JDBC driver是由ExtClassLoader来加载的。其实不只是JDBC其他的java API都是这样的模式,估计是为了安全考虑。
但是这样岂不是会出现上面所说的找不到类的情况吗?有Context Class Loader来解决。
【java.exe怎么找到jre】
1.java.exe自己的路径下是否有JRE目录。
2.父目录下是否有JRE目录。
3.查找HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment下面的JavaHome指向的路径和RuntiemLib指向的jvm.dll(JDK6.0下面的注册表为例)
至于用的是哪个java.exe 找一下环境变量吧!
常见错误 不知道是哪个java.exe 调用了那个JRE里面的jvm.dll