java虚拟机之类加载机制

前言

我们都知道,java编译的结果是字节码,不是本地机器码,这也是java跨平台的一大表现。既然java编译后是字节码,那么就不能实际地在本地(物理机器)运行。java字节码运行在jvm虚拟机上面,既然这样,那么jvm虚拟机是如何加载读取一个类的信息的呢?

我们平时写完java代码生成的是class文件。最后在运行的时候,虚拟机把描述类的信息从class文件加载到内存,然后再进行校验、解析和初始化等过程,最后形成可以被java虚拟机“读懂”的java类型。那么从class——>java虚拟机能“读懂”的java类型就是本文要讲解的内容。

类加载的时机

既然说到了类加载,那么到底什么时候jvm才会加载某个类呢?

其实这个问题简单,肯定是运行时需要这个类的时候才会去加载啦!

具体来说,类加载分为几个阶段的:

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

那么到底什么时候才开始加载类的第一个阶段?加载?

其实这个问题在不同虚拟机上面实现是不一样的,这个可以由虚拟机自己把握就行。不过java虚拟机规范中明确了当遇到下面5种情况的时候,必须初始化,此处的初始化是在加载,验证,准备,解析后的初始化,所以这几个阶段应该在其之前完成:

  • 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行初始化,则先初始化。这4个字节码常见的出现场景是:
    • 使用new关键字实例化对象的时候
    • 读取或设置静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候
    • 调用一个类的静态方法的时候。
  • 反射调用时,如果类没有初始化,得触发初始化。
  • 当初始化一个类的时候,如果它的父类没有初始化,先触发父类初始化
  • 虚拟机启动的时候,包含main()方法的类(入口类)先初始化。
  • JDK1.7动态语言支持的时候MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化的时候,需要先触发其进行初始化。

对于上面的5种情况,java虚拟机规范中使用的词语是“有且只有”,所以对一个类进行主动引用而进行的初始化就只有上面几种情况。其余的情况都是被动引用
举例被动引用的情况:

  • 通过子类引用父类的静态字段的时候,子类不会初始化
  • 通过数组引用类的时候,不会触发其初始化
  • 常亮在编译阶段会存入调用类的常量池,所以也不会触发定义类的初始化

其实除了类的加载过程,还有接口的加载过程,接口的加载过程和类的有一点不同,就是上面5种情况的第三种,接口应该是:

  • 一个接口初始化的时候,并不会要求其父接口初始化

类加载的过程

加载

加载是类加载的第一个阶段,主要做的工作是:

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

这里的通过类的全限定名来获取此类的二进制字节流并不一定通过class文件来获得,也可以通过jar包或者网络来获得,还可以是动态代理通过运算时计算生成。

验证

在类加载进内存后,第二件事做的是验证这个类的合法性,并且不会危害到虚拟机自身的安全。

因为class文件是可以“伪造”的,如果不对其加以验证, 可以在运行的时候会危害到虚拟机导致系统崩溃。

在这一个阶段,虚拟机做的事主要有:

  • 文件格式验证
    • 是否以魔术0xCAFEBABE开头
    • 主次版本是否符合要求(是否在当前虚拟机能够处理的范围内)
    • 常量池里面是否有不被支持的常量类型
    • 指向常量的索引是否有指向不存在的常量或不符合类型的常量
    • CONSTANT_Utf8_info型的常亮是否有不符合UTF-8编码的数据
    • Class文件中的各个部分和文件本身是否有被删除的或附加的其他信息
    • and so on
  • 元数据验证
    • 这个类是否有父类(除Object)
    • 这个类是否继承了不允许被继承的类(final修饰)
    • 如果这个类不是抽象类,是否实现了父类和接口中的要求的所有方法
    • 类中的字段和父类是否产生矛盾(不符合规则的重载等)
  • 字节码验证
    • 保证操作数栈的数据类型是否与指令代码序列都能配合工作。比如:在操作栈中放置一个int,使用的时候却按照Long型来加载入本地变量表中。
    • 保证跳转指令不会跳转到方法外的字节码上
    • 保证方法体中的类型转换是有效的
  • 符号引用验证
    • 符号引用中通过字符串描述的全限定名是否可以找到对应的类
    • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
    • 符号引用中的类、字段、方法的访问性(private,public protected,default)是否可以被当前类访问。

