类加载机制
一、类冲突测试
写了两个类路径完全一样的类,然后分别打包成a.jar和b.jar。在另一个project里面同时依赖这两个jar包,eclipse不会报错,编译和运行也不会报错。而JVM真正载入的类是a.jar的类(JVM应该会根据jar的名称顺序来载入类吧)。
二、jar包结构
1、导出runnable jar file
此时导出的类包含该project包含的所有类(pom里面的类,buildpath里面设置的类)
包结构如下图:
2、导出非runnable jar(现在使用maven开发,使用的都是这种形式)
此时导出的只有本工程自己的class文件,只有pom文件,没有依赖的class文件。
保包结构如下图:
3、war包结构分析
一个webapp下的目录结构如下图。
WEB-INF的结构如下图。其中classes文件是自己的class文件,lib是依赖的外部类。还需要注意web.xml也在该目录下。
三、系统类加载器
可以通过静态方法ClassLoader.getSystemClassLoader()获得,获得的ClassLoader可以认为是单例的
该类加载器在父类加载器加载不到类的时候,在java.class.path下查找类
四、当前类加载器自动加载和指定类加载器加载
1、什么是当前类加载器自动加载? 不是程序使用specificClassloader.loadClass(String)来设置类加载器来加载
Object a=new Object();
Class.forName(String); //和上面的含义一样
2、指定类加载器 (需要打破当前类加载器的限制)
1、线程上下文类加载器
2、通过新建或者object.getClassLoader()获得类加载器来加载类(classloader.loadClass())
五、Class.forName(String)
源码如下。红色字体说明Class.forName实际上是使用当前类加载器进行加载。实现里面最后调用了native方法。
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
下面是JVM的执行过程分析:
1、系统字典(包含3列,根据类的全路径和类的定义加载器可以通过hash的方法找到对应的Class实例)
2、执行过程(图)
3、执行过程
步骤 | 说明 |
---|---|
1 | 调用Class.forName(className)方法,该方法会调用native的JVM实现,调用前该方法会确定准备好需要加载的类名以及ClassLoader,将其传递给native方法 |
2 | 进入到JVM实现后,首先会在SystemDictionary中根据类名和ClassLoader组成hash,进行查询,如果能够命中,则返回 |
3 | 如果加载到则返回 |
4 | 如果在SystemDictionary中无法命中,将会调用Java代码:ClassLoader.loadClass(类名),这一步将委派给Java代码,让传递的ClassLoader进行类型加载 |
5 | 以URLClassLoader为例,ClassLoader确定了类文件的字节流,但是该字节流如何按照规范生成Class对象,这个过程在Java代码中是没有体现的,其实也就是要求调用ClassLoader.defineClass(byte[])进行解析类型,该方法将会再次调用native方法,因为字节流对应Class对象的规范是定义在JVM实现中的 |
6 | 进入JVM实现,调用SystemDictionary的resolve_stream方法,接受byte[],使用ClassFileParser进行解析 |
7 | SystemDictionary::define_instance_class |
8 | 如果类型被加载了,将类名、ClassLoader和类型的实例引用添加到SystemDictionary中 |
9 | 返回 |
10 | 返回 |
11 | 从Java实现返回到Java代码的defineClass,返回Class对象 |
12 | 返回给loadClass(Classname)方法 |
13 | 返回给Java实现的SystemDictionary,因为在resolve_class中调用的ClassLoader.loadClass。这里会做出一个判断,如果加载Class的ClassLoader并非传递给resolve_class的ClassLoader,那么会将类名、传递给resolve_class的ClassLoader以及类型的实例引用添加到SystemDictionary中 |
14 | 返回给Class.forName类型实例 |
上述的过程比较复杂,但是简化理解一下它所做的工作,我们将SystemDictionary记作缓存,Class.forName或者说Java默认的类型加载过程是:
- 首先根据ClassLoader,我们称之为initialClassLoader和类名查找缓存,如果缓存有,则返回;
- 如果缓存没有,则调用ClassLoader.loadClass(类名),加载到类型后,保存<类名,真实加载类的ClassLoader,类型引用>到缓存,这里真实加载类的ClassLoader我们可以叫做defineClassLoader;
- 返回的类型在交给Java之前,将会判断defineClassLoader是否等于initialClassLoader,如果不等,则新增<类名,initialClassLoader,类型引用>到缓存。这里区分initialClassLoader和defineClassLoader的原因在于,调用initialClassLoader的loadClass,可能最终委派给其他的ClassLoader进行了加载。
六、ClassLoader.loadClass(String)
可以指定类加载器加载类,该方法的源码实现了双亲委托机制。
相对于Class.forName()该过程开始于第4步,没有前3步,该过程简单说就是:调用ClassLoader.loadClass(类名),加载到类型后,保存<类名,真实加载类的ClassLoader,类型引用>到缓存,这里真实加载类的ClassLoader我们可以叫做
defineClassLoader。也就是,调用ClassLoader.loadClass(类名)之后,并不一定会在缓存中生成一条<类名,ClassLoader,类型引用>的记录,但是一定会生成一条<类名,真实加载类的ClassLoader,类型引用>的记录。
该方法首先调用下面描述的方法,查询系统字典看是否能查到,如果系统字典里面没有然后走双亲委托制查询和加载。
七、ClassLoader.findLoadedClass(String className)
1、 该方法是protected final修饰的方法,也就是ClassLoader的子类可以内部使用,但是无法通过ClassLoader.findLoadedClass直接调用。
2、该方法从SystemDictionary中获取的,当调用ClassLoader.findLoadedClass(className)时,会到SystemDictionary中以className和ClassLoader为key,进行查询,如果命中,则返回类型实例。
八、J2EE的类加载器
九、几种常见的类加载错误
NoClassDefFoundError(类加载器找不到类)(可能的一种情况是:pom的scope为provided,但是web服务器又找不到时会报此错误)
NoSuchMethodError(jar包冲突,因为编译依赖的jar包和真正执行时加载的jar包不一样。本质是没有记载到正确的类)
ClassCastException(多余一个类被加载,ClassLoader定义了命名空间。)
LinkageError (还需要研究下)(多余一个类被加载)
十、maven的编译和打包以及程序的运行
1、maven可以进行包的依赖管理和编译以及打包
编译时首先编译依赖的子project,然后再编译父project,以此递推。
各个子project在编译和打包的过程中,所依赖的jar的优先级由自己的pom文件定义,而不是父pom。
2、在出现类加载的错误时,应该考虑下面两个过程来定位和解决问题
1、编译时期
2、运行时期