您的位置:首页 > 编程语言 > Java开发

Java类的加载机制及常见异常

2016-04-25 12:26 671 查看

Java类的加载机制

Java中一个类若是要被使用,必然经过加载以及初始化的过程。

这里我们来研究一下一个类是如何被加载的,以及加载类时可能会出现的异常

类加载器简介

自定义类加载器

加载过程中可能会出现的异常

Class的加载过程

总结

1.类的加载器简介

一般加载器分为四级:引导类加载器,扩展类加载器,系统类加载器,用户自定义加载器。

通常:

系统类加载器:加载我们自己写的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


2.自定义类加载器

虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,我们还是需要为应用开发出自己的类加载器以满足一些特殊需求。

通常的,自定义类加载器是以继承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;这样的类型转换,会抛出异常么?

3.加载过程中可能会出现的异常

1.ClassNotFoundException

无法找到目标类。

通常加载类的方式:

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();
}

}
}


2.ClassNotFoundError

我们知道一个类在被加载的过程中要经历三个阶段:

读取:找到.class文件,读取

链接:校验读取到的.class文件是否符合规范

初始化:载入静态资源,静态块,产生一个Class对象

ClassNotFoundException发生在“读取”阶段。

ClassNotFoundError发生在“链接”阶段。

两者区别是ClassNotFoundException发生时,可以认为是没有分配内存的,至多是一个byte[]的内存(存放.class字节码)。

而ClassNotFoundError会为Class对象准备好内存。

3.NoClassDefFoundError

当目前执行的类已经编译,但是找不到它的定义时。

也就是说你如果编译了一个类B,在类A中调用,编译完成以后,你又删除掉B,运行A的时候那么就会出现这个错误。

通常发生在“链接”阶段。

4.关于涉及到类型转换的部分

这部分应该不属于类加载时可能会发生的异常范畴,不过还是觉得可以说一下,因为它归根结底还是由于类加载引起的。

看代码:

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认识是不同的东西。

以上两点就能说明这个出现类型转换异常的部分原因。

4.Class的加载过程

这里从源码调度浅要分析一下一个类的加载过程。

以下是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对象,否则抛出异常。

Created with Raphaël 2.1.0开始加载准备工作是否已经被加载返回Class对象End父加载器是否存在尝试加载目标类能否加载进入子加载器yesnoyesnoyesno

大致流程图如上,画的不好请见谅。

5.总结

1.ClassLoader的层级结构

2.类加载时出现的异常及其原因

3.Class加载时的执行流程

4.可以在启动参数内加上-XX:+TraceClassLoading来观察有哪些类在启动时被加载

5.defineClass()方法更多的是用来加载不再classes下的文件,或者是在AOP时覆盖原来类的字节码,需要注意的是,对于同名类使用2次及以上defineClass()回抛出异常。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java class