Java类加载机制解析

来源:互联网 发布:小说有声阅读软件 编辑:程序博客网 时间:2024/06/10 17:52

深入理解Java类加载器(1):Java类加载原理解析

1 基本信息

  每个开发人员对Java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然和大部分开发人员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对理解java虚拟机的连接模型和java语言的动态性都有很大帮助。

2 Java虚拟机类加载器结构简述

2.1 JVM三种预定义类型类加载器

  我们首先看一下JVM预定义的三种类型类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:

  启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

  系统(System)类加载器:系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

  除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个将在后面单独介绍。

 

2.2 类加载双亲委派机制介绍和分析

 

       在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。

                                                                              

图一 标准扩展类加载器继承层次图


图二系统类加载器继承层次图

通过图一和图二我们可以看出,类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中几个最重要的方法:

[java] view plain copy

 

1.  //加载指定名称(包括包名)的二进制类型,供用户调用的接口  

2. public Class<?> loadClass(String name) throws ClassNotFoundException{ … }  

3.    

4. //加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用  

5.  protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }  

6.   

7.  //findClass方法一般被loadClass方法调用去加载指定名称类,供继承用  

8. protected Class<?> findClass(String name) throws ClassNotFoundException { … }  

9.    

10.//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承  

11. //(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)  

12.protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }  

  通过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader)和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass)方法。既然这样,我们就可以通过分析java.lang.ClassLoader中的loadClass(String name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:

[java] view plain copy

 

1.  public Class<?> loadClass(String name) throws ClassNotFoundException {  

2.     return loadClass(name, false);  

3.  }  

4.   

5.  protected synchronized Class<?> loadClass(String name, boolean resolve)  

6.         throws ClassNotFoundException {  

7.    

8.     // 首先判断该类型是否已经被加载  

9.      Class c = findLoadedClass(name);  

10.    if (c == null) {  

11.         //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载  

12.        try {  

13.             if (parent != null) {  

14.                //如果存在父类加载器,就委派给父类加载器加载  

15.                 c = parent.loadClass(name, false);  

16.            } else {  

17.                 //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,  

18.                //通过调用本地方法native findBootstrapClass0(String name)  

19.                 c = findBootstrapClass0(name);  

20.            }  

21.         } catch (ClassNotFoundException e) {  

22.            // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能  

23.             c = findClass(name);  

24.        }  

25.     }  

26.    if (resolve) {  

27.         resolveClass(c);  

28.    }  

29.     return c;  

30.}  

  通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:


图三 类加载器默认委派关系图

  上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

[java] view plain copy

 

1.  public class LoaderTest {  

2.   

3.      public static void main(String[] args) {  

4.         try {  

5.              System.out.println(ClassLoader.getSystemClassLoader());  

6.             System.out.println(ClassLoader.getSystemClassLoader().getParent());  

7.              System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());  

8.         } catch (Exception e) {  

9.              e.printStackTrace();  

10.        }  

11.     }  

12.}  

  说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器。
  代码输出如下:

[plain] view plain copy

 

1.  sun.misc.Launcher$AppClassLoader@6d06d69c  

2. sun.misc.Launcher$ExtClassLoader@70dea4e  

3.  null  

  通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null我们还是借助于代码分析一下。

  我们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:

[java] view plain copy

 

1.  protected ClassLoader() {  

2.     SecurityManager security = System.getSecurityManager();  

3.      if (security != null) {  

4.         security.checkCreateClassLoader();  

5.      }  

6.     //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器  

7.      this.parent = getSystemClassLoader();  

8.     initialized = true;  

9.  }  

10.  

11. protected ClassLoader(ClassLoader parent) {  

12.    SecurityManager security = System.getSecurityManager();  

13.     if (security != null) {  

14.        security.checkCreateClassLoader();  

15.     }  

16.    //强制设置父类加载器  

17.     this.parent = parent;  

18.    initialized = true;  

19. }  

  我们再看一下ClassLoader抽象类中parent成员的声明:

[java] view plain copy

 

1.  // The parent class loader for delegation  

2. private ClassLoader parent;  

  声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:
  1. 系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
  2. 扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
  现在我们可能会有这样的疑问:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?

图四 标准扩展类加载器和系统类加载器成员大纲视图



图五 扩展类加载器和系统类加载器公共父类成员大纲视图


  通过图四和图五可以看出,标准扩展类加载器和系统类加载器及其父类(java.NET.URLClassLoader和java.security.SecureClassLoader)都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass)方法。有关java.lang.ClassLoader中默认的加载委派规则前面已经分析过,如果父加载器为null,则会调用本地方法进行启动类加载尝试。所以,图三中,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。(在后面的用户自定义类加载器部分,还会做更深入的分析)。

 

2.3 类加载双亲委派示例

 

  以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子。首先在IDE中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

[java] view plain copy

 

1.  package classloader.test.bean;  

2.   

3.  public class TestBean {  

4.       

5.      public TestBean() { }  

6. }  

  在现有当前工程中另外建立一测试类(ClassLoaderTest.java)内容如下:

  测试一:

[java] view plain copy

 

