天天看点

JVM- 类的加载过程、类加载器,看这就够了。

作者:晓视世界

一、类的加载过程

类从加载到内存中开始,到卸载出内存位置,为类的生命周期。

包括加载(loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiazation)、使用(Using)、卸载(Unloading)7个阶段。其中验证、准备、连接统称为连接(linking)。

JVM- 类的加载过程、类加载器,看这就够了。

其中加载、验证、准备、初始化和卸载这5个阶段的顺序是一定的;类的加载过程必须按照这个顺序按部就班的开始,而解析阶段不一定;

解析阶段在某些情况下,可以在初始化之后解析,支持java语言的运行时绑定(也就是动态绑定或晚期绑定)

1、Loading加载

(1)同过全限定名获取定义此类的二进制流;

(2)将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内;

(3)然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中)用来封装类在方法区的数据结构,并作为方法区这个类的各种数据的访问入口;

(4)加载.class的方式:

本地系统直接加载;

从zip包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础;常见的应用程序在服务器上部署就是从JAR包中读取.class文件;

从网络中获取,这种场景最典型的就是Applet(小程序);

运行时计算生成,这种场景使用的最多的就是动态代理,在java.lang.reflect.Proxy,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流;;

从专有数据库中提取.class文件;例如:中间件服务器(SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发;

有其他文件生成,如:JSP应用,即有JSP文件生成对应的Class类

其他

2、Verification验证(Linking连接的第一阶段)

验证是连接阶段的第一步,包含:文件格式验证、元数据验证、字节码验证、符号引用验证。

验证阶段非常总要,但不一定必要,可以在实施阶段使用-Xverify:none来关闭大部分的验证措施。

(1)文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

通过文件格式验证后,字节流会进入内存的方法区中进行存储;

其余3个验证阶段都是基于方法区的存储结构进行的,不会再直接操作字节流;

验证点包括:

是否以魔数0xCAFEBABE开头;

主、次版本号是否在当前虚拟机的处理范围内;

常量池中的常量类型是否有不被指出的常量类(检查常量tag标志);

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

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

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

(2)元数据验证

对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。

验证点包括:

这个类是否有父类(除了java.lang.Object之外,所有的类都应该有父类);

这个类的父类是否继承了不允许被继承的类(被final修饰的类);

如果这个类不是抽象类,是否继承了其父类或接口中要求实现的所有方法;

类中的字段、方法是否与父类产生矛盾(例如:覆盖了父类的final方法、不符合规则的方法重载,例如方法参数都一样,返回值类型却不同);

(3)字节码验证

通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;

对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件;

(4)符号引用验证

对自身以外(常量池中的各种符号引用)的信息进行匹配行校验。

3、Preparation准备(Linking连接的第二阶段)

为类的静态变量在方法区中分配内存,设定初始值;

不包括实例变量,实例变量是随着对象实例化之后和对象一起分配在java堆中;

如:

public static int value=3;           

在准备阶段过后初始值为0,而不是3。只有在初始化之后该内存中的值才会变成3.

java中所有基本数据类型的零值:

数据类型 零值

int  0
long  0L
short  (short) 0
char  ‘\u0000’
byte  (byte) 0
boolean  false
float  0.0f
double  0.0d
reference  null           

特殊情况:

public static final  int value = 3;           

编译时javac会为value生成ConstantValue属性,在准备阶段就会根据ConstantValue属性直接将value赋值为3;

4、Resolution解析(Linking连接的第三阶段)

将常量池中的符号引用替换为直接引用的过程;

顺序不定,有可能在初始化之后解析,根据需要判断是在类加载器加载时解析还是在使用前解析;

解析动作主要针对类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符;

示例代码

public class JVMClassResolutionTest {
  interface Interface0{
    int A=0;
  }
  interface Interface1 extends Interface0{
    int A=1;
  }
  interface Interface2{
    int A=2;
  }
  static class Parent implements Interface1{
    public static int A=3;
  }
  static class Son extends Parent implements Interface2{
    //public static int A=4;
  }
  public static void main(String[] args) {
    System.out.println(Son.A);
  }
}           

报错:

The field Son.A is ambiguous           

5、Initialization初始化

初始化阶段是执行类构造器<clinit>()方法的过程。为类的静态变量赋予真正的初始值。

<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,收集顺序根据源文件顺序决定。静态语句块只能访问定义在静态语句块之前的变量,如:

示例1

public class Test16 {
  static {
     i =1;
  }
  static int i =0;
}           

解析:代码正常编译通过,定义在静态语句块之后的变量,在静态语句中可以赋值;

示例二

public class Test16 {
  static {
     i =1;
     System.out.println("i===="+i); //代码编译报错Cannot reference a field before it is defined(非法向前引用)
  }
  static int i =0;
}           

解析:定义在静态语句块之后的变量,在静态语句中可以赋值,不可以访问;

示例三

public class Test16 {
  static {
     i =1;
  }
  static int i =0;
  static {
    System.out.println("i===="+i);
  }
}           

解析:定义在静态语句块之后的变量,在静态语句中可以赋值,在其之后可以访问;

类的加载顺序。示例如下:

public class ClassLoaderTest {
  public static void main(String[] args) {
        son sons=new son();
    }
}


class parent{
    private static  int a=1;
    private static  int b;
    private   int c=initc();
    static {
        b=1;
        System.out.println("1.父类静态代码块:赋值b成功");
        System.out.println("1.父类静态代码块:a的值"+a);
    }
    int initc(){
        System.out.println("3.父类成员变量赋值:---> c的值"+c);
        this.c=12;
        System.out.println("3.父类成员变量赋值:---> c的值"+c);
        return c;
    }
    public parent(){
        System.out.println("4.父类构造方式开始执行---> a:"+a+",b:"+b);
        System.out.println("4.父类构造方式开始执行---> c:"+c);
    }
}


class son extends parent{
    private static  int sa=1;
    private static  int sb;
    private   int sc=initc2();
    static {
        sb=1;
        System.out.println("2.子类静态代码块:赋值sb成功");
        System.out.println("2.子类静态代码块:sa的值"+sa);
    }
    int initc2(){
        System.out.println("5.子类成员变量赋值--->:sc的值"+sc);
        this.sc=12;
        return sc;
    }
    public son(){
        System.out.println("6.子类构造方式开始执行---> sa:"+sa+",sb:"+sb);
        System.out.println("6.子类构造方式开始执行---> sc:"+sc);
    }
}           

运行结果

1.父类静态代码块:赋值b成功
1.父类静态代码块:a的值1
2.子类静态代码块:赋值sb成功
2.子类静态代码块:sa的值1
3.父类成员变量赋值:---> c的值0
3.父类成员变量赋值:---> c的值12
4.父类构造方式开始执行---> a:1,b:1
4.父类构造方式开始执行---> c:12
5.子类成员变量赋值--->:sc的值0
6.子类构造方式开始执行---> sa:1,sb:1
6.子类构造方式开始执行---> sc:12           

解析:父类静态代码块>子类静态代码块>父类成员变量赋值>父类构造方式>子类成员变量赋值>子类构造器

即验证了静态代码块优先初始化,有验证了在调用子类时,会优先初始化其父类。

<clinit>()方法对于类和接口来说不是必须的,类中没有静态语句块和对变量的赋值操作,就不会生成<clinit>()方法;相同的接口中没有对变量的赋值操作也不会生成<clinit>()方法;

接口和类的不同在于,子类执行<clinit>()方法之前其父类会先执行<clinit>()方法;子接口执行<clinit>()方法之前不会执行其父类接口的<clinit>()方法;

<clinit>()方法在多线程环境中会被加锁、要求同步,多个线程同时初始化一个类,只有一个线程执行<clinit>()方法,其他线程阻塞等待。若<clinit>()方法执行时间过长会造成进行阻塞。实例:

public class Test01 {
public static void main(String[] args) {
  Runnable script = new Runnable() {
    public void run() {
      System.out.println(Thread.currentThread()+"start");
      JVMDeadLoopTest dlt = new JVMDeadLoopTest();
      System.out.println(Thread.currentThread()+"run over");
    }
  };
  JVMDeadLoopTest dlt1 = new JVMDeadLoopTest();
  Thread thread1 = new Thread(script);
  Thread thread2 = new Thread(script);
  thread1.start();
  System.out.println("----");
  thread2.start();
}           

```

public class JVMDeadLoopTest {
    static {
      if(true) {
        System.out.println(Thread.currentThread()+"init JVMDeadLoopTest");
        //int i=0;
        while(true) {
          //i = i+1;
          //if(i>3) { break; }
        }
      }
    }
           

执行结果:

Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]init JVMDeadLoopTest           

分析:线程一直在初始化JVMDeadLoopTest类中的静态块部分,所以其他两个线程一直处于阻塞状态;

放开代码中注释掉的部分,执行结果:

Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]init JVMDeadLoopTest
Thread[Thread-0,5,main]run over
Thread[Thread-1,5,main]run over           

未出现线程阻塞的情况

6、Using使用

7、Unloading卸载

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

一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机本身会始终引用这些类加载器,而这类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的;由用户自定义的类加载器所加载的类是可以被卸载的

8、拓展:类实例化

类的生命周期除了加载、连接、初始化之外,还有类实例化、垃圾回收和对象终结

为新的对象分配内存;

为实例变量赋予默认值;

为实例变量赋予正确的初始值;

java编译器为它编译的每一个类至少生成一个实例初始化方法,在java的class文件中,这个实例初始化方法被称为。针对源代码中的每一个类的构造方法,java编译器都会产生一个

二、类的加载、连接、初始化代码示例

1、类的使用方式

java程序对类的使用分两种

主动使用

创建类的实例

调用类的静态方法

访问某个类或接口的静态变量

初始化类的子类

反射

java虚拟机启动时被标明为启动类的类(包含main方法)

JDK1.7开始提供的动态语言支持(java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化);

被动使用

除了主动使用的其中情况外,其他使用java类的方法都看做是对类的被动使用,都不会导致类的初始化;

2、代码理解

示例一:类的加载、连接和初始化过程

代码一

public class JVMTest01 {
  public static void main(String[] args) {
    System.out.println(Child01.str);
  }
}


class Father01{
  public static String str = "我是父类01静态变量!";
  static {
    System.out.println("Father01 static block");
  }
}           
class Child01 extends Father01{


  static {
    System.out.println("Child01 static block");
  }
}           

运行结果:

Father01 static block
我是父类01静态变量!           

代码二

public class JVMTest01 {
  public static void main(String[] args) {
    System.out.println(Child01.str);
  }
}


class Father01{
  public static String str = "我是父类01静态变量!";
  static {
    System.out.println("Father01 static block");
  }
}


class Child01 extends Father01{
  
  public static String str = "我是子类类01静态变量!";
  
  static {
    System.out.println("Child01 static block");
  }
}           

运行结果:

Father01 static block
Child01 static block           

我是子类类01静态变量!

分析:

代码一中,我们通过子类调用父类中的str,这个str是在父类中被定义的,对Father01主动使用,没有主动使用Child01,因此Child01中的静态代码没有执行,父类中的静态代码执行了。对于静态字段来说,只有直接定义了该字段的类才会被初始化。

代码二中,对Child01主动使用;根据主动使用的7中情况,调用类的子类时,其所有的父类都会被先初始化,所以Father01会被初始化。当一个类初始化时,要求其父类全部已经初始化完毕。

以上验证的是类的初始化情况,那么如何验证类的加载情况呢,可以通过在启动的时候配置虚拟机参数:-XX:+TraceClassLoading查看

运行代码一运行结果:

[Opened C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]
[Loaded java.io.Serializable from C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]
......
[Loaded JVMTest.Father01 from file:/D:/work-space/TestCoJava/bin/]
[Loaded JVMTest.Child01 from file:/D:/work-space/TestCoJava/bin/]
Father01 static block
我是父类01静态变量!
[Loaded java.lang.Shutdown from C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]           

可以看见控制台打印了very多的日志,第一个加载的是java.lang.Object类(不管加载哪个类,他的父类一定是Object类),后面是加载的一系列jdk的类,他们都位于rt包下。往下查看,可以看见Loaded classloader.Child01,说明即使没有初始化Child01,但是程序依然加载了Child01类。

拓展

所有的JVM参数都是以-XX:开头的;

如果形式是:-XX:+,表示开启option选项;

如果形式是:-XX:-,表示关闭option选项;

如果形式是:-XX:=,表示将option选项设置的值为value;

示例二:常量的本质含义

public class JVMTest02 {
  public static void main(String[] args) {
    System.out.println(Father02.str);
  }
}


class Father02{
  public static final String str="我是父类的静态常量!";
  
  static {
    System.out.println("Father02 static block");
  }
      
}           

运行结果:

我是父类的静态常量!

1

分析:可以看到这段代码并没有初始化Father02类。这是因为final表示的是一个常量,在编译阶段常量就被存入调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。在这段代码中,str被存入到JVMTest02的常量池中,之后JVMTest02和Father02没有任何关系,甚至可以删除Father的class文件。

反编译JVMTest02:

Compiled from "JVMTest02.java"
public class classloader.JVMTest02 {
  public classloader.JVMTest02();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return


  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String 我是父类的静态常量!
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}           

分析:

第一块是JVMTest02的构造方法,第二块是我们要看的main方法。可以看见“3: ldc #4 // String 我是父类的静态常量!”,此时这个值已经是“我是父类的静态常量!”了,而不是Father02.str,证实了上面说的在编译阶段常量就已经被存入调用常量的方法所在的类的常量池中了。

拓展(助记符):

Idc:表示int、float或String类型的常量值常量池中推送到栈顶;

bipush:表示将单字节(-128至127)的常量推送至栈顶;

sipush:表示将短整型(-32768至32767)的常量推送至栈顶;

=iconst_1:表示将int类型的1推送至栈顶(这类助记符只有iconst——m1-iconst_5七个;==

示例三:编译器常量与运行期常量的区别

public class JVMTest03 {
  public static void main(String[] args) {
    System.out.println(Father03.str);
  }
}


class Father03{
  public static final String str= UUID.randomUUID().toString();
  
  static {
    System.out.println("Father03 static block");
  }
      
}           

运行结果:

Father03 static block
2d38fe6a-9b12-4d6c-92d7-a015e24eb198           

分析:

本代码与示例二的区别在于str的值是在运行时确认的,而不是编译时就确定好的,属于运行期常量,而不是编译期常量。当一个常量的值并非编译期间确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,导致这个类被初始化。

示例四:数组创建的本质

代码一:

public class JVMTest04 {
  public static void main(String[] args) {
    Father04 father04_1 = new Father04();
    System.out.println("--------------");
    Father04 father04_2 = new Father04();
  }
}
class Father04{
  static {
    System.out.println("Father04 static block");
  }
}           

运行结果:

Father04 static block
--------------           

分析:

创建类的实例时,会初始化类;

所有的java虚拟机的实现,必须在每个类或接口被java程序“首次主动使用”时才初始化他们

代码二:

public class JVMTest04 {
  public static void main(String[] args) {
    
    Father04[] father04s = new Father04[1];
    System.out.println(father04s.getClass());
  }
}
class Father04{
  static {
    System.out.println("Father04 static block");
  }
}           

运行结果:

class [LJVMTest.Father04;           

分析:

创建数组对象不在主动使用的7中情况内,所以不会初始化Father04;

打印father04s的类型为[LJVMTest.Father04,这是虚拟机在运行期生成的。-> 对于数组示例来说,其类型是有JVM在运行期动态生成的,表示为[LJVMTest.Father04这种形式,动态生成的类型,其父类就是Object。

对于数组来说,javaDoc经常讲构成数组的元素为Component,实际上就是将数组降低一个维度后的类型;

反编译一下:

public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: anewarray     #2                  // class classloader/Father04
       4: astore_1
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: aload_1
       9: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      12: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      15: return           

分析:

anewarray:表示创建一个引用类型(如类、接口、数组)的数组,并将其引用值值压入栈顶

newarray:表示创建一个指定的原始类型(如int、float、char等)的数组,并将其引用值压入栈顶;

示例五:接口的加载与初始化

代码一

public class JVMTest05 {


  public static void main(String[] args) {
    System.out.println(Child05.i);
    System.out.println(Child05.j);
  }
}


interface Father05{
  int i = 5;
}


interface Child05 extends Father05{
  int j = 6;
}           

编译后删除Father05.class文件和Child05.class文件,运行结果

5
6           

分析

接口中定义的常量本身就是public、static、final的;

接口中的常量在编译阶段已经存在于JVMTest05类的常量池中了,此时Father05和Child05都不会被加载;

代码二

public class JVMTest05 {


  public static void main(String[] args) {
    System.out.println(Child05.j);
  }
}


interface Father05{
  int i = 5;
}


interface Child05 extends Father05{
  //取0~8之间的随机数
  int j = new Random().nextInt(8);
}           

运行结果:

4
1           

把Father05.class文件删除,运行结果:

Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Father05
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at classloader.Test05.main(Test05.java:15)
Caused by: java.lang.ClassNotFoundException: classloader.Father05
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 13 more           

分析:运行时的常量,

只有在真正使用到父接口的时候(如引用接口中定义的常量时),才会加载初始化Child05,并且初始化Child05的父类Father05.class,Father05.class已经被删除,所以报错。

代码三:

public class JVMTest06 {


  public static void main(String[] args) {
    System.out.println(Child06.j);
  }
}


interface Father06{
  Thread thread = new Thread(){
    {
      System.out.println("Father05 code block");
    }
  };
}


class Child06 implements Father06{
  public static int j = 8;
}           

运行结果

8           

分析:在初始化一个类时,并不会先初始化他所实现的接口

代码四:

public class JVMTest07 {
    public static void main(String[] args) {
        System.out.println(Father07.thread);
    }
}


interface GrandFather {
    Thread thread = new Thread() {
        {
            System.out.println("GrandFather code block");
        }
    };
}


interface Father07 extends GrandFather{
    Thread thread = new Thread() {
        {
            System.out.println("Father07 code block");
        }
    };
}           

运行结果:

Father07 code block
Thread[Thread-0,5,main]           

分析:初始化一个接口时,并不会初始化他的父接口

示例六:类加载器的准备阶段和初始化阶段

代码一

public class JVMTest08 {


  public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
    
    System.out.println("i:" +Singleton.i);
    System.out.println("j:" +Singleton.j);
  }
}           
class Singleton{
  public static int i;
  
  public static int j = 0;
  
  private static Singleton singleton = new Singleton();
  
  private Singleton() {
    i++;
    j++;
  }


  public static Singleton getInstance() {
    return singleton;
  }
}           

运行结果:

i:1
j:1           

分析:首先Singleton.getInstance()调用Singleton的getInstance方法,getInstance返回singleton实例,singleton的实例是==new Singleton();==出来的,因此调用了自定义的私有构造方法。在调用构造方法之前,给静态变量赋值,i默认为0,j显示的赋值为0,经过构造函数之后,值都为1。

代码二

public class JVMTest08 {


  public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
    
    System.out.println("i:" +Singleton.i);
    System.out.println("j:" +Singleton.j);
  }
}


class Singleton{
  public static int i;
  
  private static Singleton singleton = new Singleton();
  
  private Singleton() {
    i++;
    j++;
  }


  public static int j = 0;
  
  public static Singleton getInstance() {
    return singleton;
  }
}           

运行结果:

i:1
j:0           

分析:程序主动使用了Singleton类,准备阶段对类的静态变量分配内存,赋予默认值,下面给出类在连接及初始化阶段常量的值的变化

i :0
singleton:null
j :0
getInstance:初始化
i:0
singleton:调用构造函数
i:1
j:1
j:0【覆盖了之前的1】           

因此返回值i的值为1,j的值为0;

三、类加载器

1、类加载器体系结构

类加载器分为两种,一种是java虚拟机自带的类加载器,一种是用户自定义的类加载器。

JVM- 类的加载过程、类加载器,看这就够了。

(1)java虚拟机再带的类加载器

BootStrapClassLoader启动类加载器

没有父加载器;依赖底层操作系统;

没有继承java.lang.ClassLoader类,由C++编写;

负责加载系统类(指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类,<JAVA_HOME>\lib目录中,或者-XbootclassPath参数指定路径下的javaAIP的核心类库,如java.lang.等;

Bootstrap loader所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载Launcher.java中的ExtClassLoader(扩展类装载器,负责加载扩展类,就是继承类和实现类),并设定其parent为null,代表其父类加载器为BootStrapLoader

ExtClassLoader扩展类加载器

由sun.misc.Launcher$ExtClassLoader实现,继承java.lang.ClassLoader,其父加载器为BootStrapClassLoader启动类装载器;

负责加载<JAVA_HOME>\lib\ext目录下、java.ext.dirs系统变量指定路径中的的类库;如果用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载;

负责加载扩展类,就是继承类和实现类

AppClassLoader应用程序类加载器(系统类加载器)

由sun.misc.Launcher$AppClassLoader实现,继承java.lang.ClassLoader,,父加载器为ExtClassLoader扩展类装载器;

这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,故又称系统加载类;

应用程序中默认的类加载器,是用户自定义类加载器的默认父加载器;

从环境变量classpath或者系统或者系统属性java.class.path所指定的目录中加载类

(2)用户自定义的类加载

默认APPClassLoader为父加载器,java.lang.ClassLoader的子类;

用户可以定义类的加载方式;

2、类与加载器

对于任意一个类,类和加载这个类的类加载器一同确立了其在虚拟机中的唯一性。每个类加载器在虚拟机中都有自己独立的命名空间。同一个类可能存在于多个命名空间。只要类的加载器不一样,同一个类也不相等,这里的相等包括equals方法、isAssignableFrom()方法、isInstance()方法的返回结果。

代码待补充

拓展:

-命名空间:java虚拟机为每一个类装载器维护一个唯一标识的命名空间。一个java程序可以多次装载具有同一个权限命名的多个类。java虚拟机要确定这“多个类”的唯一性,因此,当多个类装载器都装载了同名的类时,为了唯一的标识这个类,还要在类名前加上装载该类的类装载器的标识(指出了类所位于的命名空间)。

命名空间有助于安全的实现,因为你可以有效地在装入了不同命名空间的类之间设置一个防护罩。在Java虚拟机中,在同一个命名空间内的类可以直接进行交 互,而不同的命名空间中的类甚至不能察觉彼此的存在,除非显式地提供了允许它们进行交互的机制。一旦加载后,如果一个恶意的类被赋予权限访问其他虚拟机加 载的当前类,它就可以潜在地知道一些它不应该知道的信息,或者干扰程序的正常运行。

3、双亲委派机制

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。

(1)工作原理

-类加载器收到类加载请求,不会先自己去加载,而是请求委托给父加载器加载,如果还存在上一层,则继续向上委托,最终到达顶层的BootStrapClassLoader启动类加载器。只有当父类无法完成时,子类才会尝试自己去加载;示例如下:

package test;   
  import java.net.URL;   
  import java.net.URLClassLoader;   
  public class ClassLoaderTest {   
      private static int count = -1;   
      public static void testClassLoader(Object obj) {   
          if (count < 0 && obj == null) {   
             System.out.println("Input object is NULL";   
             return;   
         }   
          ClassLoader cl = null;   
          if (obj != null && !(obj instanceof ClassLoader)) {   
              cl = obj.getClass().getClassLoader();   
          } else if (obj != null) {   
              cl = (ClassLoader) obj;   
          }   
          count++;   
          String parent = "";   
          for (int i = 0; i < count; i++) {   
              parent += "Parent ";   
          }   
          if (cl != null) {   
              System.out.println(   
                  parent + "ClassLoader name = " + cl.getClass().getName());   
              testClassLoader(cl.getParent());   
          } else {   
             System.out.println(   
                  parent + "ClassLoader name = BootstrapClassLoader";   
             count = -1;   
          }   
     }   
      public static void main(String[] args) {   
          URL[] urls = new URL[1];   
          URLClassLoader urlLoader = new URLClassLoader(urls);   
          ClassLoaderTest.testClassLoader(urlLoader);   
     }   
}           

以上例程的输出为:

ClassLoader name = java.net.URLClassLoader
Parent ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent Parent ClassLoader name = BootstrapClassLoader           

类装载器请求过程

以上例程1为例.将main方法改为:

ClassLoaderTest tc = new ClassLoaderTest();
ClassLoaderTest.testClassLoader(tc);           

输出为:

ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent ClassLoader name = BootstrapClassLoader           

程序运行过程中,类路径类装载器发出一个装载ClassLoaderTest类的请求, 类路径类装载器必须首先询问它的Parent—扩展类装载器 —来查找并装载这个类,同样扩展类装载器首先询问启动类装载器。由于ClassLoaderTest不是 Java API(JAVA_HOME\jre\lib)中的类,也不在已安装扩展路径(JAVA_HOME\jre\lib\ext)上,这两类装载器 都将返回而不会提供一个名为ClassLoaderTest的已装载类给类路径类装载器。类路径类装载器只能以它自己的方式来装载 ClassLoaderTest,它会从当前类路径上下载这个类。这样,ClassLoaderTest就可以在应用程序后面的执行中发挥作用。

在上例中,ClassLoaderTest类的testClassLoader方法被首次调用,该方法引用了Java API中的类 java.lang.String。Java虚拟机会请求装载ClassLoaderTest类的类路径类装载器来装载 java.lang.String。就像前面一样,类路径类装载器首先将请求传递给它的Parent类装载器,然后这个请求一路被委托到启动类装载器。但 是,启动类装载器可以将java.lang.String类返回给类路径类装载器,因为它可以找到这个类,这样扩展类装载器就不必在已安装扩展路径中查找 这个类,类路径类装载器也不必在类路径中查找这个类。扩展类装载器和类路径类装载器仅需要返回由启动类装载器返回的类java.lang.String。从这一刻开始,不管何时ClassLoaderTest类引用了名为java.lang.String的类,虚拟机就可以直接使用这个 java.lang.String类了。

(2)一个经典的实例说明

package java.lang;  
  
public class String {  
    public static void main(String[] args){  
          
    }  
}           

大家发现什么不同了吗?对了,我们写了一个与JDK中String一模一样的类,连包java.lang都一样,唯一不同的是我们自定义的String类有一个main函数。我们来运行一下:

java.lang.NoSuchMethodError: main
    Exception in thread "main"           

这是为什么? 我们的String类不是明明有main方法吗?

其实联系我们上面讲到的双亲委托模型,我们就能解释这个问题了。

运行这段代码,JVM会首先创建一个自定义类加载器,不妨叫做AppClassLoader,并把这个加载器链接到委托链中:AppClassLoader -> ExtClassLoader -> BootstrapLoader。

然后AppClassLoader会将加载java.lang.String的请求委托给ExtClassLoader,而 ExtClassLoader又会委托给最后的启动类加载器BootstrapLoader。

启动类加载器BootstrapLoader只能加载JAVA_HOME\jre\lib中的class类(即J2SE API),问题是标准API中确实有一个java.lang.String(注意,这个类和我们自定义的类是完全两个类)。BootstrapLoader以为找到了这个类,毫不犹豫的加载了j2se api中的java.lang.String。

最后出现上面的加载错误(注意不是异常,是错误,JVM退出),因为API中的String类是没有main方法的。

结论:我们当然可以自定义一个和API完全一样的类,但是由于双亲委托模型,使得我们不可能加载上我们自定义的这样一个类。所以J2SE规范中希望我们自定义的包有自己唯一的特色(网络域名)。还有一点,这种加载器原理使得JVM更加安全的运行程序,因为黑客很难随意的替代掉API中的代码了。

继续阅读