JVM

JVM高级特性-虚拟机执行子系统

Posted by YaPi on April 18, 2017

虚拟机执行子系统

class文件

class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在class文件之中。当遇到需要占用8字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。

class文件采用类似于c语言的伪结构来存储。这种伪机构只有两种数据类型。无符号数和表。

无符号数属于基本的数据类型,以u1,u2,u4,u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值。

表是由多个无符号数或其他表作为数据项构成的复合数据类型。

常量池是因为内部数量不定,所以在class文件类,在常量池开始的位置需要一个u2类型的数据表示其数量。常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于java语言层面的常量概念,如文本字符串、声明未final的常量值。而符号引用则属于编译原理方面的概念,包括下面三类常量:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符
类加载机制

主要生命周期包括七个部分

  1. 加载
  2. 验证-连接
  3. 准备-连接
  4. 解析-连接
  5. 初始化
  6. 使用
  7. 卸载

其中,加载,验证,准备,初始化,卸载这个5个阶段的顺序是确定的。 加载的过程虚拟机并没有强制规定。但是初始化过程虚拟机是有严格规定的

  1. 遇到new,getstatic,putstatic,invokestatic这四条字节码指令时触发。对应实力化一个对象、设置或获取一个类的静态字段(呗final修饰、已在编译期把结果放入常量池的静态字段除外)
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候。
  3. 初始化一个类,若其父类没有初始化需要先初始化其父类
  4. main函数执行的那个类

接口中不能使用static{]语句块。一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候才会呗舒适化。

  • 加载

主要完成三件事

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

加载完成过后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。然后再内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存储再方法区里面)

  • 验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求。

  1. 文件格式验证,魔数、版本号、常量池…
  2. 元数据验证,是否有父类,是否继承了不能被继承的类,若不是抽象类是否实现啦父类的所有方法等
  3. 字节码验证。
  4. 符号引用验证,判断是否具有权限访问
  • 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区分配。这个阶段进行内存分配仅包括类变量(static修饰),不包含实例变量。实力变量会随着对象实例一起分配在java堆中。若是被final修饰的,会直接赋值为原值。

  • 解析

将常量池内的符号引用替换为直接引用的过程

  1. 符号引用是一组符号来描述所引用的目标
  2. 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
  • 初始化

初始化阶段是执行类构造器()方法的过程。此方法中包含有对所有类变量的赋值动作和静态语句块中的语句合并产生的。若都没有,则可以不为其生成此方法

类与类加载器

对于任意两个类,要判断其是否相等,只有再这两个类是由同一个类加载器加载的前提下才有意义。否则,即使是同一个class文件,还是不想等。

  • 双亲委派模型

从java虚拟机角度来分,只存在两种不同的类加载器。一种是启动类加载器(Bootstrap ClassLoader,C语言实现),是虚拟机的一部分。另一种就是所有其他类加载器,这些加载器都是java实现的。并且,全部继承java.lang.ClassLoader

  1. 启动类加载器(Bootstrap ClassLoader),负责加载/lib目录中的,或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的类库(如:rt.jar)名称不对的不会加载。
  2. 扩展类加载器(Extension ClassLoader,由sum.misc.Launcher$ClassLoader实现),他负责加载/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的类库
  3. 应用程序加载器(Application ClassLoader,由sum.misc.Launcher$App-ClassLoader),这个加载器是ClassLoader中的getSystemClassLoader()方法的返回值。所以一般也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是的都使用组合关系来复用父加载器的代码。它并不是一个强制性的约束模型,而是java设计者推荐给开发者的一种类加载器实现方式。

它的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此,所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器无法完成这个加载请求时,子加载器才会尝试自己去加载。这样的好处是确保了java体系中的基础类的唯一,比如java.lang.Object。

                Bootstrap ClassLoader
                        |
                Extension ClassLoader
                        |
                Application ClassLoader
            /                           \
        自定义类加载器               自定义类加载器

有些情况下需要破坏双亲委派模型,比如JNDI服务,它的主要代码是再rt.ja里面,但是具体的各大厂商实现的代码是在classpath下。但启动类加载器是不认识这些代码的,所以,就有了线程上下文类加载器。这个加载器通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时未设置,它将会从从父线程中继承一个,如果在应用程序的全局范围内都没有设置的豪华,那这个类加载器默认就是应用程序类加载器。java中所有涉及SPI的加载动作基本上都采用这种方式,比如:JNDI,JDBC,JCE等。

有时为了动态性的设置,也会破坏双亲委派模型,比如:代码热替换,模块热部署等。