1.  package classloader.test.bean;  

2.   

3.  public class ClassLoaderTest {  

4.   

5.      public static void main(String[] args) {  

6.         try {  

7.              //查看当前系统类路径中包含的路径条目  

8.             System.out.println(System.getProperty("java.class.path"));  

9.              //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean  

10.            Class typeLoaded = Class.forName("classloader.test.bean.TestBean");  

11.             //查看被加载的TestBean类型是被那个类加载器加载的  

12.            System.out.println(typeLoaded.getClassLoader());  

13.         } catch (Exception e) {  

14.            e.printStackTrace();  

15.         }  

16.    }  

17. }  

  对应的输出如下:

[plain] view plain copy

 

1.  C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  

2. sun.misc.Launcher$AppClassLoader@73d16e93  

  说明:当前类路径默认的含有的一个条目就是工程的输出目录。
  测试二:

  将当前工程输出目录下的TestBean.class打包进test.jar剪贴到<Java_Runtime_Home>/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:

[plain] view plain copy

 

1.  C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  

2. sun.misc.Launcher$ExtClassLoader@15db9742  

  对比测试一和测试二,我们明显可以验证前面说的双亲委派机制,系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。
  测试三:
  将test.jar拷贝一份到<Java_Runtime_Home>/lib下,运行测试代码,输出如下:

[plain] view plain copy

 

1.  C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  

2. sun.misc.Launcher$ExtClassLoader@15db9742  

  测试三和测试二输出结果一致。那就是说,放置到<Java_Runtime_Home>/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载<Java_Runtime_Home>/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<Java_Runtime_Home>/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

 

3 java程序动态扩展方式


  Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。
  运行时动态扩展java应用程序有如下两个途径:

 

3.1 调用java.lang.Class.forName(…)加载类

 

  这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发哪个类加载器开始加载任务。这里需要说明的是多参数版本的forName(…)方法:

[java] view plain copy

 

1.  public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException  

  这里的initialize参数是很重要的。它表示在加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是完成初始化的)。有些场景下需要将initialize设置为true来强制加载同时完成初始化。例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题。因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用。这就要求驱动程序类必须被初始化,而不单单被加载。Class.forName的一个很常见的用法就是在加载数据库驱动的时候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载 Apache Derby 数据库的驱动。

 

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(Stringname)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

[java] view plain copy

 

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.//虚拟机本地实现,获取当前类的类加载器,前面介绍的ClassgetClassLoader()也使用此方法  

21. native ClassLoader getClassLoader0();  

 

4.3 在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?

 

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

[java] view plain copy

 

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()方法的实现:

[java] view plain copy

 

1.  private static synchronized void initSystemClassLoader() {  

2.     //...  

3.      sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  

4.     scl = l.getClassLoader();  

5.      //...  

6. }  

  我们可以写简单的测试代码来测试一下:

[java] view plain copy

 

1.  System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());  

  本机对应输出如下:

[plain] view plain copy

 

1.  sun.misc.Launcher$AppClassLoader@73d16e93  

  所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:
  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(…)逻辑。举一个例子来验证该问题:

[java] view plain copy

 

1.  //用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)  

2. public class WrongClassLoader extends ClassLoader {  

3.    

4.     public Class<?> loadClass(String name) throws ClassNotFoundException {  

5.          return this.findClass(name);  

6.     }  

7.    

8.     protected Class<?> findClass(String name) throws ClassNotFoundException {  

9.          // 假设此处只是到工程以外的特定目录D:\library下去加载类  

10.        // 具体实现代码省略  

11.     }  

12.}  

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

[java] view plain copy

 

1.  //问题5测试代码一  

2. public class WrongClassLoaderTest {  

3.    

4.     public static void main(String[] args) {  

5.          try {  

6.             WrongClassLoader loader = new WrongClassLoader();  

7.              Class classLoaded = loader.loadClass("beans.Account");  

8.             System.out.println(classLoaded.getName());  

9.              System.out.println(classLoaded.getClassLoader());  

10.        } catch (Exception e) {  

11.             e.printStackTrace();  

12.        }  

13.     }  

14.}  

  这里D:"classes"beans"Account.class是物理存在的。输出结果:

[plain] view plain copy

 

1.  java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)  

2.     at java.io.FileInputStream.open(Native Method)  

3.      at java.io.FileInputStream.<init>(FileInputStream.java:106)  

4.     at WrongClassLoader.findClass(WrongClassLoader.java:40)  

5.      at WrongClassLoader.loadClass(WrongClassLoader.java:29)  

6.     at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)  

7.      at java.lang.ClassLoader.defineClass1(Native Method)  

8.     at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  

9.      at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  

10.    at WrongClassLoader.findClass(WrongClassLoader.java:43)  

11.     at WrongClassLoader.loadClass(WrongClassLoader.java:29)  

12.    at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  

13. Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object  

14.    at java.lang.ClassLoader.defineClass1(Native Method)  

15.     at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  

16.    at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  

17.     at WrongClassLoader.findClass(WrongClassLoader.java:43)  

18.    at WrongClassLoader.loadClass(WrongClassLoader.java:29)  

