深入理解JVM(四)-Java虚拟机类加载机制

对于C/C++开发者来说,他们在内存管理方面具有至高的权利,但是也承担着巨大的维护责任。而对于Java程序员来说,有了JVM(Java虚拟机)管理机制的帮助,再也不用担心内存泄漏和内存溢出问题了。因此,这篇文章我将深入探讨一下JVM,它的内部结构以及运行原理。

JVM的类加载机制

Java程序的执行过程

这里写图片描述

当Java文件被编译为class文件后,就可以通过(java ClassName)来执行你的Java程序,JRE的类加载器从硬盘中读取class文件,载入到系统分配给JVM的内存区域–运行数据区(Runtime Data Areas). 然后执行引擎解释或者编译类文件,转化成特定CPU的机器码,CPU执行机器码,至此完成整个过程。

那么,现在就来深入了解下,类加载器的加载过程。

一、类加载的时机

这里写图片描述

类初始化的时机:

  • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令的时候。

  • 使用java.lang.reflect进行反射调用的时候。

  • 当初始化一个类的时候,发现其父类还没有初始化,那么先去初始化它的父类。

  • 当虚拟机启动的时候,需要初始化main函数所在的类。

  • 当使用JDK1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时,需要对应的类初始化。

二、类加载器的加载过程

  1. 加载

    • 通过一个类的全限定名来获取此类的二进制字节流。

    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    • 在Java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。

加载阶段尚未完成,连接阶段可能以及开始,但这些夹在加载阶段进行的动作,仍然属于连接阶段的内容,两个阶段的开始时间仍然保持着先后顺序。

  1. 验证

    • 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这个阶段的验证是基于字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储所以后面的验证阶段都是基于方法区的存储结构进行的。

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

    • 字节码验证:进行数据流和控制流分析,对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。

    • 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),对常量池中的各种符号引用的信息进行匹配性的校验。

  2. 准备
    准备阶段是正式为类变量分配并设置类变量初始值的阶段,变量所使用的内存都将在方法区中进行分配,需要说明的是:

    这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;这里所说的初始值“通常情况”是数据类型的零值,假如:

    public static int value = 123;

    value在准备阶段过后的初始值为0而不是123,而把value赋值的putstatic指令将在初始化阶段才会被执行。

  3. 解析
    解析阶段是在虚拟机将常量池内的符号引用替换为直接引用的过程。

    • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

    • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

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

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

    • 方法与实例构造器< init>()不同,不需要显示的调用父类构造器,虚拟机会保证在子类的< clinit>()方法执行之前,父类的< clinit>()已经执行完毕。

    • < clinit>()方法对于类或接口来说不是必须的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit>()方法。

    • 执行接口的< clinit>()不需要先执行父接口的< clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。接口的实现类在初始化时也不会执行接口的< clinit>()方法。

    • 虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确的加锁和同步,如果多个线程同时去初始化一个类,则只会有一个线程去执行这个类的< clinit>()方法,其他线程需要阻塞等待。

感谢

如果大家想了解更多,欢迎继续阅读接下来的章节,推荐大家熟读《深入理解Java虚拟机》这本书。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值