Java基础: JVM(一) JVM概述与字节码
Java基础: JVM(二) 常量池
Java基础: JVM(三) JVM执行引擎01
Java基础: JVM(四) Java栈帧
Java基础: JVM(五) JVM执行引擎02
Java基础: JVM(六) 类变量和类方法解析
Java基础: JVM(七) 类生命周期与类加载器
Java基础: JVM(八) 热加载
Java基础: JVM(九) Java内存模型
Java基础: JVM(十) 编译相关
Java基础: JVM(一) JVM概述与字节码 常量池从Java字节码文件看,其实主要包含三部分:常量池、字段信息、方法信息。其中常量池存储了字段和方法的相关符号信息,也是Java字节码文件的核心。这里说的常量池不同于JVM内存模型中的常量区,这里的常量池仅仅是字节码中的一部分信息,是Java编译器对Java源代码进行语法解析后的产物,比较粗糙,包含了各种引用,信息不够直观。而JVM最终会将字节码文件中的常量池信息进行二次解析,还原出所有常量元素的全部信息,并存储到JVM内存模型中的常量区,让内存与编写的Java源代码保持一致 JVM解析Java类字节码文件的接口是ClassFileParser::parseClassFile(),其步骤为: 内存分配JVM想要解析常量池信息,就必须先划出一块内存空间,将常量池的结构信息加载进来,然后才能进一步解析。内存空间的划分主要考虑两点:分配多大的内存、分配在哪里。JVM使用一个专门的C++类constantPoolOop来保存常量池的结构信息,而该类里实际保存数据的是属于typeArrayOop类型的_tags实例对象。typeArrayOop类是继承与oopDesc顶级结构,且没有新增字段,也就是这种类型仅仅只有标记和元数据 oopFactory::new_constantPool()链路比较长,从宏观层面看,大体可以分为3个步骤: 涉及到的类: 由于JVM的堆在JVM初始化过程中便完成了指定大小的空间分配,因此完成当前被加载的类所对应的常量池内存分配没有真正向操作系统申请,仅从JVM已申请的堆内存中划拨了一块空间用于存储常量池结构信息。内存的申请最终通过 constantPool的内存有多大为一个Java类创建对应的常量池,需要在JVM堆区为常量池先申请一块连续的内存空间,其大小取决于一个Java类在编译时所确定的常量池大小 在常量池初始化链路中会调用constantPoolKlass::allocate()方法,该方法会调用constatnPoolOopDesc::object_size(length)方法来获取常量池大小:
sizeof()函数在计算C++类时,该函数返回的是其所有变量的大小加上虚函数指针的大小。对于constantPoolOopDesc本身包含8个字段,由于继承oopDesc父类,还会包含父类的2个成员变量:
而对于oopDesc包含的static BarrierSet* bs这个静态类型变量,在JVM启动之初会被操作系统直接分配到JVM程序的数据段内存区,因此10个字段在32位平台上将返回40(字节)。而对于HeapWordSize是HeapWord类的大小,而该类只包含一个char指针型变量,即返回指针宽度,因此32位平台上返回4(字节),64位平台上返回8(字节),这样header_size()函数就是计算出constantPoolOopDesc这个类型实例在内存汇总所占用的双字数(32位平台一个指针宽度为4字节,即双字) 内存空间布局在为constantPoolOop常量池对象分配内存时,需要分析JVM为常量池所申请的内存空间布局模型,前面说过JVM为constantPoolOop实例分配的内存大小是(headSize + length)个指针宽度。在JDK6中JVM为常量池申请的内存位于perm区的一片连续空间,而JVM为常量池申请内存时也是整片区域连续划分,不会存在碎片化
初始化内存JVM为Java类对应的常量池分配好空间后,就需要执行这段内存空间的初始化,所谓的初始化就是清零操作。由于在申请内存空间时执行了内存对齐,同时由于JVM堆区会反复加载新类和擦除旧类,因此如果不执行清零,则会影响后续对Java类的解析 在CollectedHeap::common_permanent_mem_allowcate_init()中调用init_obj(obj, size),其内部调用其他函数对JVM内部对象清零,会将制定内存区的内存数据全部清空为零值 opp-klass模型上一篇文章和这篇之前也讲到过opp-klass模型 —— 一分为二的内存模型。为了消灭指针的目的,JVM本身的类模型非常更复杂,而且这种复杂性从JVM发布以来,即使从JDK6到JDK8也仅仅是对JVM内部的几种具体类型进行了重组和去繁就简,并没有根本上的变革,只要Java语言对外提供的功能特性不发生巨大变化,则JVM内部的类模型就不会发生巨大的质变 前面讲过,JVM内部基于oop-klass模型描述一个Java类,将一个Java类一拆为二分别描述,第一个模型是oop,第二个模型是klass。oop是普通对象指针,用来表示对象的实例信息,看起来像个指针,而实际上对象实例数据都藏在指针所指向的内存首地址后面的一片内存区域中。而klass包含元数据和方法信息,用来描述Java类或JVM内部自带的C++类型信息,便是前面讲的数据结构,Java类的基层信息、成员变量、静态变量、成员方法、构造函数等信息都在klass中保存,JVM据此可以在运行期反射出Java类的全部结构信息,当然JVM本身所定义的用于描述Java类的C++类也使用klass去描述 JVM使用oop-klass这种一分为二的模型描述一个Java类,虽然模型只有两种,但是其实从3个不同的维度对一个Java类进行了描述。侧重于描述Java类的实例数据的第一种模型oop,主要为Java类生成一张“实例数据视图”,从数据维度描述一个Java类实例对象中各个属性在运行期的值。而第二种模型klass则分别从两个维度去描述一个Java类,第一个维度是Java类的“元信息视图”,另一个维度则是虚函数列表,或者叫做方法分发规则。元信息视图为JVM在运行期呈现Java类的”全息”数据结构信息,这是JVM在运行期以动态反射出类信息的基础。 在Java语言中并没有虚函数这个概念,就是不能使用virtual这个关键字去修饰一个Java方法。而C++实现面向对象多态性的关键字主要就是virtual,而JVM在内部使用C++类定义的一套对象机制去表达Java类的面向对象机制,这样Java类最终被表达成为JVM内部的C++类,并且Java类方法的调用最终要通过对应的C++,而Java方法本身不支持virtual这个关键字修饰,那么面对一个多重继承的Java类体系,C++如何知道类中哪个方法是虚函数,哪个不是呢?而Java的做法是将Java类的所有函数都视为是virtual的,这样Java类中的每个方法都可以直接被其子类、子子类覆盖而不需要增加任何关键字作为修饰。正因如此,Java类中的每个方法都可以晚绑定,只不过对于一些确定的调用,在编译器就可以实现早绑定。正因为JVM将Java类中每个函数都视为虚函数,所以最终在JVM内部的C++层面就必须维护一套函数分发表 体系总览在JVM内部定义了3种结构去描述一种类型:oop、klass、handle类,这3种数据结构不仅能够描述外在的Java类,也能够描述JVM内在的C++类型对象 前面讲过klass主要描述Java和JVM内部C++类的元信息和虚函数,这些元信息的实际值就保存在oop里面。oop中保存一个指针指向klass,这样在运行期JVM便能知道每一个实例的数据结构和实际类型。handle则是对oop的行为的封装,在访问Java类时一定是通过handle内部指针得到oop实例的,再通过oop就能拿到klass,如此handle最终便能操纵oop的行为了(特别的,如果是调用JVM内部C++类型所对应的oop的函数,就不需要通过handle来中转,直接通过oop拿到指定klass便能实现)。klass不仅包含自己所固有的行为接口,而且也能够操作Java类的函数。由于Java函数在JVM内部都被表示为虚函数,因此handle模型其实就是Java类行为的表达 它们三者的关系如下,可以看到Handle类内部只有一个成员变量_handle,类型为oop*,最终指向一个oop的首地址,因此只要拿到handle,就能进一步获取oop和关联的klass实例,从而取到klass对象实例后,便能实现对oop对象方法的调用 oop体系Hotspot里的oop其实就是GC所托管的指针,每一个oop都是一种xxxOopDesc*类型的指针。所有oopDesc及其子类(除markOopDesc外)的实例都由GC所管理,这才是最重要的,是oop区分Hotspot里所使用的其他指针类型的地方 对象指针本质而言就是个指针,由于OOP的鼻祖SmallTalk语言,它的对象也由GC管理,但是其一些简单的值类型对象会使用”直接对象”的机制实现,例如整数类型就并不是在GC堆上分配的对象实例,而是直接将实例内容存在了对象指针里的对象,这种指针也叫做带标记的指针,这一点与markOopDesc类型如出一辙,因为markOopDesc也是将整数值直接存储在指针里面,这个指针其实没有指向内存的功能。所以在SmallTalk运行期,每拿到一个指针需要判断是直接对象还是真的指针,如果是真的指针,那么就是普通的对象指针了。所以在Hotspot里,oop就是指一个真的指针,而markOop则是一个看起来像指针但实际上是藏在指针里的对象(数据),这也是markOop实例不受GC托管的原因,因为只要出了函数作用域,指针变量就直接从堆栈中释放掉了,不需要垃圾回收 在oopsHierarchy.hpp中定义了oop体系的所有成员,有十多种不同的oop,其中最常用的是constantPoolOop和instanceOop,前者前面讲过,后者作为Java程序的解释器和虚拟运行介质,JVM将Java实例映射为instanceOop klass体系klass提供了2种能力: 与oop相同,在klassHierarchy.hpp中也定义了klass体系的所有成员,除去constantPoolOop对应的constantPoolKlass外,最重要的就是instanceKlass和klassKlass(在JDK8中已被去除)了
如果一个Klass既不是instance也不是array,则其_layout_helper为0;如果是instance则值为正数,表示instance的大小;如果是一个数组,则值为负数 handle体系handle封装了oop,通过oop拿到klass,因此handle间接封装了klass,JVM内部使用一个table来存储oop指针 如果oop是对普通对象的直接引用,那么handle就是对普通对象的一种间接引用,是因为GC考虑使用这种方式的: 在涉及Java类的继承和接口继承,C++领域类的继承和多态性最终通过vptr(虚函数表)来实现,在klass内部记录了每一个类的vptr信息 vtable虚函数表:vtable中存放Java类中非静态和非private的方法入口,JVM调用Java类的方法(非static/private)时,最终会访问vtable,找到对应的方法入口 itable接口函数表:Itable中存放Java类所实现的接口类方法,同样JVM调用接口方法时,最终会访问itable,找到对应的接口方法入口 不过要注意,vtable和itable里存放的并不是Java类方法和接口方法的直接入口,而是指向了Method对象入口,JVM会通过Method最终拿到真正的Java类方法入口,得到方法所对应的字节码/二进制机器码并执行。当然对于被JIT进行动态编译后的方法,JVM最终拿到的是其对应的被编译后的本地方法入口 同样在handles.hpp里定义了handle体系的成员,还通过宏分别批量声明了oop和klass家族的各个类所对应的handle类型,在编译器宏被替换后,便有opp和klass其对应的handle体系。但是为啥定义了2套不同的handle体系,那是因为JVM使用oop-klass这种一分为二的模型去描述Java类以及JVM内部的特殊类群体,为此JVM内部特定义了各种oop和klass类型。但是对于每一个oop都是一个C++类型,即klass;而对于每个klass所对应的class,在JVM内部又被封装为oop。JVM在具体描述一个类型时,会用oop去存储这个类型的实例数据,并用klass去存储这个类型的元数据和虚方法表。当一个类型完成生命周期后JVM触发GC去回收,回收时既要回收一个类实例对应的实例数据oop也要回收对应的元数据和虚方法表(不是同时回收,一个在堆垃圾回收,一个在方法区垃圾回收)。为了让GC既能回收oop又能回收klass,因此oop本身被封装成oop,而klass也被封装为oop,只是JVM内部恰好将描述实例的oop全部定义为以oop结尾的类,并将描述类结构和方法的klass全部定义为以klass结尾的类,正好与JVM内部描述类信息的模型oop-klass重名了,所以产生了误解。 互相转换1、oop和klass到handle: 2、klass与oop相互转化: klass模型创建在JVM启动过程中,先创建了klassKlass实例,再根据该实例,创建了常量池所对应的Klass类 —— constantPoolKlass。因此要分析constantPoolKlass需要先了解klassKlass实例的构建 klassKlass创建大体上分为6步: 同样,constantPoolKlass模型构建的步骤也是这样,只不过不需要第6步。JVM最终在方法区创建的对象是constantPoolKlass,但由于每个klass最终都被包装为oop,因此在内存中构建的模型为: 常量池解析Java类源代码被编译成字节码,字节码中使用常量池来描述Java类中的结构信息,JVM加载某个类时需要解析字节码中的常量池信息,从字节码文件中还原出Java源代码中定义的全部变量和方法。而JVM运行时的对象constantPoolOop便是用来保存JVM对字节码常量池信息分析结果的 constantPoolOop在创建的过程中,会执行constantPoolKlass::allocate()函数,该函数主要干了3件事情: 完成constantPoolOop的基本构建工作,然后JVM就是一步一步将字节码信息翻译成可被物理机器识别的动态的数据结构,即本文顶端步骤里写到的ClassFileParser::parse_constant_pool_entries()解析常量池信息,这个函数中通过一个for循环处理所有的常量池元素,每次循环开始先执行 参考: |
|||||||||||||||||||
注意这里是 klass 对象的“首地址”?
实例说明class Model
{
public static int a = 1;
public int b;
public Model(int b) {
this.b = b;
}
}
public static void main(String[] args) {
int c = 10;
Model modelA = new Model(2);
Model modelB = new Model(3);
}
上述代码得 OOP-Klass 模型入下所示 |
|
JDK8中没有 klassKlass了