准备

这个阶段正式为类变量分配内存并设置初始值。这些变量都会在方法区中分配。因为在方法区中分配,所以这个阶段处理的都是一些static修饰的变量,不包括实例变量的。

还有要注意的是,这里的初始化并不是直接把你在程序中所赋予的值赋予给变量,而是赋予的0值。

比如:

public static int a = 10;

上面这行代码在这个阶段只会为a初始化为0,并不会初始化为10。

提示第二点:这个阶段不会去管方法中的局部变量,这个阶段处理的是类变量。

不过有个例外,我们都知道final修饰的变量是不可变的,所以你要是这样定义变量:

public static final int a = 10;

那么在准备阶段就会为它初始化为10了。

解析

在这个阶段,主要是虚拟机将常量池中的符号引用代替为直接引用。

符号引用在CLASS文件中它以CONSTANT_CLASS_INFO, CONSTANT_FIELDREF_INTO, CONSTANT_METHODREF_INFO等类型的常量出现。

符号引用:(Symbolic References)符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量,引用的目标并不一定已经加载到内存中,与虚拟机内存布局无关。
直接引用:(Direct References)直接引用可以是直接指向目标的指针,相对偏移量,或是一个能间接定位到目标的句柄。与虚拟机内存布局相关。

解析动作主要针对类/接口,字段,类方法,接口方法四类符号引用进行。分别对应于常量池的CONSTANT_CLASS_INFO,CONSTANT_FIELDREF_INFO,CONSTANT_METHODREF_INFO,
CONSTANT_INTERFACEMETHODREF_INFO四种类型。

其实我感觉,我们在写代码的时候,比如定义一个类,用A表示;那么在class文件里面会为它生成一个符号引用,只是简单地用来表示A类;在解析阶段,由于上面的几个步骤已经生成了A的class对象,那么符号引用就没有意义了,所以这里就把它转化成一种直接引用,比如指向A的class对象的引用。这是我的理解。

初始化

刚刚说了,上面在准备阶段做的一件事是

public static int a = 10;

为a初始化为0对吧? 那么我们在程序中是显示为a初始化为10 的, 所以现在,我们得把这个a初始化为10了。

在准备阶段中,变量已经被赋过一次系统要求的零值;而在初始化阶段,则是根据程序员通过程序制定的计划来赋值。

在这个最后的阶段,虚拟机主要执行了<clinit>()方法,在这个方法里面,主要收集了类中所有类变量的初始化动作(a = 10),和静态代码块(static{})中的语句合并而成。

当然在执行<clinit>()方法前必须保证其父类已经执行完<clinit>()

当然<clinit>()也不是必须的,如果类中没有静态语句块,也没有对变量的赋值操作,那么编译器是不会生成这个方法的。

当然在多线程的环境里面,虚拟机会为这个方法加锁以保证只有一个线程能执行初始化动作。

卸载

有了上面的几个过程,一个类基本就加载完成 了,最后剩下使用和卸载,使用就不说了。对于卸载,虚拟机会在代码中当代表类的Class对象不再被引用时,即不可达时,Class对象就会结束生命周期,此类在方法区内的数据也会被卸载,从而结束此类的生命周期。

这里注意一点:由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。

Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器几种。

Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可达的。

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

当用户自定义的类加载器的引用被置为null并且用户自定义的类加载器加载的class对象引用也置为null并且所有该类的实例都没被引用时,那么此时用户自定义的类加载器也就结束生命周期了,那么该类在方法区内的二进制数据被卸载。当程序再次需要该类的时候,就会重新加载,在Java虚拟机的堆区会生成一个新的代表这个类的class对象。

其实这里应该可以通过查看他们的hash码是否相等来判断是不是同一个class实例。

参考资料

《深入理解java虚拟机》


作者:尸情化异
链接:http://www.jianshu.com/p/5936560594f9
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值