深入理解JVM(类篇)

前言

在java中,类是最重要的部分之一,虚拟机把代码转换为.class文件中的字节码,然后再把字节码转化为机器码,正是由于这个操作,使得java能够一次编译,到处运行。
类中的文件结构是什么样子的?虚拟机又是如何加载类,这篇文章将做一个总结,简单介绍一下类文件结构,然后重点讲述面试的热点问题之一:类加载器。

类文件结构

class文件是一组以8个字节为基础单位的二进制流,中间没有任何分隔符,当遇到需要占用8个字节以上的数据的时候,将采用高位在前的方式分割成若干个8字节。
在class文件中,只有两种数据结构:无符号数和表。
表是由多个无符号数或者其他表构成的复合数据类型。
当需要描述同一类型但是数量不定的多个数据时,经常会使用一前置的容器计数器加若干个连续数据项的方式。

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count - 1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attribute_count1
attribute_infoattributesattributes_count

魔数和版本号

最前面magic意思是魔数,它的唯一作用是确定该文件是一个能被虚拟机识别的class文件。
在magic之后的minor_version和major_version意思是次要版本号和主要版本号。

常量池

然后是constant_pool_count也就是常量池计数器,用于表示常量池中有多少常量。
之后的constant_pool即为常量池入口。
常量池中包括两大类常量:字面量和符号引用。
字面量类似java中的常量,包括:文本字符串、被声明为final的常量值等。
符号引用则是描述类、接口等符号,虚拟机首先在类加载的时候首先会获取符号引用,然后在类创建或者运行时解析的时候通过符号引用找到对应类所在的内存地址。

访问标志

在常量池结束之后,接下来的是访问标志(access_flags),访问标志用于标识这个class是类还是接口;是否定义为public类型等等。

类索引、父类索引、接口索引集合

类索引(this_class)和父类索引(super_class)都是u2类型的数据,而接口索引集合(intefaces)是一组u2数据的集合。
类索引和父类索引用于确定该类和该类的父类的全限定名(除了object类,所有的类父类都不为0)。
接口索引集合用于确定这个类实现了哪些接口。

字段表集合

字段表集合(field_info)用于描述接口或者类中声明的变量,但是不包括方法内部的局部变量。

方法表集合

类似字段表,用于描述方法的集合。

属性表集合

在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

虚拟机类加载机制

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

其中加载、验证、准备、初始化和卸载这五个阶段的顺序是固定的,类型加载的过程必须按照这种顺序按部就班的开始(而不是完成,因为这些阶段是交错进行的),但解析不一定,解析可以在初始化之后再开始,这是为了支持JAVA动态绑定特性。

类加载的时机

《java虚拟机规范》中明确指出,有且只有6种情况需要立即对类进行初始化(加载、验证、准备需要在此之前开始):

  1. 遇到new、getstatic、putstatic、invokestatic这四条字节码的时候,如果类型没有经过初始化,则需要先触发其初始化阶段。触发这4条字节码的典型场景有:
    • 使用new关键字进行实例化。
    • 读取或者设置一个类型的静态字段(已被final修饰、在编译期把结果放入常量池的除外)。
    • 调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有初始化,则需要先初始化。
  3. 初始化类的时候,如果其父类还没进行过初始化则先对其父类进行初始化。
  4. 虚拟机启动的时候,用户需要指定一个主类,虚拟机会先初始化这个主类
  5. 使用动态语言支持的时候,如果java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic、REF_pubStatic、REF_invokeStatic、REF_newInvokeSpcial四种类型的方法句柄的时候,如果这个方法句柄对应的类没有进行过初始化,则进行初始化。
  6. 当一个接口定义了默认方法(被default修饰的接口方法),如果有这个接口的实现类进行了初始化,则这个接口需要在其之前进行初始化。

加载

加载阶段,虚拟机需要完成以下3件事情:

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

验证

主要是用来验证这个Class文件的字节流是否正确,分为4个部分:

  1. 文件格式验证:验证是否符合前面所讲的class文件规范
  2. 元数据验证:对字节码进行语义分析,看类是否符合《java语言规范》
  3. 字节码验证:通过数据流和控制流进行分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:判断类中的符号引用是否正确。

准备

准备阶段是为类中定义的变量(静态变量,不包括实例变量)分配内存,并且设置类变量初始值(同样是类型初始值,非用户定义的初始值)。

解析

这个阶段主要是把类中的符号引用替换为直接引用的过程。

  • 符号引用:用一组符号来描述需要引用的目标,虚拟机可以通过符号引用找到对应的目标。
  • 直接引用:可以是指向目标的指针、相对偏移量、一个能够间接定位到目标的句柄。

初始化

初始化阶段就是执行类构造器<clinit>()<clinit>()方法并非是程序员在java代码中直接写的方法,它是javac编译器自动产生的。

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是按语句在源文件中出现的顺序决定的。
  • <clinit>()方法与类的构造方法不同,不需要显式地调用父类构造器,java虚拟机会保证在子类的方法执行前,父类的方法已经执行完毕(所以父类的静态变量的赋值会在子类前面)。
  • <clinit>()方法不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  • 接口不能使用静态语句块,但是仍有变量赋值操作,但是接口不需要在子类之前执行父类的<clinit>()方法。只有当父类中定义的变量被使用时才会调用。
  • 虚拟机必须保证一个类的<clinit>()方法被正确的加锁同步。

类加载器

双亲委派模型

自JDK1.2以来,Java就一直保持着三层类加载器、双亲委派的类加载架构。

  • 启动类加载器:负责加载存放在lib目录下或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机内存中。
  • 扩展类加载器:负责加载lib/ext目录中或者被java.ext.dirs系统变量所指定的所有路劲中的所有类库。
  • 应用程序类加载器:加载ClassPath上的所以类库。

除了这三种类加载器之外,用户还可以自定义一个类加载器来进行拓展。
双亲委派模型

双亲委派的工作原理是:如果一个类加载器收到类加载请求,那么首先会委派给其父类进行加载,因此所有的类的加载请求最后都会被传到启动类加载器中,只有当父类反馈自己无法成功加载的时候,子加载器才会尝试自己进行加载。

破坏双亲委托模型

在《深入理解JVM》的书中,介绍了3种破坏双亲委派模型的方式。

  • 重写ClassLoader类中的loadClass()方法。由于双亲委托模型的具体实现在loadClass()方法中,所以一旦覆盖就破坏了双亲委托机制。
  • 使用线程上下文类加载器:例如JDBC之类的服务中需要由启动类加载器来加载在ClassPath目录下的厂商提供的文件,所以需要一个线程上下文类加载器来进行加载,一旦启动类加载器发现自己无法加载这些类,就委托给线程上下文类加载器进行加载。
  • 代码热部署:以OSGi为例,每一个程序模块都有一个自己的类加载器,当需要更换模块的时候,连同模块的类加载器一同换掉。在OSGi中,类加载器不再是双亲委派模型,而是一个更加复杂的网状结构。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值