19.     at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  

  这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。

[java] view plain copy

 

1.  //问题5测试二  

2. //用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)  

3.  public class WrongClassLoader extends ClassLoader {  

4.   

5.      protected Class<?> findClass(String name) throws ClassNotFoundException {  

6.         //假设此处只是到工程以外的特定目录D:\library下去加载类  

7.          //具体实现代码省略  

8.     }  

9.  }  

  将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:

[plain] view plain copy

 

1.  beans.Account  

2. WrongClassLoader@1c78e57  

  2、正确设置父类加载器
  通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。
  3、保证findClass(String name)方法的逻辑正确性
  事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

 

4.6 如何在运行时判断系统类加载器能加载哪些路径下的类?

 

  一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。
  二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty("java.class.path")。

 

4.7 如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

 

  方法之一:

[java] view plain copy

 

1.  import java.net.URL;  

2. import java.net.URLClassLoader;  

3.    

4. public class ClassLoaderTest {  

5.    

6.     /** 

7.       * @param args the command line arguments 

8.      */  

9.      public static void main(String[] args) {  

10.        try {  

11.             URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();  

12.            for (int i = 0; i < extURLs.length; i++) {  

13.                 System.out.println(extURLs[i]);  

14.            }  

15.         } catch (Exception e) {  

16.            //…  

17.         }  

18.    }  

19. }  

  本机对应输出如下:

[plain] view plain copy

 

1.  file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar  

2. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar  

3.  file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar  

4. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar  

5.  file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar  

6. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar  

7.  file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar  

8. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar  

9.  file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar  

10.file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar  

11. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar  

12.file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar  

 

5 开发自己的类加载器

 

  在前面介绍类加载器的代理委派模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
  方法 loadClass()抛出的是java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是java.lang.NoClassDefFoundError异常。
  类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

  在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

 

5.1 文件系统类加载器

 

  第一个类加载器用来加载存储在文件系统上的Java字节代码。完整的实现如下所示。

[java] view plain copy

 

1.  package classloader;  

2.   

3.  import java.io.ByteArrayOutputStream;  

4. import java.io.File;  

5.  import java.io.FileInputStream;  

6. import java.io.IOException;  

7.  import java.io.InputStream;  

8.   

9.  // 文件系统类加载器  

10.public class FileSystemClassLoader extends ClassLoader {  

11.   

12.    private String rootDir;  

13.   

14.    public FileSystemClassLoader(String rootDir) {  

15.         this.rootDir = rootDir;  

16.    }  

17.   

18.    // 获取类的字节码  

19.     @Override  

20.    protected Class<?> findClass(String name) throws ClassNotFoundException {  

21.         byte[] classData = getClassData(name);  // 获取类的字节数组  

22.        if (classData == null) {  

23.             throw new ClassNotFoundException();  

24.        } else {  

25.             return defineClass(name, classData, 0, classData.length);  

26.        }  

27.     }  

28.  

29.     private byte[] getClassData(String className) {  

30.        // 读取类文件的字节  

31.         String path = classNameToPath(className);  

32.        try {  

33.             InputStream ins = new FileInputStream(path);  

34.            ByteArrayOutputStream baos = new ByteArrayOutputStream();  

35.             int bufferSize = 4096;  

36.            byte[] buffer = new byte[bufferSize];  

37.             int bytesNumRead = 0;  

38.            // 读取类文件的字节码  

39.             while ((bytesNumRead = ins.read(buffer)) != -1) {  

40.                baos.write(buffer, 0, bytesNumRead);  

41.             }  

42.            return baos.toByteArray();  

43.         } catch (IOException e) {  

44.            e.printStackTrace();  

45.         }  

46.        return null;  

47.     }  

48.  

49.     private String classNameToPath(String className) {  

50.        // 得到类文件的完全路径  

51.         return rootDir + File.separatorChar  

52.                + className.replace('.', File.separatorChar) + ".class";  

53.     }  

54.}  

  如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写loadClass()方法,而是覆写findClass()方法。
  类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

  加载本地文件系统上的类,示例如下:

[java] view plain copy

 

1.  package com.example;  

2.   

3.  public class Sample {  

4.   

5.      private Sample instance;  

6.   

7.      public void setSample(Object instance) {  

8.         System.out.println(instance.toString());  

9.          this.instance = (Sample) instance;  

10.    }  

11. }  

[java] view plain copy

 

1.  package classloader;  

2.   

3.  import java.lang.reflect.Method;  

4.   

5.  public class ClassIdentity {  

6.   

7.      public static void main(String[] args) {  

8.         new ClassIdentity().testClassIdentity();  

9.      }  

10.  

11.     public void testClassIdentity() {  

12.        String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";  

13.         FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);  

14.        FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);  

15.         String className = "com.example.Sample";  

16.        try {   

17.             Class<?> class1 = fscl1.loadClass(className);  // 加载Sample  

18.            Object obj1 = class1.newInstance();  // 创建对象  

19.             Class<?> class2 = fscl2.loadClass(className);  

20.            Object obj2 = class2.newInstance();  

21.             Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);  

