`
weiwu83
  • 浏览: 188525 次
  • 来自: ...
社区版块
存档分类
最新评论

Java类加载原理解析第一篇(下)

    博客分类:
  • JAVA
阅读更多
每个java开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到 了java技术体系中的类加载。Java的类加载机制是java技术体系中比较核心的部分,虽然和大部分开发人 员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对 理解java虚拟机的连接模型和java语言的动态性都有很大帮助。
由于关于java类加载的内容较多,所以打算分三篇文章简述一下:
第一篇:类加载,类加载器,双亲委派,自定义类加载器
第二篇:插件环境 Bundle类加载器
第三篇:线程上下文类加载器
作者:朱兴  
 
 
 
 3       程序动态扩展方式java
Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义 的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。 通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连 接它们并进行有选择的解析。
       运行时动态扩展java应用程序有如下两个 途径:
 
 
3.1    调用 java.lang.Class.forName(…)
这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发那个类加载器开始 加载任务。这里需要说明的是多参数版本的forName(…)方法:
 
 
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
 
 
这里的initialize参数是很重要的,可以觉得被加载同时是否完成初始化的工作( 说明: 单参数版本的forName方法默认是不完成初始化的).有些场景下,需要将 initialize设置为true来强制加载同时完成初始化,例如典型的就是利用 DriverManager进行JDBC驱动程序类注册的问题,因为每一个JDBC驱动程序类的静态初始化方法都用 DriverManager注册驱动程序,这样才能被应用程序使用,这就要求驱动程序类必须被初始化,而不单单被加 载.
 
 
3.2    用户自定义类加载器
 
通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类 加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否 被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍, 这里就简要叙述一下一般用户自定义类加载器的工作流程吧(可以结合后面问题解答一起看):
 
 
1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回; 否则转入步骤2
2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真个虚拟机中各种类加载器最 终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤 3
3、调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调 用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异 常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种 类不止一种)。
 
      (说明:这里说的自定义类加载器是指JDK 1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下)
 
 
4       常见问题分析:
4.1    由不同的类加载器 加载的指定类型还是相同的类型吗?
 
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配 类名包括包名和类名。但在JVM中一个类用其全名和一个 加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名 空间.我们可以用两个自定义类加载器去加载某自定义类型(注意,不要将自定义类型的字节码 放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两 个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写 两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后 再对比测试一下测试结果。
 
 
4.2    在代码中直接调用 Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对 应的jdk的代码:

  1. //java.lang.Class.java
  2. public static Class<?> forName(String className)throws ClassNotFoundException {
  3. return forName0(className, true, ClassLoader.getCallerClassLoader());
  4. }
  5.  
  6. //java.lang.ClassLoader.java
  7. // Returns the invoker's class loader, or null if none.
  8. static ClassLoader getCallerClassLoader() {
  9. // 获取调用类(caller)的类型
  10. Class caller = Reflection.getCallerClass(3);
  11. // This can be null if the VM is requesting it
  12. if (caller == null) {
  13. return null;
  14. }
  15. // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader
  16. return caller.getClassLoader0();
  17. }
  18.  
  19. //java.lang.Class.java
  20. //虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使 用此方法
  21. native ClassLoader getClassLoader0();

4.3    在编写自定义类加 载器时,如果没有设定父加载器,那么父加载器是?
前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我 们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自 java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:

  1. //摘自java.lang.ClassLoader.java
  2. protected ClassLoader() {
  3. SecurityManager security = System.getSecurityManager();
  4. if (security != null) {
  5. security.checkCreateClassLoader();
  6. }
  7. this.parent = getSystemClassLoader();
  8. initialized = true;
  9. }

我们再来看一下对应的getSystemClassLoader()方法的实现:

  1. private static synchronized void initSystemClassLoader() {
  2. //...
  3. sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
  4. scl = l.getClassLoader();
  5. //...
  6. }

我们可以写简单的测试代码来测试一下:
System.out.println(sun.misc.Launcher.getLauncher ().getClassLoader());
本机对应输出如下:
AppClassLoader@197d257">sun.misc.Launcher$AppClassLoader@197d257
所以,我们现在可以相信当自定义类加载器没有指定父类加载器 的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:
即时用户自定义类加载器不指定父类加载器,那么,同样可以加 载如下三个地方的类:
1.    <Java_Runtime_Home>/lib下的类
2.    < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类
3.    当前工程类 路径下或者由系统变量java.class.path指定位置中的类
 
 
 
4.4    在编写自定义类加 载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类 ,就肯定会加载失败吗?
 
 
JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载 器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可 以得出如下结论:
 
即时用户自定义类加载器不指定父类加载器,那么,同样可以加载到 <Java_Runtime_Home>/lib下的类,但此时就不能 够加载<Java_Runtime_Home>/lib/ext目录下的类 了。
 
   说明:问题 3和问题4 的推断结论是基于用户自定义的类加载器本身延续了 java.lang.ClassLoader.loadClass (…)默认委派逻辑,如果用户对这一默认委派逻 辑进行了改变,以上推断结论就不一定成立了,详见问题 5。
4.5    编写自定义类加载器时,一般有哪些注意点?
 
 
1.      一般尽量不要覆 写已有的loadClass)方法中的委 派逻辑
一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能 正常工作。在JVM规范和JDK文档中 (1.2或者以后版本中),都没有建议用户覆写loadClass(…) 方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写 findClass(…)逻辑。举一个例子来验证该问题:

  1. //用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)
  2. public class WrongClassLoader extends ClassLoader {
  3. public Class<?> loadClass(String name) throws ClassNotFoundException {
  4. return this.findClass(name);
  5. }
  6. protected Class<?> findClass(String name) throws ClassNotFoundException {
  7. //假设此处只是到工程以外的特定目录D:/library下去加载类
  8. 具体实现代码省略
  9. }
  10. }

    通过前面的分析我们已经知道,用户自定义类加 载器(WrongClassLoader)的默
       认的类加载器是系统类加载 器,但是现在问题4种的结论就不成立了。大家可以简
       单测试一下,现在 <Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工
       程类路径上的类都加载不上 了。

  1. //问题5测试代码一
  2. public class WrongClassLoaderTest {
  3. public static void main(String[] args) {
  4. try {
  5. WrongClassLoader loader = new WrongClassLoader();
  6. Class classLoaded = loader.loadClass("beans.Account");
  7. System.out.println(classLoaded.getName());
  8. System.out.println(classLoaded.getClassLoader());
  9. } catch (Exception e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. }

(说明:D:\classes\beans\Account.class物理存在的)
输出结果:
java.io.FileNotFoundException: D:\classes\java\lang\Object.class ( 系统找不到指定的路径。)
    at java.io.FileInputStream.open(Native Method)
    at java.io.FileInputStream.<init> (FileInputStream.java:106)
    at WrongClassLoader.findClass (WrongClassLoader.java:40)
    at WrongClassLoader.loadClass (WrongClassLoader.java:29)
    at java.lang.ClassLoader.loadClassInternal (ClassLoader.java:319)
    at java.lang.ClassLoader.defineClass1 (Native Method)
    at java.lang.ClassLoader.defineClass (ClassLoader.java:620)
    at java.lang.ClassLoader.defineClass (ClassLoader.java:400)
    at WrongClassLoader.findClass (WrongClassLoader.java:43)
    at WrongClassLoader.loadClass (WrongClassLoader.java:29)
    at WrongClassLoaderTest.main (WrongClassLoaderTest.java:27)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
    at java.lang.ClassLoader.defineClass1 (Native Method)
    at java.lang.ClassLoader.defineClass (ClassLoader.java:620)
    at java.lang.ClassLoader.defineClass (ClassLoader.java:400)
    at WrongClassLoader.findClass (WrongClassLoader.java:43)
    at WrongClassLoader.loadClass (WrongClassLoader.java:29)
    at WrongClassLoaderTest.main (WrongClassLoaderTest.java:27)
 
这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于 覆写loadClass(…)引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。

  1. //问题5测试二
  2. //用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)
  3. public class WrongClassLoader extends ClassLoader {
  4. protected Class<?> findClass(String name) throws ClassNotFoundException {
  5. //假设此处只是到工程以外的特定目录D:/library下去加载类
  6. 具体实现代码省略
  7. }
  8. }

将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输 出结果如下:
beans.Account
这说明,beans.Account加载成功,且是由自定义类加载器WrongClassLoader加载。
这其中的原因分析,我想这里就不必解释了,大家应该可以分析的出来了。
2.      2、正确设置父类加载器
通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最 重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都 可以随便举出例子了。
3.      3、保证findClassString )方法的逻辑正确性
事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码 内容。
 
一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统 类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以 获取到;
二是可以直接通过获取系统属性java.class.path 来查看当前类路径上的条目信息 , System.getProperty("java.class.path")
 
4.7    如何在运行时判断 标准扩展类加载器能加载哪些路径下的类?
方法之一:

  1. try {
  2. URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent ()).getURLs();
  3. for (int i = 0; i < extURLs.length; i++) {
  4. System.out.println(extURLs[i]);
  5. }
  6. } catch (Exception e) {//…}
  7. 本机对应输出如下:
  8. file:/D:/EOS4HW2/jdk1.5.0_09/jre/lib/ext/dnsns.jar
  9. file:/D:/EOS4HW2/jdk1.5.0_09/jre/lib/ext/localedata.jar
  10. file:/D:/EOS4HW2/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar
  11. file:/D:/EOS4HW2/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar

5       总结:
写这篇文章的初衷是通过分析JDK相关代码来验证一些加载规则,核心就是借助双亲 委派机制来分析一个加载请求处理的主要过程,所列举的几个简单的例子实际意义不大,因为遇到的情况 往往比例子情况复杂的多。
下一篇文章,我会重点分析一下Eclipse的插件类加载器,并分析一下插件环境下的 类加载和普通java应用场景中的类加载有什么不同,并会提供一个比较完整的类加载器。
插件类加载器就是一个由eclipse开发的一个用户自定义类加载器,所以分析时候用 到的一些基本的东西都是在这篇文章中涉及到的。
这篇文章写的时候时间比较紧,乱糟糟的,大家 见谅。中间参考了JVM规范、jdk文档和代码、《Inside Java Virtual Machine》一书等资料。
分享到:
评论

相关推荐

    java源码包---java 源码 大量 实例

    第一步:运行ServerData.java 启动服务器,然后服务器处于等待状态 第二步:运行LoginData.java 启动(客户端)登陆界面 输入用户名 ip为本机localhost 第三步:在登陆后的界面文本框输入文本,然后发送 可以同时启动...

    JAVA上百实例源码以及开源项目

    第一步:运行ServerData.java 启动服务器,然后服务器处于等待状态 第二步:运行LoginData.java 启动(客户端)登陆界面 输入用户名 ip为本机localhost 第三步:在登陆后的界面文本框输入文本,然后发送 可以同时启动...

    深入理解java7

    第二部分是7-13章,对JVM、Java源代码和字节代码操作、类加载器、对象生命周期、多线程、并发编程、泛型、安全等Java平台的核心技术进行了深入解析,掌握这部分内容有助于深入理解Java的底层原理;第三部分为第14章...

    JAVA上百实例源码以及开源项目源代码

    第一步:运行ServerData.java 启动服务器,然后服务器处于等待状态 第二步:运行LoginData.java 启动(客户端)登陆界面 输入用户名 ip为本机localhost 第三步:在登陆后的界面文本框输入文本,然后发送 可以同时启动...

    java源码包4

    第一步:运行ServerData.java 启动服务器,然后服务器处于等待状态 第二步:运行LoginData.java 启动(客户端)登陆界面 输入用户名 ip为本机localhost 第三步:在登陆后的界面文本框输入文本,然后发送 可以同时...

    java源码包3

    第一步:运行ServerData.java 启动服务器,然后服务器处于等待状态 第二步:运行LoginData.java 启动(客户端)登陆界面 输入用户名 ip为本机localhost 第三步:在登陆后的界面文本框输入文本,然后发送 可以同时...

    java源码包2

    第一步:运行ServerData.java 启动服务器,然后服务器处于等待状态 第二步:运行LoginData.java 启动(客户端)登陆界面 输入用户名 ip为本机localhost 第三步:在登陆后的界面文本框输入文本,然后发送 可以同时...

    成百上千个Java 源码DEMO 4(1-4是独立压缩包)

    Java编写的山寨QQ,多人聊天+用户在线 21个目标文件 摘要:JAVA源码,媒体网络,山寨QQ,Java聊天程序 Java编写的山寨QQ,多人聊天+用户在线,程序分服务端和客户端,典型C/S结构, 当用户发送第一次请求的时候,验证...

    [深入理解Java.7.核心技术与最佳实践]

    第二部分是7-13章,对JVM、Java源代码和字节代码操作、类加载器、对象生命周期、多线程、并发编程、泛型、安全等Java平台的核心技术进行了深入解析,掌握这部分内容有助于深入理解Java的底层原理;第三部分为第14章...

    java 面试题 总结

    Java Bean 是可复用的组件,对Java Bean并没有严格的规范,理论上讲,任何一个Java类都可以是一个Bean。但通常情况下,由于Java Bean是被容器所创建(如Tomcat)的,所以Java Bean应具有一个无参的构造器,另外,...

    高性能的javascript之加载顺序与执行原理篇

    前言 javascript在浏览器中的性能,可以认为是开发者所面临的最严重的可用性问题,今天,自己看完高性能的javascript的加载和执行这一章,聊聊...第一个js文件下载,要等到第一个js文件下载完全才会执行第二个js文件

    深入理解Java虚拟机视频教程(jvm性能调优+内存模型+虚拟机原理)视频教程

    第1节说在前面的话 [免费观看] 00:05:07分钟 | 第2节整个部分要讲的内容说明 [免费观看] 00:06:58分钟 | 第3节环境搭建以及jdk,jre,jvm的关系 [免费观看] 00:20:48分钟 | 第4节jvm初体验-内存溢出问题的分析与...

    成百上千个Java 源码DEMO 3(1-4是独立压缩包)

    Java编写的山寨QQ,多人聊天+用户在线 21个目标文件 摘要:JAVA源码,媒体网络,山寨QQ,Java聊天程序 Java编写的山寨QQ,多人聊天+用户在线,程序分服务端和客户端,典型C/S结构, 当用户发送第一次请求的时候,验证...

    Java语言基础下载

    第一章:Java开始 1 学习目标 1 Java历史 2 Java技术概述 3 Java技术的优点 3 Java虚拟机 4 类加载器 6 Windows环境变量 8 内容总结 13 独立实践 14 第二章: 面向对象概述 15 学习目标 15 面向对象(Object Oriented...

    解析javascript瀑布流原理实现图片滚动加载

    先科普下瀑布流吧 ...对容器中已有数据块元素进行第一次计算1 容器总宽度 2 列宽度 3 最小列数 ,得到列数后,用一个数组存放盒子所有高度,找出最小高度。之后根据序列号更新高度;看着有些拗口,实现起来就很简

    Java虚拟机

    这本书的内容是帮你全面了解java虚拟机,本书第1版两年内印刷近10次,98%以上的评论全部为5星级的好评,是整个Java图书领域公认的经典著作和超级畅销书,繁体版在台湾也十分受欢迎。第2版在第1版的基础上做了很大的...

    核心技术与最佳实践

    全书的主要内容可分为三大部分:第一部分是1 ~6 章,全面阐释Java 7 在语法、 JVM 、类库和API 等方面的所有重要新功能和新特性,掌握这部分内容有助于大幅度提升编 码效率和提高代码质量;第二部分是7 ~1 3 章...

    Java开发技术大全 电子版

    第1篇Java基础知识入门. 第1章Java的开发运行环境2 1.1Java的运行环境与虚拟机2 1.2Java的开发环境4 1.2.1JDK的安装4 1.2.2如何设置系统环境变量6 1.2.3编译命令的使用8 1.2.4解释执行命令的使用10 1.2.5...

    Java面试宝典-经典

    1. 判断第二个日期比第一个日期大 82 2. 用table显示n条记录,每3行换一次颜色,即1,2,3用红色字体,4,5,6用绿色字体,7,8,9用红颜色字体。 83 3、HTML 的 form 提交之前如何验证数值文本框的内容全部为数字? ...

Global site tag (gtag.js) - Google Analytics