JVM 加载 class 文件的原理机制(类的生命周期、类加载器)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Jerome_s/article/details/52080261

类的加载、连接与初始化


        
        • 1. 加载:查找并加载类的二进制数据
        • 2. 连接
            – 2.1 验证:确保被加载的类的正确性
            – 2.2 准备:为类的 静态变量分配内存,并将其初始 化为默认值 
            – 2.3 解析:把类中的符号引用转换为直接引用
        • 3. 初始化:为类的静态变量赋予 正确的初始

        以下代码执行结果可以更清楚的理解上面的过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public  class  Test {
     public  static  void  main(String[] args) {
         Count count = Count.getInstance();
         System.out.println( "count1 = "  + count.count1);
         System.out.println( "count2 = "  + count.count2);
     }
}
 
class  Count {
     // 这个运行结果是 count1 = 1 count2 = 0 ; 因为按顺序执行1. Count(); 2. count1; 3. count2;
     private  static  Count count =  new  Count();
     public  static  int  count1;
     public  static  int  count2 =  0 ;
     // 所以这个运行结果是 count1 = 1 count2 = 1 ;
     // private static Count count = new Count();
 
     private  Count() {
         count1++;
         count2++;
     }
 
     public  static  Count getInstance() {
         return  count;
     }
}

        下面分别对上面几个步骤进行深入的分析。

1. 类的加载


        类的加载指的是将类的 .class 文件中的 二进 制数据读入到内存中,将其放在运行时数 据区的方法区内,然后在堆区创建一个  java.lang.Class 对象,用来封装类在方法 区内的数据结构。 类的加载的最终产品是位于堆区中的 Class 对象, Class 对象封装了类在方法区内的数据结构  ,并且向 Java 程序员提供了访问方法区内 的数据结构的接口。
        

        类加载器并 不需要等到某个类被“首次主 动使用”时再加载它, JVM 规范允许类加载器在预料某个类将要 被使用时就预先加载它,如果在预先加载 的过程中遇到了.class 文件缺失或存在错误 ,类加载器必须在程序首次主动使用该类 时才报告错误(LinkageError错误), 如果这个类一直没有被程序主动使用,那 类加载器就不会报告错误。

        1.1 两种类型的类加载器 (ClassLoader)

        1.1.1 Java虚拟机自带的加载器

            • 根类加载器( Bootstrap,使用 c++ 编写,无法在 Java 代码中得到该类)
            • 扩展类加载器( Extension,使用 Java 实现)
            • 系统类加载器( System,应用加载器,使用Java代码实现)

        1.1.2 用户自定义的类加载器

            •  java.lang.ClassLoader 的子类
            • 用户可以定制类的加载方式
      
        类被加载后,就进入连接阶段。连接就是 将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

2. 连接


2.1 类的验证


类的验证主要包括以下内容

1. 类文件的结构检査:

        确保类文件遵从 Java 类文件的固定格式。

2. 语义检查:

        确保类本身符合 Java 语言的语法规定,比如验证 final 类型的类没有子类,以及 final 类型的方法没有被覆盖。

3. 子节码验证:

        确保字节码流可以被 Java 虚拟机安全地执行。字节码流代表 Java 方法(包括静态方法和实例方法),它是由被称做操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。

4. 二进制兼容的验证:

        确保相互引用的类之间协调一致。例如在 Worker 类的 gotoWork() 方法中会调用 Car 类的 run() 方法。Java 虚拟机在验证 Worker 类时, 会检查在方法区内是否存在 Car 类的 run() 方法,假如不存在(当 Worker 类和 Car 类的版本不兼容,就会出现这种问题,参考: JAVA类文件版本(class version)与JDK对应关系 ),就会抛出NoSuchMethodError 错误。

2.2 类的准备


        在准备阶段,JVM 为类的静态变最分配内存,并设置默认的初始值。例如对于以下 Sample 类,在准备阶段,将为 int 类型的静态变量 a 分配4个字节的内存空间,并且陚默认值0,为 long 类型的静态变最 b 分配8个字节的内存空间,并且陚予默认值0。
