JVM 类加载机制1

1.什么是类加载?

在JVM虚拟机实现规范中,通过ClassLoader类加载器把*.class字节码文件(文件流)加载到内存,并对字节码文件内容进行验证、准备、解析和初始化,最终形成可以被虚拟机直接使用的java.lang.Class对象,这个过程被称作类加载。

类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。

2.类的生命周期

 

85c72c471ad853002c8d929fd90e5444.png

类的生命周期包括以下 7 个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

结束类生命周期的几种场景:

  • 执行System.exit()方法
  • 程序正常执行结束
  • 程序执行中遇到了异常或错误而异常终止
  • 操作系统出现错误或强制结束程序而导致JVM虚拟机进程终止

3.类加载过程

类加载过程包含:加载、验证、准备、解析和初始化 ,一共包括5 个阶段。

可以通过一句谐音来记忆:“家宴准备了西式菜” = 家 (加载) 宴 (验证) 准备 (准备) 了西 (解析) 式 (初始化) 菜

3.1加载

在加载阶段,JVM主要完成以下3件事:

  • 通过类的完全限定名称获取定义该类的*.class字节码文件的二进制字节流。
  • 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
  • 在内存中生成一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。

由于JVM虚拟机对加载*.class字节码文件的来源并未做限制,所以出现了以下的*.class字节码文件加载方式:

1.本地文件系统直接读取;

2.从网络中通过服务器响应读取。例如:Web Applet技术;

3.从 JAR、EAR、WAR等压缩文件中读取;

4.运行时通过动态代理技术生成字节码文件。例如:在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。

5.由其他文件或容器生成。例如:由tomcat将 *.jsp 文件翻译成*.java文件后,编译生成对应的 *.class字节码文件。

在加载阶段完成之后,*.class字节码文件的类信息数据就会存储在元空间,同时在JVM虚拟机堆区生成一个该类的Class对象。

3.2验证

在验证阶段,JVM主要确保 *.class字节码文件中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的安全。

验证阶段会完成下面四个阶段的检验:

1.文件格式验证:验证字节流是否符合*.class字节码文件格式的规范,且能被当前版本的虚拟机处理。

  • 是否以魔数 0xCAFEBABE开头
  • 主、次版本号是否在当前虚拟机处理范围之内
  • 常量池的常量中是否有不被支持的常量类型(检査常量tag 标志)
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合装型的常量
  • CONSTANT_Utf8_info型的常量中是否有不符合 UTF8编码的数据
  • *.class 文件中各个部分及文件本身是否有被删除的或附加的其他信息

 

2.元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

  • 这个类是否有父类(除了 java.lang.0bject之外,所有的类都应当有父类)
  • 这个类的父类是否继承了不允许被继承的类(被finaI修饰的类)
  • 如果这个类不是抽象类, 是否实現了其父类或接口之中要求实现的所有方法
  • 类中的字段、 方法是否与父类产生了矛盾,例如:覆盖了父类的final字段。出现不符合规则的方法重载,例如:方法参数都一致, 但返回值类型却不同等。

 

3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  • 保证跳转指令不会跳转到方法体以外的字节码指令上
  • 保证方法体中的类型转换是有效的,例如:可以把子类对象赋值给父类数据装型,这是安全的;但把父类对象意赋值给子类数据类型,甚至把对象赋值给毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
  • 保证任意时刻操作数栈的数据装型与指令代码序列都能配合工作,例如:不会出现在操作栈中放置了一个 int类型的数据, 使用时却按long类型来加载入本地变量表中

 

4.符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候 , 这个转化动作将在连接的第三个阶段——解析阶段中发生。确保解析动作能正常执行。

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

为什么需要验证?

Java语言本身是相对安全的语言,但*.class字节码文件并不一定要求用Java源码编译而来,可以使用任何途径, 甚至可用十六进制编译器直接编写来产生*.class字节码文件。

类的加载是JVM针对*.class字节码文件的读取加载机制,所以虚拟机如果不检査输入的字节流,可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

另外,通过类加载机制的验证环节,可以增强解释器的运行期执行性能。因为,解释器在运行期间无需再对每条执行指令进行检查。

3.3准备

  • 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存。
  • 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
  • 初始值一般为 0 值。

 

例如:下面的类变量 value 被初始化为 0 而不是 123。

public static int value = 123;
  • 如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。

例如:下面的常量 value 被初始化为 123 而不是 0。

public static final int value = 123;

3.4.解析

将常量池的符号引用替换为直接引用。

3.5初始化

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>()方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

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

例如:以下代码中静态变量i只能赋值,不能访问,因为i定义在静态代码块的后面。

public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:

static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

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

<clinit>线程安全

虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中,该阻塞非常隐蔽,几乎不会被察觉。

4.类加载的时机

4.1主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列六种情况必须对类进行加载:

  • 当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,比如 new 一个对象,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当 jvm 执行 new指令时会加载类。即:当程序创建一个类的实例对象。
    • 当 jvm 执行 getstatic指令时会加载类。即:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当 jvm 执行 putstatic指令时会加载类。即:程序给类的静态变量赋值。
    • 当 jvm 执行 invokestatic指令时会加载类。即:程序调用类的静态方法。
  • 使用 java.lang.reflect包的方法对类进行反射调用时如 Class.forname("..."), 或newInstance() 等等。如果类没初始化,需要触发类的加载。
  • 加载一个类,如果其父类还未加载,则先触发该父类的加载。
  • 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main() 方法的类),虚拟机会先加载这个类。
  • 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。

4.2被动引用

除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用。

被动引用的常见例子包括:

  • 通过子类引用父类的静态字段,不会导致子类加载。
System.out.println(SubClass.value);  
// value 字段在 SubClass类的父类中定义
  • 通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
SuperClass[] sca = new SuperClass[10];
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。
System.out.println(ConstClass.HELLOWORLD);

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值