Java中一个类若是要被使用,必然经过加载以及初始化的过程。
这里我们来研究一下一个类是如何被加载的,以及加载类时可能会出现的异常
- 类加载器简介
- 自定义类加载器
- 加载过程中可能会出现的异常
- Class的加载过程
- 总结
一般加载器分为四级:引导类加载器,扩展类加载器,系统类加载器,用户自定义加载器。
通常:
系统类加载器:加载我们自己写的java文件。
扩展类加载器:一些导入的jar包。
引导类加载器:java运行所需的一些核心类。
自定义加载器:用户自己创建用以加载指定类的加载器。
//一个打印出加载器之间的层级关系的小demo
public class ClassLoaderTree {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
output:
//至于这里没有显示引导类加载器,是因为JDK的自身实现,当获取引导类加载器的时候,返回null。
sun.misc.Launcher$AppClassLoader@4edde6e5
sun.misc.Launcher$ExtClassLoader@79fc0f2f
虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,我们还是需要为应用开发出自己的类加载器以满足一些特殊需求。
通常的,自定义类加载器是以继承ClassLoader,但通常是用继承URLClassLoader来实现的。
public class MyClassLoader extends ClassLoader {
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] data = getClassData(name);
if (data == null) {
throw new ClassNotFoundException();
}
else {
//一般的类加载,我们是提供类名,或者一个路径,让加载器去读取类的字节码,defineClass的功能是将获取到的类的字节码进行加载,我们需要提供类的字节码。
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String path, String className) {
return path + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
//加载完后,若加载成功,拿到Class对象,可以用newInstance()方法将其实例化后就可使用。
之所以继承ClassLoader是因为我们要自己实现在加载一个类的话,需要调用ClassLoader中的一些函数,而这些函数是protected的。就比如上面的defineClass()。
这样,我们就简单的实现了一个自定义的类加载器。
这里先提个问题:
我们用以上方式得到的Class对象进行实例化后,如果对其进行例如A a = (A)o;这样的类型转换,会抛出异常么?
无法找到目标类。
通常加载类的方式:
Class 类中的 forName 方法。
ClassLoader 类中的 findSystemClass 方法。
ClassLoader 类中的 loadClass 方法。
ClassLoader 类中的 defineClass 方法
导致该异常的原因通常有以下几种:
1.类名拼写错误或者没有拼写完整类名(含包名)
2.没有导入相应的jar包
例:
public class BeanLoadDemo {
public static void main(String[] args) {
try {
//该文件不存在,或者不在此包下。
Class c = Class.forName("com.service.util.BeanTest");
c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们知道一个类在被加载的过程中要经历三个阶段:
读取:找到.class文件,读取
链接:校验读取到的.class文件是否符合规范
初始化:载入静态资源,静态块,产生一个Class对象
ClassNotFoundException发生在“读取”阶段。
ClassNotFoundError发生在“链接”阶段。
两者区别是ClassNotFoundException发生时,可以认为是没有分配内存的,至多是一个byte[]的内存(存放.class字节码)。
而ClassNotFoundError会为Class对象准备好内存。
当目前执行的类已经编译,但是找不到它的定义时。
也就是说你如果编译了一个类B,在类A中调用,编译完成以后,你又删除掉B,运行A的时候那么就会出现这个错误。
通常发生在“链接”阶段。
这部分应该不属于类加载时可能会发生的异常范畴,不过还是觉得可以说一下,因为它归根结底还是由于类加载引起的。
看代码:
public class ClassLoaderDemo extends ClassLoader {
public Class define(byte[] buff) {
return defineClass(null, buff, 0, buff.length);
}
public void read() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
int n = 0;
BufferedInputStream br = new BufferedInputStream(
new FileInputStream(
new File("/Users/admin/OpenService/target/classes/com/service/util/beanfactory/BeanTest.class")));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while ((n = br.read()) != -1) {
bos.write(n);
}
br.close();
byte[] buff = bos.toByteArray();
Class clazz = define(buff);
Object o = clazz.newInstance();
BeanTest test = (BeanTest)o;
}
public static void main(String[] args) {
try {
// new ClassLoaderDemo().read();
} catch (Exception e) {
e.printStackTrace();
}
}
}
output:
init
java.lang.ClassCastException: com.service.util.beanfactory.BeanTest cannot be cast to com.service.util.beanfactory.BeanTest
at com.service.util.beanfactory.ClassLoaderDemo.read(ClassLoaderDemo.java:39)
at com.service.util.beanfactory.ClassLoaderDemo.main(ClassLoaderDemo.java:53)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
稍微说明一下,BeanTest这个类中只有一个显示的构造函数(实例化时输出一个“init”),以及一个info 函数(也是简单的输出一个字符串)。
这里加载类的方式是直接读取字节码,然后用ClassLoader的 defineClass 方法来加载。
结果抛出的异常时类型转换失败,而且从描述上看,还是自己转换为自己出现异常。
为什么会出现这样的异常?
由于我们使用的是自定义类加载器直接继承ClassLoader,同时也使用了这个加载器去加载BeanTest 。
可以理解为工人A生产了一个产品P。
但是在默认情况下,我们自己写的类都是有AppClassLoade来加载的。
同样类比于工人头子S生产产品P。
在进行类型转换时,这里用的是: BeanTest b = (BeanTest)o;
这里出现的“BeanTest” ,又是用AppClassLoader来加载的。
那就出现了一个问题,虽然看上去都是产品P,但是是由不同的人生产的。那自然就无法进行转换。于是抛出类型转换异常。
在启动参数内加入 -XX:+TraceClassLoading,可以发现:
[Loaded com.service.util.beanfactory.BeanTest from JVM_DefineClass]
[Loaded com.service.util.beanfactory.BeanTest from file:/Users/admin51/OpenService/target/classes/]
也可以证明,该类在不同加载器内分别被加载。
根据Class加载的文档资料:
1.跨ClassLoader访问一些数据是比较麻烦的,但是并不是不能做到,比如JMX2。
2.同一个类在同一个ClassLoader中只能加载一次,言下之意就是在不同ClassLoader种可以加载多次。也说明由不同ClassLoader产生的同一个类的Class对象,JVM认识是不同的东西。
以上两点就能说明这个出现类型转换异常的部分原因。
这里从源码调度浅要分析一下一个类的加载过程。
以下是ClassLoader中的一个方法:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 确认该类是否已经被加载,调用的是一个本地方法。
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//parent表示当前加载器的父加载器
if (parent != null) {
//先由父加载器去尝试加载,同样会进入父加载器的该函数内,继续尝试用其父加载器去加载
c = parent.loadClass(name, false);
} else {
//如果父加载器不存在,就用根加载器去加载它
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常,找不到该类。
}
if (c == null) {
//如果还是无法加载Class,则会去调用一个本地方发findClass去加载这个类。
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
过程:
1.负责加载的加载器先去搜寻其已经加载过的类,是否包含目标类。
2.若未被加载,询问其父加载器是否加载过
3.若父加载器不存在,则有当前加载器尝试进行加载。
4.若父加载器加载不了,则用子加载器尝试进行加载。
5.若加载成功,返回Class对象,否则抛出异常。
大致流程图如上,画的不好请见谅。
1.ClassLoader的层级结构
2.类加载时出现的异常及其原因
3.Class加载时的执行流程
4.可以在启动参数内加上-XX:+TraceClassLoading来观察有哪些类在启动时被加载
5.defineClass()方法更多的是用来加载不再classes下的文件,或者是在AOP时覆盖原来类的字节码,需要注意的是,对于同名类使用2次及以上defineClass()回抛出异常。