1
2
3
4
5
6
7
8
9
public  class  Sample {
     private  static  int  a =  1 ;
     private  static  long  b;
 
     static  {
         b =  2 ;
     }
     // ...
}


2.3 类的解析


        在解析阶段,JVM  会把类的二进制数据中的符号引用替换为直接引用。例如在 Worker 类的gotoWork() 方法中会引用 Car 类的run() 方法。
1
2
3
public  void  gotoWord(){
     car.run();  //这段代码在Worker类的二进制数据中表示为符号引用
}

        在 Worker 类的二进制数据中,包含了一个对 Car 类的 run() 方法的符号引用,它由 run() 方法的全名和相关描述符组成。在解析阶段,JVM 会把这个符号引用替换为一个指针,该指针指向 Car 类的 run() 方法在方法区内的内存位置, 这个指针就是直接引用

3. 类的初始化


        在初始化阶段,JVM 执行类的初始化语句,为类的静态变最赋予初始值。在程序中,静态变量的初始化有两种途径:
        (1)在静态变量的声明处进行初始化;
        (2)在静态代码块中进行初始化。
        例如在以下代码中,静态变最 a 和 b 都被显式初始化, 而静态变最 c 没有被显式初始化,它将保持默认值0。
1
2
3
4
5
6
7
8
9
10
public  class  Sample {
     private  static  int  a =  1 ;
     private  static  long  b;
     private  static  long  c;
 
     static  {
         b =  2 ;
     }
     // ...
}
   
        静态变量的声明语句,以及静态代码块都被看做类的初始化语句,JVM 会按照初始化语句在类文件中的先后顺序来依次执行它们。例如当以下 Sample 类被初始化后,它的静态变最 a 的取值为4。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class  Sample {
     private  static  int  a =  1 ;
 
     static  {
         a =  2 ;
     }
 
     static  {
         a =  4 ;
     }
 
     public  static  void  main(String[] args) {
         System.out.println(a);  // 输出4
     }
}

类的初始化步骤

        (1) 假如这个类还没有被加载和连接,那就先进行加载和连接。
        (2) 假如类存在直接的父类,并且这个父类还没有被初始化, 那就先初始化直接的父类(接口除外,下面有详细介绍)。
        (3) 假如类中存在初始化语句,那就依次执行这些初始化语句。

3. 1 类的初始化时机


Java 程序对类的使用方式可分为两种

        – 主动使用
        – 被动使用

        所有的 JVM 实现必须在每个类或接 口被 Java 程序“首次主动使用”时才初始 化它们。

主动使用的情况(六种)
 
        – 创建类的实例
        – 访问某个类或接口的静态变量,或者对该静态 变量赋值 
        – 调用类的静态方法 
        – 反射(如  Class.forName(“com.demo.Test”)  ) 
        – 初始化一个类的子类
        –  JVM 启动时被标明为启动类的类( Java   Test)

        除了以上六种情况,其他使用 Java 类的方 式都被看作是对类的被动使用都不会导 致类的初始化。

        以下代码可以加深理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public  class  Test {
     public  static  void  main(String[] args) {
         // x是一个编译时的常量,编译的时候就知道值是多少,不需要对类进行初始化
        // 如果 final 去除掉以后,就会执行“FinalTest staic block!”了
         System.out.println(FinalTest.x);
         // x非编译时的常量,x在编译时不知道是多少,
         // 运行才知道的就需要对类进行初始化,对类进行初始化static代码快就会执行
         System.out.println(FinalTest2.x);
     }
}
 
class  FinalTest {
     public  static  final  int  x =  6  3 ;
 
     static  {
         System.out.println( "FinalTest staic block!" );
     }
}
 
class  FinalTest2 {
     public  static  final  int  x =  new  Random().nextInt( 100 );
 
     static  {
         System.out.println( "FinalTest2 staic block!" );
     }
}
运行结果:    
2
FinalTest2 staic block!
50

