Java虚拟机之类加载子系统


在这里插入图片描述

类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

这一步骤,对应于上图中黄色标注的类加载子系统。它就是负责将Class文件从外部加载进来,加载进来的类信息被存放在方法区

而类加载子系统是通过类加载器 去完成这一操作的

类加载器

从Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且继承自抽象类java.lang.ClassLoader

系统提供的3种类加载器:

  • 启动类加载器 Bootstrap ClassLoader
    • 这个类加载器负责将存放在<JAVA_HOME>\lib目录中的或被-Xbootclasspath参数指定的路径中的类库加载到虚拟机内存中
    • 启动类加载器无法被Java程序直接引用
  • 扩展类加载器(Extension ClassLoader)
    • 负责加载<JAVA_HOME>\lib\ext目录中或被java.ext.dirs所指定的路径中的所有类库,开发者可直接使用
  • 应用程序类加载器(Application ClassLoader)
    • 也被称为系统类加载器
    • 负责加载用户类路径上所指定的类库。如果没有自定义类加载器,则这个就是程序中默认的类加载器

示例:查看类加载器

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("systemClassLoader>>>>>" + systemClassLoader);
        ClassLoader extensionClassLoader = systemClassLoader.getParent();
        System.out.println("extensionClassLoader>>>>>" + extensionClassLoader);
        ClassLoader bootstrapClassLoader = extensionClassLoader.getParent();
        System.out.println("bootstrapClassLoader>>>>>" + bootstrapClassLoader);
    }
}

输出

systemClassLoader>>>>>sun.misc.Launcher$AppClassLoader@18b4aac2
extensionClassLoader>>>>>sun.misc.Launcher$ExtClassLoader@39a054a5
bootstrapClassLoader>>>>>null
双亲委派模型

从上面的实例可以看出,类加载器之间似乎存在层次关系。没错,类加载器之间的这种层次关系,就被称为类加载器的双亲委派模型

在这里插入图片描述

双亲委派要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器

双亲委派的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中未找到所需的类),子类才尝试去自己加载

双亲委派机制的优势:

  • 避免类被重复加载
  • 保障核心API,防止被随意篡改

示例1:自定义String类

在这里插入图片描述

public class String {
    static {
        System.out.println("自定义String类");
    }
}
public class Test {
    public static void main(String[] args) {
        String str=new String();
        System.out.println("test");
    }
}

若自定义java.lang.String可以被正常加载的话,那么输出结果应该是

自定义String类
test

实际上输出结果是

test

这也就验证了双亲委派的机制

这种对Java核心源代码的保护 也会称为沙箱安全机制

破坏双亲委派模型
类加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中,验证、准备、解析这3个部分统称为连接(Linking)

在这里插入图片描述

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这个顺序按部就班地开始,而解析阶段就不一定了:它在某些情况下可以在初始化阶段之后再开始。

至于什么时候开始类加载过程的第一个阶段,Java虚拟机规范并没有强制约束,这点可以交给虚拟机的具体实现来自由把握。

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

class文件的获取方式:

  • 从zip包中读取
  • 从网络中获取,典型场景:Web Applet
  • 运行时计算生成
  • 从数据库读取
连接

连接分为三个子阶段:验证>准备>解析

验证

目的在于确保Class文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全

主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

文件格式验证

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

  • 是否已模数0xCAFEBABE开头
  • 主、次版本号是否在当前虚拟机处理范围之内
  • 常量池的常量是否有不被支持的常量类型
cafe babe 0000 0034 0012 0a00 0500 0e07
000f 0a00 0200 0e07 0010 0700 1101 0006
3c69 6e69 743e 0100 0328 2956 0100 0443
6f64 6501 000f 4c69 6e65 4e75 6d62 6572
5461 626c 6501 0004 6d61 696e 0100 1628
5b4c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 3b29 5601 000a 536f 7572 6365 4669
6c65 0100 0f53 7472 696e 6754 6573 742e
6a61 7661 0c00 0600 0701 0010 6a61 7661
2f6c 616e 672f 5374 7269 6e67 0100 1d63
6f6d 2f77 6f6a 6975 7368 6977 6f2f 6a76
6d2f 5374 7269 6e67 5465 7374 0100 106a
6176 612f 6c61 6e67 2f4f 626a 6563 7400
2100 0400 0500 0000 0000 0200 0100 0600
0700 0100 0800 0000 1d00 0100 0100 0000
052a b700 01b1 0000 0001 0009 0000 0006
0001 0000 0008 0009 000a 000b 0001 0008
0000 0025 0002 0002 0000 0009 bb00 0259
b700 034c b100 0000 0100 0900 0000 0a00
0200 0000 0b00 0800 0c00 0100 0c00 0000
0200 0d

如以上16进制字节码,开头内容为0xcafebabe 并且虚拟机版本号 0x00000034==>52 刚好对应JDK8

元数据验证

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

  • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
  • 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
  • 类中的字段、方法是否与父类产生矛盾

字节码验证

主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二个阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件

符合引用验证

这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段发生。符号引用验证可以看做是对类自身以外的信息进行匹配性校验

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符合引用中的类、字段、方法的访问性(private/protected/public/default)是否可被当前类访问
准备

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

