类加载器概述
类加载从JDK1.0就有,最初是为满足Java Applet的需要开发出来的,虽说Java Applet现在早已死翘翘,但是类加载器在别处绽放光彩,如热部署。
类加载器,顾名思义就是加载Java类到虚拟机中,负责读取Java字节码,并转换成java.lang.Class类的一个实例,通过newInstance()方法就可以创建出该类的一个对象,这里的读取可以从本地文件,或者从网络上读取,这个类由java.lang.ClassLoader定义。可以把类加载器比如成咖啡,程序员是字节码,程序员通过咖啡,生产出程序,程序就是java.lang.Class。
通过class.getClassLoader()方法可以获取加载此类的类加载器。
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由开发人员编写的。
系统提供的类加载器主要有下面三个:
引导类加载器(BootstrapClassLoader):用来加载 Java 的核心库,也就是%JAVA_HOME%/jre/lib
目录,用原生代码(C++)写。
扩展类加载器(ExtClassLoader):用来加载 Java 的扩展库,负责加载%JAVA_HOME%/jre/lib/ext
下目录中的类库。实现类是sun.misc.Launcher$ExtClassLoader
应用类加载器(ApplicationClassLoader):也称之为系统类加载器,负责加载当前应用classpath路径下的类库,在没有自定义类加载器的时候,开发人员所编写的类都是由它来完成加载,可以通过 ClassLoader.getSystemClassLoader()来获取它。实现类是sun.misc.Launcher$AppClassLoader
也就是说对于不同的类,Class.getClassLoader()一般在没有其他干扰下,会返回以上三种类加载器,但是要注意的是,返回null不是没有类加载器,而是代表BootstrapClassLoader,并且除了BootstrapClassLoader,其他两种都是继承自ClassLoader。
类加载验证
BootstrapClassLoader
首先验证引导类加载器,可以通过以下代码获取BootstrapClassLoader所加载的目录或者jar。
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
或者通过属性方式.
String[] split = System.getProperty("sun.boot.class.path").split(":");
for (String s : split) {
System.out.println(s);
}
他输出如下,其中最后一行有个jre/classes目录,这个目录默认不存在,应该是留给用户的。
也就是这些都是由引导类加载器加载,其中就包括了核心类rt.jar。
通过System.out.println(String.class.getClassLoader());
获取String类的ClassLoader,此时会发现是null,则代表是引导类加载器加载,同样其他jar包中的类一个道理,比如jsse.jar中的SunRsaSign类System.out.println(SunRsaSign.class.getClassLoader());
同样会输出null。
如果想让我们的类让他加载,可以指定参数-Xbootclasspath/a: <目录>。
比如HxlClass的包名是com.company,把他带包放入/home/test目录下,并指定参数-Xbootclasspath/a:/home/test,通过Class.forName(“com.company.HxlClass”)方式加载他,同样输出null。这时候可以通过反射对HxlClass类为所欲为,其实这个可以放在jre/classes下,也一样。
Object o = Class.forName("com.company.HxlClass").newInstance();
System.out.println(o.toString());
当然在加载jar包的时候,要指明jar的名字,如-Xbootclasspath/a:/home/test/LibJava.jar。
ExtClassLoader
接下来是扩展类加载器,负责加载%JAVA_HOME%/jre/lib/ext
下的所有类,通过以下方式可以获取加载的路径。
System.out.println(System.getProperty("java.ext.dirs"));
他输出如下,而另一个路径也是不存在的,需要自己创建。
到%JRE_HOME%/jre/lib/ext
目录下,有很多扩展包。
拿zipfs.jar来说,里面有一个ZipFileSystem类,输出他的加载器的时候是sun.misc.Launcher$ExtClassLoader,则表示他是由扩展类加载器加载。
System.out.println(ZipFileSystem.class.getClassLoader());
同样的操作,如果想让我们的类让他加载,要指定参数-Djava.ext.dirs=<路径>,如-Djava.ext.dirs=/home/test,在次使用Class.forName(“com.company.HxlClass”)加载此类,并获取他的加载器,则会输出sun.misc.Launcher$ExtClassLoader,也可以放入另一个目录下,需要自己创建,也就是上面说的。
Object o = Class.forName("com.company.HxlClass").newInstance();
System.out.println(o.getClass().getClassLoader());
但是当指定-Djava.ext.dirs=/home/test的时候,会发现以前扩展类的路径下的类无法加载。原因是-Djava.ext.dirs有覆盖性,解决办法是指明原来的扩展路径,多个路径用:分割。
加入原来的扩展目录后再次运行。
但是在Idea中运行时,ZipFileSystem是由sun.misc.Launcher$AppClassLoader加载,并没有报错。这是因为idea把扩展类的目录增加到了classpath中,由SystemClassLoader加载了,这是由于双亲委派导致。
SystemClassLoader
主要负责加载classpath所指定位置下的类或者jar,通过以下方式可以获取路径。
System.out.println("---"+System.getProperty("java.class.path"));
一般情况下,我们自己编写的类是由他加载,可以通过-classpath指定路径。当你程序运行抛出ClassNotFoundException时候,可以通过他指明缺少类的路径来解决。
双亲委派
思想是自己不想干,让父亲帮忙干。
类加载器在尝试自己查找某个类的字节代码并加载时,会先委托给他的父类加载器,由父类加载器先去尝试加载,以此类推。如果父亲能加载成功,那就直接返回,如果父亲加载不了,则在向下传递,由子类完成,比如SystemClassLoader尝试加载类的时候,先委托给ExtClassLoader,ExtClassLoader又委托给BootstrapClassLoader,在没有更上一层了,如果BootstrapClassLoader无法加载,那就向下让ExtClassLoader加载,成功则直接返回,ExtClassLoader加载不成功则SystemClassLoader加载,如果SystemClassLoader加载不了,则抛出异常。
用一个例子可以验证,首先在/home/test目录下放一个LibJava.jar,其中有个类是com.company.HxlClass,然后尝试加载他,并输出他的类加载器。
测试代码如下:
public static void main(String[] args) throws ClassNotFoundException{
System.out.println(Class.forName("com.company.HxlClass").getClassLoader());
}
如果不向三个类加载器中某一个指明这个jar路径时,肯定是抛出ClassNotFoundException异常,意味着三个类加载都不知道他的路径,都无法加载。
当向其中一个类加载器指明这个路径后,加载com.company.HxlClass的一定是他,如下图,因为只有他知道。
如果三个类加载器加载的路径下都有这个jar路径,则一定是BootstrapClassLoader加载,如下图。因为遵守规则,父亲能干的父亲干。
判断是否同一个类
Java 虚拟机不仅要看类的全名是否相同,还要看加载这个类的类加载器是否一样,只有都相同的情况下,才认为两个类是相同的,否则即便是同样的字节代码,被不同的类加载器加载之后,会认为是不同的。
这个很容易就验证。编写一个Dog类,代码如下。首先创建两个自定义的类加载,并加载同一个类,在利用对象的强制转换,如果转换失败则会抛异常。
public class Dog {
public void setDog(Object o){
System.out.println("setDog>>"+o);
Dog dog =(Dog)o;
}
}
测试代码如下。
public static void main(String[] args) throws ClassNotFoundException,
NoSuchMethodException, IllegalAccessException,
InstantiationException, InvocationTargetException {
class TestClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
FileChannel channel = new FileInputStream("/home/test/Dog.class").getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int size = 0;
while ((size = channel.read(byteBuffer)) != -1) {
byteBuffer.limit();
byteArrayOutputStream.write(byteBuffer.array(), 0, size);
}
return defineClass(name, byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
throw new ClassNotFoundException();
}
}
TestClassLoader classLoader1 = new TestClassLoader();
TestClassLoader classLoader2 = new TestClassLoader();
Class<?> cls1 = classLoader1.loadClass("Dog");
Class<?> cls2 = classLoader2.loadClass("Dog");
System.out.println(cls1.getClassLoader() +" "+ cls2.getClassLoader());
Object o1 = cls1.newInstance();
Object o2 = cls2.newInstance();
Method setDog1 = cls1.getDeclaredMethod("setDog", Object.class);
System.out.println(setDog1 +" "+setDog1);
setDog1.invoke(o1,o2);
}
运行后,发现他会报ClassCastException异常。
自定义类加载器
在上面已经演示了一个自定义类加载器TestClassLoader,要想自定义,首先继承ClassLoader,然后重写findClass方法,返回值是Class对象,通过内部defineClass将class文件的byte[]转换成Class对象,defineClass是java层的方法,最终会调用到defineClass1这个native方法。
下面是完成从网络中读取字节码并加载的NetworkClassLoader。
public class NetworkClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
URL url = new URL("http://blog.houxinlin.com/Dog.class");
InputStream inputStream = url.openStream();
byte[] data = new byte[2048];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int size = 0;
while ((size = inputStream.read(data)) != -1) {
byteArrayOutputStream.write(data, 0, size);
}
return defineClass(name, byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size());
} catch (IOException ex) {
ex.printStackTrace();
}
throw new ClassNotFoundException();
}
}
测试代码。
NetworkClassLoader networkClassLoader =new NetworkClassLoader();
System.out.println(networkClassLoader.loadClass("Dog"));
如果想从jar中加载某个类,可以使用URLClassLoader,并且AppClassLoader和BootstrapClassLoader都继承他。
String jarFile ="/home/test/LibJava.jar";
URL url1 =new File(jarFile).toURL();
URLClassLoader myClassLoader = new URLClassLoader(new URL[]{url1});
JarFile file =new JarFile(jarFile);
Enumeration<JarEntry> entries = file.entries();
while (entries.hasMoreElements()){
JarEntry jarEntry = entries.nextElement();
if (!jarEntry.isDirectory()){
if (jarEntry.getName().endsWith(".class")){
String name =jarEntry.getName().replaceAll("/",".");
name=name.substring(0,name.length()-6);
System.out.println(myClassLoader.loadClass(name).newInstance().toString());
}
}
}
源码分析
这要追随到Launcher类,java的入口。这个类由BootstrapClassLoader加载。其中this.loader作为getClassLoader方法的返回值,也就是说可以通过调用Launcher.getLauncher().getClassLoader()也可以拿到AppClassLoader。
public Launcher() {
//扩展类加载器
Launcher.ExtClassLoader var1;
try {
//实例化扩展类加载器,单例模式
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//实例化AppClassLoader,单例模式,并将AppClassLoader的父加载器设置成ExtClassLoader。
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//对当前线程设置类加载器
Thread.currentThread().setContextClassLoader(this.loader);
}
其中在getExtClassLoader调用层中调用到了getExtDirs方法,获取扩展类的目录集合,最后把这个File[]传递到ExtClassLoader构造方法中。
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
同样的getAppClassLoader也是获取到java.class.path的值,实例化AppClassLoader需要两个参数,一个是ava.class.path,一个是父ClassLoader。
接下来是ClassLoader中的loadClass方法,双亲委派也就在这里。调用所有爸爸们的loadClass如果都无法加载,则调用自己的findClass尝试加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//如果已经被加载。直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果存在父ClassLoader,则让父ClassLoader先尝试加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//不存在,则交给BootstrapClass,
//BootstrapClass会调用到findBootstrapClass这个native层方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
//如果还等于null,意味着各位爸爸们都无法加载,自己来,调用findClass,也就是为什么自定义类加载器要重写findClass
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;
}
}
Tomcat类加载器
在Tomcat中,每个 Web 应用都有一个对应的类加载器,不同的是首先自己去尝试加载某个类,如果找不到则交给父加载器,与上面双亲委派的顺序相反,这是 Java Servlet 规范中的推荐做法。比如,有两个Web应用,都采用了某个类库,一个采用1.0版本,一个采用2.0版本,此时如果采用一个类加载器,那么导致jar覆盖,可能无法启动成功。
Tomcat这样的作法就保证了隔离性,灵活性,和性能。