22.            setSampleMethod.invoke(obj1, obj2);  

23.         } catch (Exception e) {  

24.            e.printStackTrace();  

25.         }  

26.    }  

27. }  

  运行输出:com.example.Sample@7852e922

 

5.2 网络类加载器


  下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
  类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。

[java] view plain copy

 

1.  package classloader;  

2.   

3.  import java.io.ByteArrayOutputStream;  

4. import java.io.InputStream;  

5.  import java.net.URL;  

6.   

7.  public class NetworkClassLoader extends ClassLoader {  

8.   

9.      private String rootUrl;  

10.  

11.     public NetworkClassLoader(String rootUrl) {  

12.        // 指定URL  

13.         this.rootUrl = rootUrl;  

14.    }  

15.   

16.    // 获取类的字节码  

17.     @Override  

18.    protected Class<?> findClass(String name) throws ClassNotFoundException {  

19.         byte[] classData = getClassData(name);  

20.        if (classData == null) {  

21.             throw new ClassNotFoundException();  

22.        } else {  

23.             return defineClass(name, classData, 0, classData.length);  

24.        }  

25.     }  

26.  

27.     private byte[] getClassData(String className) {  

28.        // 从网络上读取的类的字节  

29.         String path = classNameToPath(className);  

30.        try {  

31.             URL url = new URL(path);  

32.            InputStream ins = url.openStream();  

33.             ByteArrayOutputStream baos = new ByteArrayOutputStream();  

34.            int bufferSize = 4096;  

35.             byte[] buffer = new byte[bufferSize];  

36.            int bytesNumRead = 0;  

37.             // 读取类文件的字节  

38.            while ((bytesNumRead = ins.read(buffer)) != -1) {  

39.                 baos.write(buffer, 0, bytesNumRead);  

40.            }  

41.             return baos.toByteArray();  

42.        } catch (Exception e) {  

43.             e.printStackTrace();  

44.        }  

45.         return null;  

46.    }  

47.   

48.    private String classNameToPath(String className) {  

49.         // 得到类文件的URL  

50.        return rootUrl + "/"  

51.                 + className.replace('.''/') + ".class";  

52.    }  

53. }  

  在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:

  客户端接口:

[java] view plain copy

 

1.  package classloader;  

2.   

3.  public interface Versioned {  

4.   

5.      String getVersion();  

6. }  

[java] view plain copy

 

1.  package classloader;  

2.   

3.  public interface ICalculator extends Versioned {  

4.   

5.      String calculate(String expression);  

6. }  

  网络上的不同版本的类:

[java] view plain copy

 

1.  package com.example;  

2.   

3.  import classloader.ICalculator;  

4.   

5.  public class CalculatorBasic implements ICalculator {  

6.   

7.      @Override  

8.     public String calculate(String expression) {  

9.          return expression;  

10.    }  

11.   

12.    @Override  

13.     public String getVersion() {  

14.        return "1.0";  

15.     }  

16.  

17. }  

[java] view plain copy

 

1.  package com.example;  

2.   

3.  import classloader.ICalculator;  

4.   

5.  public class CalculatorAdvanced implements ICalculator {  

6.   

7.      @Override  

8.     public String calculate(String expression) {  

9.          return "Result is " + expression;  

10.    }  

11.   

12.    @Override  

13.     public String getVersion() {  

14.        return "2.0";  

15.     }  

16.  

17. }  

  在客户端加载网络上的类的过程:

[java] view plain copy

 

1.  package classloader;  

2.   

3.  public class CalculatorTest {  

4.   

5.      public static void main(String[] args) {  

6.         String url = "http://localhost:8080/ClassloaderTest/classes";  

7.          NetworkClassLoader ncl = new NetworkClassLoader(url);  

8.         String basicClassName = "com.example.CalculatorBasic";  

9.          String advancedClassName = "com.example.CalculatorAdvanced";  

10.        try {  

11.             Class<?> clazz = ncl.loadClass(basicClassName);  // 加载一个版本的类  

12.            ICalculator calculator = (ICalculator) clazz.newInstance();  // 创建对象  

13.             System.out.println(calculator.getVersion());  

14.            clazz = ncl.loadClass(advancedClassName);  // 加载另一个版本的类  

15.             calculator = (ICalculator) clazz.newInstance();  

16.            System.out.println(calculator.getVersion());  

17.         } catch (Exception e) {  

18.            e.printStackTrace();  

19.         }  

20.    }  

21.   

22.}  

 

 

 

 