这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

这里的类变量不包含被final修饰的类变量,被final修饰的类变量在准备阶段就被初始化为其实际值了

public static final int value=123;
public static int number=123;

在准备阶段,number属性被赋予初始值0,而value属性被初始化为123

基本数据类型的零值

数据类型零值数据类型零值
int0booleanfalse
long0Lfloat0.0f
short(short)0double0.0d
char‘\u0000’referencenull
Byte(byte)0
解析

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

  • 符号引用:以一组符号来描述所引用的目标,个人理解是用文本的形式描述要引用的类、方法、类型等,比如java/lang/String
  • 直接引用:就是在类运行过程中引用到的真正的类、方法等

符号引用举例

   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Class              #15            // java/lang/String
   #3 = Methodref          #2.#14         // java/lang/String."<init>":()V
   #4 = Class              #16            // com/wojiushiwo/jvm/StringTest
   #5 = Class              #17            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               StringTest.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Utf8               java/lang/String

初始化

到了初始化阶段,才真正开始执行类中定义的Java程序

初始化阶段,是执行类构造器<clinit>()方法的过程

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序来决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

举例说明()

public class StringTest {

    private static int num = 1;
    private static final String str = "哈哈";
    private int value = 11;

    static {
        System.out.println("这个是StringTest静态代码块");
    }

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

使用jclasslib查看<clinit>()方法的字节码

 0 iconst_1
 1 putstatic #3 <com/wojiushiwo/jvm/StringTest.num>
 4 getstatic #4 <java/lang/System.out>
 7 ldc #5 <这个是StringTest静态代码块>
 9 invokevirtual #6 <java/io/PrintStream.println>
12 return

其中#3、#4、#5、#6对应常量池中的符号引用

 #3 = Fieldref           #7.#27         // com/wojiushiwo/jvm/StringTest.num:I
 #4 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
 #5 = String             #30            // 这个是StringTest静态代码块
 #6 = Methodref          #31.#32        // java/io/PrintStream.println:(Ljava/lang/String;)V

可以看到静态变量num及静态代码块中的语句被放到了<clinit>方法,静态常量没有被放进去

<clinit>()方法中的指令按语句在源文件中出现的顺序执行

public class Test {
    private static int num;
    static {
        num=1;
        i=20;
    }
    private static int i=1;

    public static void main(String[] args) {
        System.out.println(Test.i);//1
        System.out.println(Test.num);//1
    }
}

可以通过查看<clinit>()字节码发现 i字段的赋值确实与其在java类中出现的顺序有关

 0 iconst_1
 1 putstatic #5 <com/wojiushiwo/jvm/Test.num>
 4 bipush 20
 6 putstatic #3 <com/wojiushiwo/jvm/Test.i>
 9 iconst_1
10 putstatic #3 <com/wojiushiwo/jvm/Test.i>
13 return

举例:非法前向引用变量

public class Test {
    private static int num;
    static {
        num=1;
        //可以赋值 但是不能访问 这里报错非法前向引用
        i=0;
        System.out.println(i);
    }
    private static int i=1;
}

若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕

public class Test {
    static class Father{
        public static int num=1;
        static {
            num=2;
        }
    }
    static class Son extends Father{
        public static int i=num;
    }

    public static void main(String[] args) {
        System.out.println(Son.i);//2
    }
}

如上代码,加载流程如下:

  • 首先,执行 main( ) 方法需要加载 Test类
  • 获取 Son.i 静态变量,需要加载 Son 类
  • Son 类的父类是 Father 类,所以需要先执行 Father 类的加载,再执行 Son 类的加载

虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁

public class Test {
    static class SubClass {
        static {

            System.out.println(Thread.currentThread().getName() + "正在初始化当前类");
            if (1 == 1) {
                while(true){}
            }
        }
    }

    public static void main(String[] args) {
        Runnable r=()->{
            System.out.println(Thread.currentThread().getName() + "开始");
            SubClass dead = new SubClass();
            System.out.println(Thread.currentThread().getName() + "结束");
        };
        Thread t1 = new Thread(r, "线程1");
        Thread t2 = new Thread(r, "线程2");

        t1.start();
        t2.start();
    }
}

为了模拟同步现象,将SubClass静态代码块自旋了。

通过运行发现,运行会被卡住,从而印证结论

以下5种情况必须立即对类进行初始化

  • 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(静态常量除外)、调用一个类的静态方法时
  • 使用java.lang.reflect包的方法对类进行反射调用时,若类没有进行过初始化,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发器父类的初始化
  • 当虚拟机启动时,用于需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
打破双亲委派机制

以加载MySQL 驱动为例,说明为什么要有打破双亲委派的情况发生?

Class.forName("com.mysql.jdbc.Driver");

JDK 声明了驱动接口Driver,而具体的数据库厂商给出不同的实现。

加载流程:

Class.forName()默认使用当前类的ClassLoader,即Bootstrap ClassLoader,而Bootstrap ClassLoader加载范围内中找不到类com.mysql.jdbc.Driver,该类存在于Application ClassLoadler的加载范围。

那要怎么在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类?

JDK提供了两种方式:Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向Application ClassLoader,它们能加载classpath中的类

这便是打破了双亲委派机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值