JVM笔记7:类加载器

时间:2022-06-18 07:43:54

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码块被称为“类加载器”

Java中的类加载器主要有2类,一类是系统提供的,另一类是由Java应用开发人员编写的,系统提供的类加载器主要有下面3个:

1,启动类加载器(Bootstarp ClassLoader)

将存放在<JAVA_HOME>\lib(JDK1.6)目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别(仅按照文件名识别,如:rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)的类库加载到虚拟机内存中

启动类加载器无法被Java程序直接引用

2,扩展类加载器(Extension ClassLoader)

负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库

由sun.misc.Launcher$ExtClassLoader实现

开发者可以直接使用扩展类加载器

3,应用程序类加载器(Application ClassLoader)

负责加载用户类路径(ClassPath)上锁指定的类库

由sun.misc.Launcher$AppClassLoader实现

public class Test {
public static void main(String[] args){
System.out.println(ClassLoader.getSystemClassLoader());
}
}

结果为:

sun.misc.Launcher$AppClassLoader@addbf1

开发者可以直接使用扩展类加载器

4,自定义类加载器

除了系统提供的类加载器之外,开发人员可以通过继承java.lang.ClassLoader类并重写该类的findClass方法的方式实现自己的类加载器

//-XX:+TraceClassLoading
public class MyClassLoader extends ClassLoader{
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("Use myclassloader findClass method.");
//name = com.test.Test
//fileName = Test.class
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
byte[] bytes = loadClassData("e:\\"+fileName);
return defineClass(name, bytes, 0, bytes.length);
} public byte[] loadClassData(String name) {
try {
FileInputStream fileInput = new FileInputStream(new File(name));
ByteArrayOutputStream bytesOutput = new ByteArrayOutputStream();
int b = 0;
while ((b = fileInput.read()) != -1) {
bytesOutput.write(b);
}
return bytesOutput.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
} public static void main(String[] args){
MyClassLoader myClassLoader = new MyClassLoader();
try {
Class<? extends Object> testClass = myClassLoader.loadClass("com.test.Test");
Object obj = testClass.newInstance();
System.out.println(obj.getClass().getName());
System.out.println(obj.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}

将Test类编译后生成的Test.class文件放到e盘下

public class Test {
public Test(){}
}

运行结果为:

......
[Loaded com.test.MyClassLoader from file:/E:/eclipseProject/jvm/bin/]
Use myclassloader findClass method.
[Loaded java.io.ByteArrayOutputStream from shared objects file]
[Loaded com.test.Test from __JVM_DefineClass__]
com.test.Test
827574
......

从输出结果可以看到com.test.Test是由MyClassLoader类加载的

绝大部分Java程序都是由这4种类加载器相互配合进行加载的,它们之间的关系如下:

JVM笔记7:类加载器

类加载器之间的这种层次关系,被称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都要有自己的父类加载器

public class Test {
public static void main(String[] args){
ClassLoader loader = Test.class.getClassLoader();
while(null != loader){
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
sun.misc.Launcher$AppClassLoader@82ba41
sun.misc.Launcher$ExtClassLoader@923e30

第一个输出的为Test类的类加载器:应用程序类加载器,是sun.misc.Launcher$AppClassLoader类的一个实例;第二个输出的为扩展类加载器,是sun.misc.Launcher$ExtClassLoader类的一个实例;这里没有输出启动类加载器,原因是如果父类加载器为启动类加载器,getParent()方法将返回null

加载器之间的父子关系一般不使用继承来维护,而是通过组合复用父类加载器的代码

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有加载器的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

可以看一下ClassLoader类中的loadClassInternal方法,虚拟机调用该方法加载类:

// This method is invoked by the virtual machine to load a class.
private synchronized Class loadClassInternal(String name)
throws ClassNotFoundException{
return loadClass(name);
} //Invoking this method is equivalent to invoking loadClass(name,false).
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
} //Subclasses of ClassLoader are encouraged to override findClass(String),
//rather than this method.
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 = 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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c); }
return c;
}

通过loadClass方法的源代码可以看出,类加载器会先检查类是否已经被加载过,如果没有加载过则调用父类加载器加载该类(如果父类加载器为空则默认使用启动类加载器作为父类加载器),如果父类加载器加载失败,调用自己的findClass方法进行加载

比起重写loadClass方法,JDK更推荐通过重写findClass方法实现自定义类加载器(详见备注1)

来看看JDK对findClass方法的描述:

/**
* Finds the class with the specified binary name.
* This method should be overridden by class loader implementations that
* follow the delegation model for loading classes, and will be invoked by
* the loadClass method after checking the parent class loader
* for the requested class. The default implementation
* throws a ClassNotFoundException.
*
* @param name
* The binary name of the class
*
* @return The resulting Class object
*
* @throws ClassNotFoundException
* If the class could not be found
*
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

对loadClass方法中的resolveClass方法也比较好奇,顺带查看了下这个方法的作用:

/**Links the specified class.  This (misleadingly named) method may be
* used by a class loader to link a class. If the class has already
* been linked, then this method simply returns. Otherwise, the class
* is linked as described in the "Execution" chapter of the Java Language
* Specification.
*/
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
} private native void resolveClass0(Class c);

可以发现虚拟机将调用该方法完成类的连接过程,类的连接过程详见:http://blog.csdn.net/a19881029/article/details/17068191

查看ClassLoader类的源代码会发现很多方法是通过调用本地方法(native修饰符修饰的方法)的方式实现的

使用双亲委派模型组织类加载器之间的关系的好处是:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:java.lang.Object类,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,启动类加载器在其搜索范围内可以搜索到的只有rt.jar中的java.lang.Object类(详见备注2),这样可以保证Object类始终由启动类加载器从rt.jar中的java.lang.Object加载得到,确保了Object类的唯一性(详见备注3)

如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己实现一个名为java.lang.Object类,并用自定义的类加载器进行加载,系统中将出现多个不同的Object类,Java类型体系中最基础的行为将无法保证,应用程序也将变得一片混乱

备注:

1,为什么JDK不推荐通过重写loadClass方法实现自定义类加载器?

通过重写findClass方法实现自定义类加载器:当调用loadClass方法加载类时,由于自定义类加载器没有重写loadClass方法,实际调用的是ClassLoader类的loadClass方法,该方法保证如果父类能够加载所需加载的类,则把加载动作委托给父类完成,当所有父类都无法完成加载动作时,才把加载动作交由自定义类加载器的findClass方法完成,完全符合Java类加载器双亲委派模型的设计思路

通过重写loadClass方法实现自定义类加载器:当调用loadClass方法加载类时,将直接调用自定义类加载器中重写的loadClass方法完成加载动作,如果重写的loadClass方法中没有实现首先尝试将加载动作委托给父类完成这一过程,将打破双亲委派模型的设计思路,设计是可以被打破的,但是需要更好的理由(JDBC,JNDI就打破了双亲委派模型)

当然,如果在重写的loadCLass方法中首先尝试让父类加载器完成加载过程,则本质上也是没有没有问题的,只是依然别扭罢了,首先就是为什么不使用现成的实现?其次如果父类加载器无法完成加载动作,还是要把加载过程委托给自定义类加载器的findClass方法,关键问题是,在ClassLoader类中,findClass是一个空方法,也就是说你还是得重写自己的findClass方法,绕了一大圈,又回来了,除非你能确定父类加载器能够完成加载动作,这时将不会调用自定义类加载器的findClass方法,不过这样一来,你为什么要实现自己的类加载器?综上,使用重写findClass方法实现自定义的类加载器就对了,不过下面依然尝试了一下通过重写loadClass方法实现自定义类加载器:

//-XX:+TraceClassLoading
public class MyClassLoader extends ClassLoader{
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
} @Override
public Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("Use myclassloader findClass method.");
//name = com.test.Test
//fileName = Test.class
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
byte[] bytes = loadClassData("e:\\"+fileName);
return defineClass(name, bytes, 0, bytes.length);
} public byte[] loadClassData(String name) {
try {
FileInputStream fileInput = new FileInputStream(new File(name));
ByteArrayOutputStream bytesOutput = new ByteArrayOutputStream();
int b = 0;
while ((b = fileInput.read()) != -1) {
bytesOutput.write(b);
}
return bytesOutput.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
} public static void main(String[] args){
MyClassLoader myClassLoader = new MyClassLoader();
try {
Class<? extends Object> testClass = myClassLoader.loadClass("com.test.Test");
Object obj = testClass.newInstance();
System.out.println(obj.getClass().getName());
System.out.println(obj.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}

还是将Test.class文件放置在e盘下:

......
Use myclassloader findClass method.
[Loaded java.io.ByteArrayOutputStream from shared objects file]
[Loaded com.test.Test from __JVM_DefineClass__]
com.test.Test
827574
......

当然如果Test.class文件与MyCLassLoader.class文件放置在同一个路径下,应用程序类加载器(也就是MyClassLoader类加载器的父类加载器)将完成Test类的加载动作,此时不会跳进MyClassLoader类的findClass方法,运行结果如下:

......
[Loaded com.test.Test from file:/E:/eclipseProject/jvm/bin/]
com.test.Test
21174459
......

2,在自定义类加载器中,使用defineClass方法加载一个我自己实现的java.lang.Object类

package java.lang;

public class Object {
public Object(){}
}

运行时抛出下面的异常(被禁止的包名称):

java.lang.SecurityException: Prohibited package name: java.lang

事实上,加载所有以"java."开头的类都会抛出这个异常,这应该是出于JDK对其自身实现的基础类的保护

......
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
......

3,比较2个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这2个类是源于同一个Class文件,只要加载它们的类加载器不同,这2个类必定不相等(个人认为这是很容易理解的,同一个Class文件被2个不同的Java进程加载所产生的2个类肯定是不同的。判断2个类相等,最终判断的还是其指向的已分配内存区是否为同一个,对于2个独立的Java进程,其使用的内存空间是没有交集的)