当 JVM 初始化一个类时,要求它的所有父类都己经被初始化,但是这条规则并不适用于接口。

        • 在初始化一个类时,并不会先初始化它所实现的接口。
        • 在初始化一个接口时,并不会先初始化它的父接口。
        
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变最时,才会导致该接口的初始化。

没有使用接口的代码如下(接口代码不好模拟啊):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public  class  Test {
     static  {
         System.out.println( "Test static block!" );
     }
 
     public  static  void  main(String[] args) {
         System.out.println(Child.b);
     }
}
 
class  Parent {
     static  int  a =  3 ;
 
     static  {
         System.out.println( "Parent static block!" );
     }
}
 
class  Child  extends  Parent {
     static  int  b =  4 ;
 
     static  {
         System.out.println( "Child static block!" );
     }
}
运行结果:
Test static block!
Parent static block!
Child static block!
4

只有当程序访问的静态变量或静态方法确 实在当前类或当前接口中定义时,才可以 认为是对类或接口的主动使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public  class  Test {
     public  static  void  main(String[] args) {
         System.out.println(Child.a);
         Child.doSomething();
     }
}
 
class  Parent {
     static  int  a =  3 ;
 
     static  {
         System.out.println( "Parent static block!" );
     }
 
     static  void  doSomething() {
         System.out.println( "do something!" );
     }
}
 
class  Child  extends  Parent {
 
     static  {
         System.out.println( "Child static block!" );
     }
}
运行结果:(不在当前类定义只在父类定义,参考以上 六种主动使用的情况)
Parent static block!
3
do something!

另外,调用 ClassLoader  类的 loadClass 方法加载 一个类,并不是对类的主动使用,不会导 致类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public  class  Test {
     public  static  void  main(String[] args)  throws  ClassNotFoundException {
         // 获取系统类加载器
         ClassLoader loader = ClassLoader.getSystemClassLoader();
         // 这行代码没有导致任何输出 不会导致类的初始化
         Class<?> clazz = loader.loadClass( "CL" );
         System.out.println( "------" );
         clazz = Class.forName( "CL" );
     }
}
 
class  CL {
     static  {
         System.out.println( "Class CL" );
     }
}
运行结果:
------
Class CL

类加载器


        类加载器用来把类加载到 JVM 中。从 JDK 1.2 版本开始,类的加载过程采用 父亲委托机制,这种机制能更好地保证 Java 平台的安全。在此委托机制中,除了 JVM 自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当 Java 程序请求加载器 loader1 加载 Sample 类时,loader1 首先委托自己的父加载器去加载 Sample 类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器  loader1 本身加载 Sample 类。

        JVM 自带的类加载器之间的关系
        

根(Bootstrap)类加载器

        该加载器没有父加载器。它负责加载虚拟机的核心类库,如 java.lang.*  等。例如从下面代码可以看出,java.lang.Object 就是由根类加载器加载的。根类加载器从系统属性 sun.boot.class.path 所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承 java.lang.ClassLoader 类。

扩展(Extension)类加载器
 
       它的父加载器为根类加载器。它从 java.ext.dirs 系统属性所指定的目录中加载类库,或者从 JDK 的安装目录的 jre\lib\ext 子目录(扩展目录)下加载类库,如果把用户创建的 JAR 文件放在这个目录下, 也会自动由扩展类加载器加载。扩展类加载器是纯 Java 类,是  java.lang.ClassLoader 类的子类。

系统(System)类加载器

        也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中加载类, 它是用户自定义的类加载器的默认父加载器。系统类加载器是纯 Java 类,是 java.lang.ClassLoader 类的子类。
        
用户自定义的类加载器

        除了以上虚拟机自带的加载器以外,用户还可以定制自己的类加载器(User-defined Class Loader)。Java 提供了 抽象类 javaJang.ClassLoader,所有用户自定义的类加载器应该继承 ClassLoader 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public  class  Test {
     public  static  void  main(String[] args)  throws  ClassNotFoundException {
         // String 是由根类加载器加载的,下面打印结果为null
         Class<?> clazz = Class.forName( "java.lang.String" );
         System.out.println(clazz.getClassLoader());
 
         // 应用加载器加载的
         Class<?> clazz2 = Class.forName( "C" );
         System.out.println(clazz2.getClassLoader());
 
 
     }
}
 
