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对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序