类的加载、连接与初始化


        
        • 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是一个编译时的常量,编译的时候就知道值是多少,不需要对类进行初始化
        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 MyClassLoader(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高级特性与最佳实践》

JVM 类的生命周期、类加载器的更多相关文章

  1. Java - JVM - 类的生命周期

    概述 简述 JVM 里 类的生命周期 上次写了 30%, 居然丢了 难受, 又要重新写 类的生命周期 加载 使用 卸载 1. 加载 概述 类型的加载 大体流程 装载 连接 验证 准备 解析(可选的) ...

  2. JVM类加载器及Java类的生命周期

    预定义类加载器(三种): 启动(Bootstrap)类加载器: 是用本地代码实现的类装入器,它负责将<Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar) ...

  3. 乐字节Java反射之三:方法、数组、类加载器和类的生命周期

    本文承接上一篇:乐字节Java发射之二:实例化对象.接口与父类.修饰符和属性 继续讲述Java反射之三:方法.数组.类加载器 一.方法 获取所有方法(包括父类或接口),使用Method即可. publ ...

  4. JVM:类的生命周期

    类的生命周期 综述 1.    只有当一个类被切实使用到的时候才会被加载到虚拟机中(例如:new, 方法调用, A a = null;不算) 2.    若在加载一个类的过程中,有其他类被切实使用到, ...

  5. <JVM中篇:字节码与类的加载篇>03-类的加载过程(类的生命周期)详解

    笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 同步更新:https://gitee.com/vectorx/NOTE_JVM https://codechina.cs ...

  6. JVM与垃圾回收机制(GC)和类的生命周期

    JVM运行时数据区 GC(垃圾回收机制) 什么是垃圾回收机制: 在系统运行过程中,会产生一些无用的对象,这些对象占据着一定的内存,如果不对这些对象清理回收无用的是对象,可能会导致内存的耗尽,所以垃圾回 ...

  7. Java类的生命周期详解

    引言 最近有位细心的朋友在阅读笔者的文章时,对java类的生命周期问题有一些疑惑,笔者打开百度搜了一下相关的问题,看到网上的资料很少有把这个问题讲明白的,主要是因为目前国内java方面的教材大多只是告 ...

  8. 【转】Java 类的生命周期详解

    一. 引 言 最近有位细心的朋友在阅读笔者的文章时,对java类的生命周期问题有一些疑惑,笔者打开百度搜了一下相关的问题,看到网上的资料很少有把这个问题讲明白的,主要是因为目前国内java方面的教材大 ...

  9. 【转载】详解java类的生命周期

    原文地址:http://blog.csdn.net/zhengzhb/article/details/7517213 引言 最近有位细心的朋友在阅读笔者的文章时,对java类的生命周期问题有一些疑惑, ...

随机推荐

  1. Codeforces Round #396(Div. 2) A. Mahmoud and Longest Uncommon Subsequence

    [题意概述] 找两个字符串的最长不公共子串. [题目分析] 两个字符串的最长不公共子串就应该是其中一个字符串本身,那么判断两个字符串是否相等,如果相等,那么肯定没有公共子串,输出"-1&qu ...

  2. Node.js URL

    稳定性: 3 - 稳定 这个模块包含分析和解析 URL 的工具.调用 require('url') 来访问模块. 解析 URL 对象有以下内容,依赖于他们是否在 URL 字符串里存在.任何不在 URL ...

  3. MongoDB 自动增长

    MongoDB 没有像 SQL 一样有自动增长的功能, MongoDB 的 _id 是系统自动生成的12字节唯一标识. 但在某些情况下,我们可能需要实现 ObjectId 自动增长功能. 由于 Mon ...

  4. Android开发技巧——设置系统状态栏颜色

    开门见山,先来三张效果图: 然后我们再来讲如何实现以及如何快速地实现. 如何实现 实现设置系统状态栏颜色需要至少在Android 4.4.2(API 19)以上.这是因为,在这个版本以下,没有任何的A ...

  5. webpack dev server 和 sublime text 配合时需要注意的地方

    参考:https://webpack.js.org/guides/development/ Adjusting Your Text Editor Some text editors have a &q ...

  6. Java中常用缓存Cache机制的实现

    缓存,就是将程序或系统经常要调用的对象存在内存中,一遍其使用时可以快速调用,不必再去创建新的重复的实例. 这样做可以减少系统开销,提高系统效率. 缓存主要可分为二大类: 一.通过文件缓存,顾名思义文件 ...

  7. malloc_stats---检查内存泄露的神器

    在之前的博客中提到过,valgrind可以用来检测内存泄露,但在使用中,往往会遇到一些问题,给调试工作带来很多不必要的麻烦,我自己遇到的有以下两种: (1)内存泄露误检(系统初始化时,可能有一些需要长 ...

  8. Android 5.0 调色 Palette调色功能

    Palette非常好用,也非常好玩. Palette的作用是从图像中提取突出的颜色,这样我们可以根据提取到的色值把它赋给Toolbar,标题,状态栏等,可以使我们的整个界面色调统一,效果非常好看. P ...

  9. Swift基础之自定义PUSH和POP跳转动画

    之前用OC代码写过PUSH和POP的转场动画,闲来无事,将其转换成Swift语言,希望对大家有帮助,转载请注明.... 如何实现PUSH和POP的转场动画? 首先,创建一个NSObject的类,分别用 ...

  10. C++ 中const作用

    一.对const与#define的特点及区别的理解 #define只是用来做文本替换的,#define常量的生命周期止于编译期,它存在于程序的代码段,在实际程序中它只是一个常数,一个命令中的参数,并没 ...