class  C {
}
打印结果:
null
sun.misc.Launcher$ AppClassLoader@42a57993

类加载的父委托机制


        在父亲娄托机制中,各个加载器按照父子关系形成树形结构,除了根类加载器以外,其余的类加载器都有且只有一个父加载器。
        

        loader2 首先从自己的命名空间中查找 Sample 类是否己经被加载,如果己经加载,就直接返回代表 Sample 类的 Class 对象的引用。 如果 Sample 类还没有被加载,loader2 首先请求 loader1 代为加载,loader1 再请求系统类加载器代为加载,系统类加载器再请求扩展类加载器代为加载,扩展类加载器再请求根类加载器代为加载。若根类加载器和扩展类加载器都不能加载,则系统类加载器 尝试加载,若能加载成功,则将 Sample 类所对应的 Class 对象的引用返回给 loader1,loader1 再将引用返回给 loader2,从而成功将 Sample 类加载进虚拟机。若系统类加载器不能加载 Sample 类,则 loader1 尝试加载 Sample 类(上图所示),若 loader1 也不能成功加载,则 loader2 尝试加载。若所有的父加载器及 loader2 本身都不能加载,则抛出 ClassNotFoundException 异常。

        需要指出的是,加载器之间的父子关系实际上指的是加载器对象之间的包装关系, 而不是类之间的继承关系。一对父子加载器可能是同一个加载器类的两个实例,也可能不是。在子加载器对象中包装了一个父加载器对象。例如以下 loader1 和 loader2 都是 MyClassLoader 类的实例,并 loader2 包装了 loader1, loader1 是 loader2 的父加载器。
1
2
3
4
ClassLoader loader1 =  new  MyClassLoader();
 
// 参数loader1将作为loader2的父加载器
ClassLoader loader2 =  new M yClassLoader(loader1);

        当生成一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器就将成为该类加载器的父加载器。

         父亲委托机制的优点是能够提高软件系统的安全性,因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类, 从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。例如,java.lang.Object 类总是由根类加载器加载,其 他任何用户自定义的类加载器都不可能加载含有恶意代码的 java.lang.Object 类。

        定义类加载器 :如果某个类加载器能够加载一个类,那么该类加载器就称作定义类加载器;
        初始类加载器:定义类加载器及其所有子加载器都称作初始类加载器;

命名空间


        每个类加载器都有自己的命名空间, 命名空间由该加载器及所有父加载器所加载的类组成 。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

        不会出现完整名字一样的原因是在同一个命名空间,只会被类加载器加载一次。不同命名空间就会被各自不同命名空间的类加载器分别加载。