Java类加载器加载类顺序

 java是一门解释执行的语言,由开发人员编写好的java源文件先编译成字节码文件.class形式,然后由java虚拟机(JVM)解释执行,.class字节码文件本身是平台无关的,但是jvm却不是,为了实现所谓的一次编译,到处执行,sun提供了各个平台的JVM实现--也就是说jvm不是跨平台的,编译好的字节码文件被放在不同的操作系统平台上的jvm所解释执行,这个章节主要解释一下JVM装载类的机制

  1.ClassLoader是什么?

  一个类如果要被JVM所调度执行,必须先把这个类加载到JVM内存里,java.lang下有个很重要的类ClassLoader,这个类主要就是用来把指定名称(指定路径下)的类加载到JVM中

  2.ClassLoader的分类

  主要分4类,见下图橙色部分

  JVM类加载器:这个模式会加载JAVA_HOME/lib下的jar包

  扩展类加载器:会加载JAVA_HOME/lib/ext下的jar包

  系统类加载器:这个会去加载指定了classpath参数指定的jar文件

  用户自定义类加载器:sun提供的ClassLoader是可以被继承的,允许用户自己实现类加载器

  类加载器的加载顺序如图所示:


  3.类加载顺序

  JVM并不是把所有的类一次性全部加载到JVM中的,也不是每次用到一个类的时候都去查找,对于JVM级别的类加载器在启动时就会把默认的JAVA_HOME/lib里的class文件加载到JVM中,因为这些是系统常用的类,对于其他的第三方类,则采用用到时就去找,找到了就缓存起来的,下次再用到这个类的时候就可以直接用缓存起来的类对象了,ClassLoader之间也是有父子关系的,每个ClassLoader都有一个父ClassLoader,在加载类时ClassLoader与其父ClassLoader的查找顺序如下图所示

 

 

 

 

1 概述

虚拟机将class文件加载进内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程,为Java类加载机制。Java加载机制的生命周期:加载验证准备解析初始化使用卸载 7个阶段。

Java语言中,类的加载,连接和初始化都是在程序运行期间完成的。

2 初始化

有且仅有以下情况下,若类没有初始化过,必须初始化:

1.      遇到new getstatic putstaticinvokestatic4个字节码时,若没有初始化则必须初始化。 
其中,new 用于实例化对象;getstatic/putstatic读取/设置一个非final的类静态字段(未加载进常量池的字段);invokestatic用于调用一个类的静态方法。

2.      使用java.lang.reflect包的方法对类进行反射调用。

3.      初始化一个类的时候,发现其父类未被初始化。则先初始化父类。对接口而言,初始化接口的时候并不会先初始化父类接口,而是到真正使用到父类接口的时候才会初始化父类接口

4.       虚拟机启动的时候,会初始化main方法所在的类。

5.      JDK1.7以后版本,使用java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,且这些句柄所对应的类未被初始化时。