运行时包(package


         由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。 这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。 假设用户自己定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于 java.lang.Spy和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的包可见成员。


创建用户自定义的类加载器 


        要创建用户自己的类加载器,只需要扩展 java.lang.ClassLoader 类,然后覆盖它的 findClass(String name) 方法即可,该方法根据参数指定的类的名字,返冋对应的 Class 对象的引用。

        自定义类加载器的结构图
        

        新建三个类,MyClassLoader、Dog、Sample,不能有包名
        新建四个文件夹D:\myapp\otherlib、D:\myapp\serverlib、D:\myapp\clientlib、D:\myapp\syslib
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public  class  MyClassLoader  extends  ClassLoader {
 
     // 类加载器名字
     private  String name;
     // 加载类的路径
     private  String path =  "d:\\" ;
     // class文件的扩展名
     private  final  String fileType =  ".class" ;
 
     public  MyClassLoader(String name) {
         super (); // 让系统类加载器成为该类加载器的父加载器
         this .name = name;
     }
 
     public  MyClassLoader(ClassLoader parent, String name) {
         super (parent);  // 显示指定该类加载器的父加载器
         this .name = name;
     }
 
     public  String toString() {
         return  this .name;
     }
 
     public  String getPath() {
         return  path;
     }
 
     public  void  setPath(String path) {
         this .path = path;
     }
 
     @Override
     protected  Class<?> findClass(String name)  throws  ClassNotFoundException {
         byte [] data =  this .loadClassData(name);
         return  this .defineClass(name, data,  0 , data.length);
     }
 
     private  byte [] loadClassData(String name) {
         InputStream is =  null ;
         byte [] data =  null ;
         ByteArrayOutputStream baos =  null ;
 
         try  {
             this .name =  this .name.replace( "." "\\" );
             is =  new  FileInputStream( new  File(path + name + fileType));
             baos =  new  ByteArrayOutputStream();
 
             int  ch =  0 ;
             while  (- 1  != (ch = is.read())) {
                 baos.write(ch);
             }
 
             data = baos.toByteArray();
         catch  (Exception e) {
             e.printStackTrace();
         finally  {
             try  {
                 baos.close();
                 is.close();
             catch  (IOException e) {
                 e.printStackTrace();
             }
         }
 
         return  data;
     }
 
     public  static  void  main(String[] args)  throws  Exception {
 
         // 父加载器为系统类加载器
         MyClassLoader loader1 =  new  MyClassLoader( "loader1" );
         loader1.setPath( "D:\\test\\serverlib\\" );
  
         // 指定loader2的父加载器为loader1
         MyClassLoader loader2 =  new  MyClassLoader(loader1,  "loader2" );
         loader2.setPath( "D:\\test\\clientlib\\" );
 
         // 指定loader3的父加载器为根加载器
         MyClassLoader loader3 =  new  MyClassLoader( null "loader3" );
         loader3.setPath( "D:\\test\\otherlib\\" );
 
         test(loader2);
         test(loader3);
     }
 
     public  static  void  test(ClassLoader loader)  throws  Exception {
         Class clazz = loader.loadClass( "Sample" );
         Object object = clazz.newInstance();
     }
 
}

1
2
3
4
5
6
public  class  Dog {
 
     public  Dog() {
         System.out.println( "Dog is load by : "  this .getClass().getClassLoader());
     }
}

1
2
3
4
5
6
7
8
9
10
11
12
public  class  Sample {
 
     public  int v1  =  1 ;
 
     public  Sample() {
 
         System.out.println( "Sample is load by : "  this .getClass().getClassLoader());
 
         // 主动使用Dog
         new  Dog();
     }
}

测试例子:
将生成的.class文件拷贝出来放置如下位置:
syslib放自己定义的加载器 MyClassLoader.class
情况1: Sample.class和Dog.class拷贝到serverlib和otherlib下,执行结果
1
2
3
4
5
D:\myapp\syslib>java MyClassLoader
Sample is load by : loader1
Dog is load by : loader1
Sample is load by : loader3
Dog is load by : loader3
Sample,由loader1加载到。

情况2. 把serverlib删掉,otherlib不删除,loader2最底层的,找不到,所以提示找不到类文件。

情况3. 都放到syslib下面(otherlib不变),由系统类加载器加载,加载当前./目录:系统加载器加载classpath
1
2
3
4
5
D:\myapp\syslib>java MyClassLoader
Sample is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Dog is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Sample is load by : loader3
Dog is load by : loader3

情况4:删除syslib下面的Sample和Dog,拷贝到serverlib将serverlib设置为classpath
1
2
3
4
5
D:\myapp\syslib>java - cp  .;d:\myapp\serverlib MyClassLoader
Sample is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Dog is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Sample is load by : loader3
Dog is load by : loader3

        这里需要注意:Sample 被加载两次是因为他们是不同的类加载器加载的。在不同的命名空间,如下图
        
        在 loader1 和 loader3 各自的命名空间中都存在 Sample 类和 Dog 类。

        在 Sample 类中主动使用了 Dog 类,当执行 Sample 类的构造方法中的 new Dog() 语句时,JVM 需要先加载 Dog 类,到底用哪个类加载器加载呢?从情况1的打印结果可以看出,加载 Sample 类的 loader1 还加载 Dog 类,JVM 会用 Sample 类的 定义类加载器去加载 Dog 类,加载过程也同样采用父亲委托机制。为了验证这一点,可以把 D:\myapp\serverlib 目录下的 Dog.class 文件刪除,然后在 D:\myapp\syslib 目录下存放一个Dog.class文件,此时程序的打印结果为:
1
2
3
4
5
D:\myapp\syslib>java MyClassLoader
Sample is load by : loader1
Dog is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Sample is load by : loader3
Dog is load by : loader3

        由此可见,当由 loader1 加载的 Sample 类首次主动使用 Dog 类时,Dog 类由系统类加载器加载。如果把 D:\myapp\serverlib 和 D:\myapp\syslib 目录下的 Dog.class 文件都删除,然后在 D:\myapp\clientlib 目采下存放一个 Dog.class 文件,此时的目录结构如下图,当由 loader1 加载的 Sample 类首次主动使用 Dog 类时,由于 loader1 及它的父加载器都无法加载 Dog 类,因此 test(loader2) 方法会抛出 ClassNotFoundException。
        


不同类加载器的命名空间关系

        同一个命名空间内的类是相互可见的。

        子加载器命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。 由父加载器加载的类不能看见子加载器加载的类。

        如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。

        修改 MyClassLoader 类的  main 方法
1
2
3
4
5
6
7
8
9
10
11
public  static  void  main(String[] args)  throws  Exception {
 
     // 父加载器为系统类加载器
     MyClassLoader loader1 =  new  MyClassLoader( "loader1" );
     loader1.setPath( "D:\\myapp\\serverlib\\" );
     
     Class clazz = loader1.loadClass( "Sample" );
     Object object = clazz.newInstance();  // 创建对象
     Sample sample = (Sample)object;
     System.out.println(sample.v1);
}
        把 Sample.class 和 Dog.class 仅仅拷贝到 D:\myapp\serverlib 下
1
2
3
4
5
6
7
8
9
10
11
D:\myapp\syslib>java MyClassLoader
Sample is load by : loader1
Dog is load by : loader1
Exception  in  thread  "main"  java.lang.NoClassDefFoundError: Sample
         at MyClassLoader.main(MyClassLoader.java:110)
Caused by: java.lang.ClassNotFoundException: Sample
         at java.net.URLClassLoader.findClass(Unknown Source)
         at java.lang.ClassLoader.loadClass(Unknown Source)
         at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
         at java.lang.ClassLoader.loadClass(Unknown Source)
         ... 1  more

        MyclassLoader 类由系统类加载器加载,而 Sample 类由 loader1 类加载,因此 MyClassLoader 看不见  Sample 类。在 MyCIassLoader 类的 main() 方法中使用 Sample 类,会导致 NoClassDefFoundError 错误。

        如果把 D:\myapp\serverlib 目录下的 Sample.class 和 Dog.class 删除,再把这两个文件拷贝到 D:\myapp\syslib 目录下,然后运行 main() 方法,也能正常运行。 此时 MyClassLoader 类和 Sample 类都由系统类加载器加载,由于它们位于同一个命名空间内,因此相互可见。

        当两个不同命名空间内的类相互不可见时,可采用 Java 反射机制来访问对方实例的属性和方法。如果把 MyClassLoader 类的 main() 方法替换为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
public  static  void  main(String[] args)  throws  Exception {
 
     // 父加载器为系统类加载器
     MyClassLoader loader1 =  new  MyClassLoader( "loader1" );
     loader1.setPath( "D:\\myapp\\serverlib\\" );
     
     Class clazz = loader1.loadClass( "Sample" );
     Object object = clazz.newInstance();  // 创建对象
     Field field = clazz.getField( "v1" );
     int  v1 = field.getInt(object);
     System.out.println( "v1:"  + v1);
}
        运行结果:
1
2
3
4
D:\myapp\syslib>java MyClassLoader
Sample is load by : loader1
Dog is load by : loader1
v1:1

类的卸载


        当 Sample 类被加载、连接和初始化后,它的生命周期就开始了。当代表 Sample 类的 Class 对象不再被引用,即不可触及时,Class 对象就会结束生命周期,Sample 类在方法区内的数据也会被卸载,从而结束 Sample 类的生命周期。由此可见,一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期。

        由 JVM 自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面己经介绍过,JVM 自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。JVM 本身会始终引用这些类加载器,而这些类加钱器则会始终引用它们所加载的类的 Class 对象,因此这些 Class 对象始终是可触及的。

        由用户自定义的类加载器所加载的类是可以被卸载的。

        实验:把 Sample.class 和 Dog.class 拷贝到 serverlib 下,修改 MyClassLoader 的 main 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public  static  void  main(String[] args)  throws  Exception {
 
     // 父加载器为系统类加载器
     MyClassLoader loader1 =  new  MyClassLoader( "loader1" );                  //1
     loader1.setPath( "D:\\myapp\\serverlib\\" );                             //2
     
     Class objClass = loader1.loadClass( "Sample" );                          //3
     System.out.println( "objClass's hashCode is "  + objClass.hashCode());   //4
     Object obj = objClass.newInstance();  // 创建对象                       //5
     
     loader1 =  null ;                                                        //6
     objClass =  null ;                                                       //7
     obj =  null ;                                                            //8
     
     loader1 =  new  MyClassLoader( "loader1" );                                //9
    loader1.setPath("D:\\myapp\\serverlib\\");
     objClass = loader1.loadClass( "Sample" );                                //10
     System.out.println( "objClass's hashCode is "  + objClass.hashCode());   //11
}

        运行结果:
1
2
3
4
5
D:\myapp\syslib>java MyClassLoader
objClass's hashCode is  1311053135
Sample is load by : loader1
Dog is load by : loader1
objClass's hashCode is  865113938

        从以上打印结果可以看出,程序两次打印 objClass 变量引用的 Class 对象的哈希码, 得到的数值不同。因此 objClass 变最两次引用不同的 Class 对象,可见在 JVM 的生命周期中,对 Sample 类先后加载了两次。

        运行以上程序时,Sample 类由 loader1 加载,在类加载器的内部实现中,用一个 Java 集合来存放所加载类的引用。另一方面,一个 Class 对象总是会引用它的类加载器,调用 Class 对象的 getClassLoader() 方法,就能获得它的类加载器。由此可见,代表 Sample 类的 Class 实例与 loader1 之间为双向关联关系。

         —个类的实例总是引用代表这个类的 Class 对象,在 Object 类中定义了 getClass() 方法,这个方法返回代表对象所属类的 Class 对象的引用。此外,所有的 Java 类都有—个静态属性 class,它引用代表这个类的 Class 对象。

        当程序执行第5步时,引用变量与对象之间的引用关系如图
        

        从上图可以看出,loader1 变量和 obj 变量间接引用代表 Sample 类的 Class 对象, 而 objClass 变量则直接引用它。
 
       当程序执行完第8步时,所有的引用变量都置为 null,此时 Sample 对象结束生命周期,MyClassLoader 对象结束生命周期,代表 Sample 类的 Class 对象也结束生命周 期,Sample 类在方法区内的二进制数据被卸载。

        当程序执行完第10步时,Sample 类又重新被加载,在 JVM 的堆区会生成一个新的代表 Sample 类的 Class 实例。

在如下几种情况下,JVM 将结束生命周期

    – 执行了System.exit()方法
    – 程序正常执行结束
    – 程序在执行过程中遇到了异常或错误而异常终 止 
    – 由于操作系统出现错误而导致Java虚拟机进程 终止


参考:
        视频: 链接: http://pan.baidu.com/s/1cIBS8A  密码:s5sh
        pdf: 链接: http://pan.baidu.com/s/1geTbRMz  密码:hiwj 【 深入Java虚拟机视频教程课件.pdf
        《 深入理解Java虚拟机 JVM高级特性与最佳实践

猜你喜欢

转载自blog.csdn.net/Jerome_s/article/details/52080261
今日推荐