初始化部分,有以下需要注意的部分:

  1. 对于静态字段的调用,只有直接定义这个字段的类才会被初始化,因此通过子类调用父类的静态字段,只会初始化父类,不会初始化子类的。
  2. new 一个(T类型的)数组的时候,并不会触发T类型的初始化,而是触发 [T 类型的初始化。这个类型是由虚拟机生成的,直接继承java.lang.Object的子类,创建动作由字节码newarray触发。此类封装了数组访问方法
  3. A访问类B中定义的常量的时候,也不会造成B类的初始化。因为在编译阶段通过常量传播优化,已经将B类中的常量的值,存储到了A类的常量池中。也就是说这两个类在编译成class文件后已经没有关系了。

接口特殊部分

  1. 接口也有初始化过程,和类的初始化类似。也会生成方法。
  2. 接口初始化和类唯一不同出,见类初始化第三条:对接口而言,初始化接口的时候并不会先初始化父类接口,而是到真正使用到父类接口的时候才会初始化父类接口

3 类加载

类加载部分分为:加载、连接(验证、准备、解析)、初始化五部分

3.1 加载

在加载阶段,虚拟机需要完成以下事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这个类的各个数据的访问入口。

注:数组类并不由类加载器加载,而是直接由虚拟机创建的。数组类的创建遵循以下规则:

  1. 如果数组的组件类型(即数组去掉一个维度的类型)是引用类型,将递归采用加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识,以达到一个类必须与类加载器一起确定唯一性
  2. 若数组的组件类型不是引用类型,Java虚拟机将会把数组标记为与引导类加载器关联
  3. 数组类的可见性与它的组件类型的可见性一致。若组件类型不是引用类型,则其可见性默认是public

加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,然后在内存中初始化一个java.lang.Class类的对象(对于HotSpot虚拟机,这个对象存储在方法区中),这个对象作为程序访问方法区这些类型数据的外部接口。加载阶段和连接阶段可能是交叉进行的。

3.2 验证

验证是连接阶段的第一步。用于保证Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害到虚拟机自身的安全。验证分4个阶段:

1.      文件格式验证,主要目的是保证输入的字节流能正确地解析并存储在方法区中,格式符合描述一个Java类型信息的要求。验证内容是:字节流是否符合class文件格式的规范,且能被当前虚拟机处理等。比如

是否以魔数0xCAFEBABE开头

主次版本号是否在当前虚拟机处理范围之类

常量池的常量中是否有不被支持的常量类型(检查常量tag标志)

指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据

Class文件中各种部分以及文件本身是否有被删除的或者附加的其他信息

…………

2.      元数据验证:语义分析,保证其描述的信息符合Java语言规范的要求:

这个类是否有父类,除了Java.lang.Object,所有类都有父类。

这个类的父类是否继承了final修饰的类等不被允许继承的类。

若这个类不是抽象类,他是否实现了父类或接口中要求实现的所有方法。

类中的字段方法是否与父类产生矛盾:覆盖了父类的final字段/不合规的重载等。

……

3.       字节码验证

主要目的是通过数据流和控制流分析,确定程序的语义是否合法、符合逻辑。这里主要验证的是方法体。Java6以后,通过StackMapTable记录了方法体中所有基本块(按照控制流程拆分的代码块)开始时本地变量表和操作数栈应有的状态。故只需要检查这里的记录的合法性即可(此部分可参考Java虚拟机之类文件结构

保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。

保证跳转指令不会跳转到方法体以外的字节码指令上

保证方法体中的类型转换是有效的。

……

4.       符号引用验证

此验证发生在虚拟机将符号引用转化为直接引用的时候。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。符号引用验证的目标是保证解析动作能正常的执行。失败会抛出Java.lang.IncompatibleClassChangeError的子类(比如Java.lang.IllegalAccessError/java.lang.NoSuchFieldError/java.lang.NoSuchMethodError等)

符号引用中通过字符串描述的全限定名能否找到对应的类

在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

符号引用中的类、字段、方法访问限定符是否可以被当前类访问

……

3.3 准备

准备阶段是正式为类变量分配内存设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配

注意:

  1. 这时候进行内存分配的仅包括类变量(static修饰的变量),不包括实例变量实例变量将在类实例化的时候随对象一起分配在Java堆中
  2. 初始值是数据类型的0值,比如int的0;long的0l;short的(short)0;char的\u0000;byte的(byte)0;boolean的false;double的0.0d;reference的null等。
  3. 若类字段属性表中存在ConstantValue属性,且类字段包含ACC_FINAL属性。(比如属性被final static修饰),则该属性会在此时被初始化为ContentValue中的值。其他情况则是在中初始化实际值。 
    比如:public static final int a=10;在这里a将被设置为10,而不是0 
    public int b = 10 在这里被初始化为0<clinit>中才被设置为10

3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

首先分析下这里两个概念:

1.       符号引用:以一组符号来描述所引用的目标。符号引用可以是任何类型的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定以及加载到内存中。具体查询Java虚拟机之类文件结构

2.       直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局是相关的。若存在直接引用,则其目标一定存在内存中。

虚拟机要求在执行了 anewarraycheckastgetfieldgetstaticinstanceof 
invokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtual 
ldcldc_w,multianewarray, new, putfield,putstatic这十六个用于操作数符号引用的字节码指令之前,先对他们所使用的符号引用进行解析。

对于invokedynamic,其由于是专门设计用来支持动态语言特性的,故其解析必须发生在程序实际运行到这条指令的时候。

解析动作主要是针对类和接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行。对应了常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_infoCONSTANT_MethodType_infoCONSTANT_MethodHandle_info,CONSTANT_InvokeDynamic_info7种常量类型。

以下详细分析其解析过程:

3.4.1.类和接口的解析

虚拟机在当前代码所在类D,将一个未解析过的符号引用N,解析为类或接口C的步骤如下:

  1. 若C不是一个数组类型,则虚拟机会将N的全限定名传递给D的类加载器去加载C这个类,具体加载和验证过程前面已经分析。
  2. 若C是一个数组类型,且数组的元素的类型为对象,例如N的描述符为[Ljava/lang/Integer,则会按照1中规则加载数组元素类型;接着由虚拟机生成一个代表此数组维度和元素的数组对象。
  3. 此时C已经在虚拟机中成为一个有效的类或者接口了,将进行符号引用验证,确定D具备对C的访问权限,若不具备访问权限则抛出java.lang.IllegalAccessError

3.4.2.字段的解析

字段解析首先会对字段表内class_index项索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。虚拟机将会按规范进行此类C的后续字段搜索:

  1. 若C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  2. 否则,若C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,若接口中包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。
  3. 否则,如果C不是java.lang.Object,则会按照继承关系从下往上递归搜索其父类,如果在父类包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。
  4. 否则,抛出java.lang.NoSuchFieldError

若查找过程成功返回了引用,将会对这个字段进行权限验证,若不具备访问权限则抛出java.lang.IllegalAccessError

3.4.3. 类方法的解析

类方法的解析首先会对类方法表内class_index项索引的CONSTANT_Class_info符号引用进行解析,也就是方法所属的类或接口的符号引用。虚拟机将会按规范进行此类C的后续类方法搜索:

  1. 类方法和接口方法符号引用的常量类型定义是分开的,若在类方法表中发现class_index中索引的C是个接口,则直接抛出java.lang.IncompatibleClassChangeError.
  2. 若第一步成功,则在类C中查找是否有简单名称和描述符都与目标相匹配的方法,有就返回这个方法的引用,查找结束
  3. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  4. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,存在则说明类C是一个抽象类,此时查找结束,抛出java.lang.AbstractMethodError
  5. 否则,宣告查找方法失败,抛出java.lang.IllegalAccessError

3.4.4. 接口方法的解析

接口方法的解析首先会对接口方法表内class_index项索引的CONSTANT_Class_info符号引用进行解析,也就是方法所属的类或接口的符号引用。虚拟机将会按规范进行此接口C的后续接口方法搜索:

  1. 若在接口方法表中发现class_index中索引的C是个类而不是接口,则直接抛出java.lang.IncompatibleClassChangeError.(这里与类方法解析正好相反)
  2. 若第一步成功,则在接口C中查找是否有简单名称和描述符都与目标匹配的方法,若有则返回此方法的直接引用,查找结束
  3. 否则,在接口C的父接口中递归查找,直到Java.lang.Object(包含)为止,查找是否有简单名称和描述符都与目标匹配的方法,若有则返回此方法的直接引用,查找结束。
  4. 否则,查找失败。抛出java.lang.NoSuchMethodError
  5. 接口中方法都是public的,所以不存在权限问题,故这里不需要权限验证。

3.4.5 MethodType解析

3.4.6 MethodHandle解析

3.4.7 InvokeDynamic解析

3.5 初始化

类初始化是类加载过程的最后一步。除了加载阶段可以使用用户自定义加载器加载外,其余阶段都是虚拟机主导的。到了初始化阶段才是真正执行Java代码:执行方法,完成类初始化。

使用注意:

1.      是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,其中编译器收集的顺序是基于源码中顺序的。静态语句块中只能访问到定义在其前面的变量定义在其后面的静态变量可以在静态语句块中赋值,但不能访问

2. publicclass TestClinit {
3. static {
4.     i = 10; //可以赋值,不报错
5.     System.out.println(i);
6.     //不能访问 Cannot reference a field before it is defined
7. }
8. staticint i;
}

9.      方法与类实例的构造函数方法不同,不需要显式的调用父类的构造器,虚拟机会保证在子类的执行前,父类的方法已经执行完毕。因此虚拟机中第一个被执行的肯定是java.lang.Object.

10.  由于父类的先执行,故父类的静态语句块要优先于子类的语句块先执行。例如,以下的例子中最终输出的将使1.(这里若是把静态块放在变量A定义的后面,则会输出2,说明静态区域的执行顺序只和源码顺序有关系。)

11.   staticclassParent{
12.      static{
13.          A = 2;
14.      }
15.      publicstaticint A =1;
16.  }
17.   
18.  staticclassSubextendsParent{
19.      publicstaticint B = A;
20.  }
21.  publicstaticvoid main(String[] args) {
22.      System.out.println(Sub.B);
}

23.  方法对于类和接口并不是必须的,若类中无静态块和静态变量赋值操作,则编译器可以不生成方法。

  1. 接口中存在变量初始化赋值操作,故也需要方法,接口和类的区别是:执行接口的方法并不要求先执行父接口的,只有当父接口中定义的变量使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的方法
  2. 虚拟机会保证一个类的方法在多线程环境中被正确的加锁和同步,若多个线程同时去初始化一个类时,只有一个线程能去执行初始化方法,其他线程阻塞等待,直到该执行线程结束。唤醒其他线程,此时,其他线程不会再次进入这个类的方法,因为同一个类加载器下,一个类只会被初始化一次

4 类加载器

类加载器的作用是通过一个类的全限定名来获取描述此类的二进制字节流。

对于任何一个类,都需要由加载他的类加载器和这个类本身一同确定其在虚拟机中的唯一性,每个类加载器都有一个独立的类名称空间。也就是说比较两个类相等是基于同一个加载器的情况下才有意义。否则一定不等。

相等是指类的Class对象的equal方法/isAssignableFrom方法/isInstance方法/以及instanceof关系判定等。

4.1 类加载器分类

对于Java虚拟机而言,只存在两类加载器:启动类加载器(Bootstrap Classloader,是虚拟机的一部分;另一个就是其他加载器,这些加载器继承了抽象类java.lang.ClassLoader

对于Java开发而言,有三种类型的加载器:

  1. 启动类加载器(Bootstrap ClassLoader),这个类加载器负责将存放在$JAVA_HOME/lib目录中或被-Xbootclasspath参数指定路径中的,虚拟机识别的(按照文件名)的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,若需要吧加载请求委派给引导类加载器,直接使用null代替即可。
  2. 扩展类加载器(Extension ClassLoader),这个加载器是由sun.misc.Launcher$ExtClassLoader实现,它负责加载$JAVA_HOME/lib/ext目录下中的,或者被java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用该加载器。
  3. 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现,这个类加载器是ClassLoader类的getSystemClassLoader()方法的返回值,故也称为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库。程序默认的类加载器,开发人员可以直接使用。

4.2 双亲委派模型

若一个类加载器收到了类加载的请求,它首先不会尝试自己去加载此类,而是把请求委派给父类(不是继承关系,只是逻辑父类)去加载。如此迭代,最终所有的加载请求都会传达到顶层的启动类加载器进行加载。以避免出现诸如多个类加载都有自己的java.lang.Object类的情况造成的混乱。

4.3 线程上下文类加载器

这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法设置一个classloader进来,在调用此方法设置自己的类加载器之前,会使用父线程中继承的类加载器。其和双亲委派模型相反,实现了父类调用子类加载器。

4.4OSGI类加载器

为了实现热替换技术,退出的加载器,这里已经不是双亲委派模型,而是网状结构,当收到类加载请求时,OSGI按照以下顺序进行搜索:

  1. 若是以java.*开头的类,则委派给父类加载器加载。
  2. 否则,若是委派列表名单内的类,则委派给父类加载器加载。
  3. 否则,若是Import列表中的类,则委派给Export这个类的Bundle的类加载去加载。
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
  6. 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器加载。
  7. 否则,查找失败。

 

 

 

 

 

 

Java类中各成分加载顺序内存中的存放位置

一、什么时候会加载类?
使用到类中的内容时加载:有三种情况
1.
创建对象:new StaticCode();
2.
使用类中的静态成员:StaticCode.num=9;  StaticCode.show();
3.
在命令行中运行:Java StaticCodeDemo

二、类所有内容加载顺序和内存中的存放位置:
利用语句进行分析。
1.Person p=new Person("zhangsan",20);
该句话所做的事情:
1.
在栈内存中,开辟main函数的空间,建立main函数的变量 p
2.
加载类文件:因为new要用到Person.class,所以要先从硬盘中找到Person.class类文件,并加载到内存中。
加载类文件时,除了非静态成员变量(对象的特有属性)不会被加载,其它的都会被加载。
记住:加载,是将类文件中的一行行内容存放到了内存当中,并不会执行任何语句。---->加载时期,即使有输出语句也不会执行。
静态成员变量(类变量)  ----->方法区的静态部分
静态方法             ----->方法区的静态部分
非静态方法(包括构造函数)  ----->方法区的非静态部分
静态代码块 ----->方法区的静态部分
构造代码块 ----->方法区的静态部分

注意:在Person.class文件加载时,静态方法和非静态方法都会加载到方法区中,只不过要调用到非静态方法时需要先实例化一个对象,对象才能调用非静态方法。如果让类中所有的非静态方法都随着对象的实例化而建立一次,那么会大量消耗内存资源,
所以才会让所有对象共享这些非静态方法,然后用this关键字指向调用非静态方法的对象


3.
执行类中的静态代码块:如果有的话,对Person.class类进行初始化。
4.
开辟空间:在堆内存中开辟空间,分配内存地址。
5.
默认初始化:在堆内存中建立对象的特有属性,并进行默认初始化。
6.
显示初始化:对属性进行显示初始化。
7.
构造代码块:执行类中的构造代码块,对对象进行构造代码块初始化。
8.
构造函数初始化:对对象进行对应的构造函数初始化。
9.
将内存地址赋值给栈内存中的变量p
2.p.setName("lisi");
1.
在栈内存中开辟setName方法的空间,里面有:对象的引用this,临时变量name
2.
p的值赋值给this,this就指向了堆中调用该方法的对象
3.
"lisi" 赋值给临时变量name
4.
将临时变量的值赋值给thisname
3.Person.showCountry();
1.
在栈内存中,开辟showCountry()方法的空间,里面有:类名的引用Person
2.Person
指向方法区中Person类的静态方法区的地址。
3.
调用静态方法区中的country,并输出。
 
注意:要想使用类中的成员,必须调用。通过什么调用?有:类名、thissuper
  
三、静态代码块、构造代码块和构造函数的区别
静态代码块:用于给类初始化,类加载时就会被加载执行,只加载一次。
构造代码块:用于给对象初始化的。只要建立对象该部分就会被执行,且优先于构造函数。
构造函数:  给对应对象初始化的,建立对象时,选择相应的构造函数初始化对象。
 
创建对象时,三者被加载执行顺序:静态代码块--->构造代码块--->构造函数
 
//
利用代码进行测试例题:06--06StaticCodeDemo.Java

[java] view plain copy

1.  class Person  

2. {  

3.  private String name;  

4. private int age=0;  

5.  private static String country="cn";  

6. Person(String name,int age)  

7.  {  

8. this.name=name;  

9.  this.age=age;   

10.}  

11. static  

12.{  

13. System.out.println("静态代码块被执行");  

14.}  

15. { System.out.println(name+"..."+age);}  

16.public void setName(String name)  

17. {  

18.this.name=name;  

19. }  

20.public void speak()  

21. {  

22.System.out.println(this.name+"..."+this.age);  

23. }  

24.public static void showCountry()  

25. {  

26.System.out.println("country="+country);  

27. }  

28.}  

29. class StaticDemo  

30.{  

31. static  

32.{  

33. System.out.println("StaticDemo 静态代码块1");  

34.}  

35. public static void main(String[] args)  

36.{  

37. Person p=new Person("zhangsan",100);  

38.p.setName("lisi");  

39. p.speak();  

40.Person.showCountry();  

41. }  

42.static  

43. {  

44.System.out.println("StaticDemo 静态代码块2");  

45. }   

46.}  




输出结果:
 StaticDemo
静态代码块1
 StaticDemo
静态代码块2
 
静态代码块被执行
 null...0    //
构造代码块
 lisi...100  //speak()
 country=cn  //showCountry()