深入JVM系列 9 - hotspot 7 & 8 运行时数据区

-

本文为深入JVM系列的一部分

可能需要阅读前文

主要内容为7的spec中的运行时数据区,这部分和hotspot 6也是比较贴近的,最后会记录hotspot8中的变化,主要是metaspace带来的一些改动


虚拟机规范

以下内容来自java虚拟机规范 se 8


概览

灵魂画图

堆 heap
--------------------------------
|                                |
|  新生代 youngGen                |
|   ------------------------     |
|  |   eden     | from | to |    |
|   ------------------------     | 
|                                |
|  老年代 oldgen                  |
|   -------------------------    |
|  |          oldgen         |   |
|   -------------------------    |
--------------------------------

本地内存 native memory
--------------------------------------
|                                    |
|      线程1     线程2   ...  线程n    |
|    --------                        |
|   |   pc   |    ...                |
|    --------                        |
|       栈                           |
|    --------                        |
|   |   帧1  |                       |
|    --------                        |
|   |   ...  |                       |          
|    --------                        |
|   |   帧n  |                       |
|    --------                        |
|                                    |
--------------------------------------


方法区 method area
 ----------------------------------
|        class1           classN    |
|    --------------                 |
|   | 运行时常量池 |                  |
|    --------------                 |
|    --------------                 |  
|   |     字段     |                 |
|    --------------      ...        |
|    --------------                 |
|   |     方法     |                 |
|    --------------                 |
|    --------------                 |
|   |     代码     |                 |
|    --------------                 |
 -----------------------------------


PC寄存器

  • java虚拟机支持多条线程同时执行,每一条java线程都有自己的pc寄存器,在任意时刻,一条java线程只会执行一个方法的代码,称为当前方法,对于非native方法,pc寄存器保存正在执行的字节码指令地址,如果方法是native的,那么pc寄存器的值为undefined,pc寄存器至少能保存一个returnAddress类型的数据或一个平台相关的本地指针的值


java堆

  • java堆是供各个线程共享的运行时内存区域,也供所有类实例和数组对象分配内存区域
  • java堆在虚拟机启动时被创建,它存储了被垃圾收集器管理的各种对象,堆空间可以是固定的也可以是动态扩展和收缩的,内存也不要求是连续的


方法区

  • 它存储了每一个类的结构信息,如运行时常量池,字段,方法数据,构造函数和普通方法的字节码内容,还包含一些在类,实例,接口初始化时用到的特殊方法
  • 方法区是供各个线程共享的运行时内存区域。方法区与传统语言中的编译代码存储区,或操作系统的正文段作用类似。方法区在虚拟机启动时创建,虽然是堆的逻辑组成部分,但简单的虚拟机可以选择在这个区域不实现垃圾收集与压缩。java 8 版本的虚拟机规范也不限定方法区的内存位置和编译代码的管理策略。
  • 方法区空间可以是固定的也可以是动态扩展和收缩的,内存也不要求是连续的


运行时常量池

  • 运行时常量池是每个class文件中的一个类或者接口的常量池表的运行时表现形式,类似于传统语言中的符号表,但数据范围更加广泛
  • 每个运行时常量池都在java虚拟机的方法区中分配,在类和接口加载时创建


本地方法栈

  • 本地方法栈,使用传统的栈支持native方法或使用其他语言支持指令集解释器时可以使用本地方法栈,本地方法栈不是必须的,如果本身不依赖传统栈,那么就不需要实现它,如果支持本地方法栈,一般在线程创建时按线程分配


虚拟机栈


  • 每一个线程都有自己私有的虚拟机栈,它与线程同时创建,用于存储栈帧,栈帧可以在堆中分配
  • 虚拟机规范允许栈设置为固定大小或动态增长或收缩


栈帧
  • 栈帧用来存储数据和部分过程结果的数据结构,同时用来处理动态链接,方法返回值和异常分配
  • 栈帧由局部变量表,操作数栈,当前方法所属类的运行时常量池的引用构成,还允许携带实现相关的一些信息
  • 栈帧随方法的调用创建,随方法的结束而销毁(无论正常还是异常结束),本地变量表和操作数栈的容量在编译时确定,并且同code属性保存及提供给栈帧使用,栈帧整体的大小则取决于实现,如果当前方法调用了其他方法,或者当前方法结束,这个方法的栈帧都不在是当前栈帧,方法返回时虚拟机将丢弃当前栈帧,使用前一个栈帧作为当前栈帧


局部变量表
  • 每个栈帧内部包含一组称为局部变量表的变量列表。栈帧中的局部变量表的长度由编译时确定,并且存储于类或或接口的二进制表示之中,即通过方法的code属性保存及提供给栈帧使用
  • 一个局部变量可以保存一个类型为boolean,byte,char,short,int,float,reference或returnAddress的数据,两个局部变量可以保存为一个类型为long或double的数据,首个局部变量的索引值为0,局部变量的索引值是整数,大小等于0,且小于局部变量表的长度
  • long和double类型的数据占用两个连续的局部变量,采用两个局部変量中较小的索引值来定位。將一个double类型的值存储在索引值为n的局部変量中,实际上的意思是索引值カn和n+1的兩个局部变量都用来存储这个值,索引值为n+1的局部変量是无法直接获取的,但可能会被写入,这种操作將会导致局部变量n的内容失效。n不一定需要是偶数,也不要求所有long和double采用64位对齐的方式连续存储
  • 虚拟机使用局部变量表来完成方法调用时的参数传递。当调用静态的方法时,参数会一次传递到局部变量表中从0开始的连续位置上,当调用实例方法时,第0个参数为this,后续的其他参数将会传递到局部变量表中从1开始的位置


操作数栈

  • 操作数栈上的每个位置都可以保存一个java虚拟机定义的任意数据类型,包括long和double,在任何时刻,操作数栈都有一个确定的栈深度,long和double栈两个单位的深度,其他数据类型占用一个深度
  • 每个栈帧内部都包含一个称为操作数栈的先进后出栈,栈帧中操作数栈的最大深度由编译期决定,并且通过code属性保存及提供给栈帧使用,在上下文明确不会产生误解的前提下,当前栈帧的操作数栈直接称为操作数栈
  • 栈帧在刚创建时,操作数栈是空的,java虚拟机提供一些字节码指令来从局部变量表或对象实例中复制常量或变量值到操作数栈中,从操作数栈中取走数据,操作数据以及把数据结果重新入栈。在调用方法时,操作数栈也用来准备调用方法的参数以及接受方法返回的结果
  • 例如,iadd字节码指令的作用是将两个int类型的数值相加,它要求在执行iadd指令时,两个int类型数值从操作栈中出栈,相加求和,然后将求和的结果重新入栈,在操作数栈中,一项运算由多个子运算嵌套进行,一个子运算的结果可以被其他外围运算所使用
  • 操作数栈中的数据必须正常操作,例如,不可以入栈连个int类型的数据,然后当做long类型去操作,有一小部分指令可以无视数据类型当做裸类型来操作,但他们不可以修改数据,也不可以拆散数据,这些操作的正确性由class文件的校验过程来保证


动态链接

  • 每个栈帧内部都包含一个指向当前方法所在类的运行常量池的引用,以便对当前方法的代码实现动态链接,在class文件里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是将这些符号引用转换为对实际方法的直接引用。类加载的过程中将要解析未被解析的符号引用,并将对变量的访问转换为在运行时在存储结构中的正确偏移量
  • 对其他类的方法和变量进行了晚期绑定,所以即使那些类发生了变化,也不会影响调用它们的方法


hotspot 实现

虚拟机按用途将内存区域分为堆和非堆



分代
  • 由于堆用于对象的分配,按对象年龄又再一步划分为新生代和老年代区域,注意虽然permGen属于分代范畴,但它位于非堆空间
  • 分代基于jvm中大量对象死得快,少量对象死得慢的观点,对于不同的最新,采取不同的收集策略,分配不同的内存空间分别存储不同年龄的对象,对象可以分为三代
youngGen: 位于堆,包括 Eden 和 From/To Survivor 区
oldGen:   位于堆
permGen:  位于非堆


堆的实现
  • hotspot中使用什么样的堆实际是由使用什么样的垃圾收集策略决定的,直接使用的堆实现有三种 ParallelScavengeHeap,GenCollectedHeap,G1CollectedHeap,继承关系如下
              CollectedHeap
     --------------|-------------
     |                           |
ParallelScavengeHeap        SharedHeap
                              ———|———
                             |       |
                  GenCollectedHeap  G1CollectedHeap
  • 所谓使用什么样的策略,体现到实践中就是设什么参数启动什么gc收集器,体现到代码中就是使用什么policy,堆相关的代码实现在hotspot的内存管理模块 memory,参数上,UseParallelGC 对应 ParallelScavengeHeap,UseG1GC 对应G1CollectedHeap, G1CollectorPolicy,默认为 GenCollectedHeap,GenCollectedHeap对应的policy如下
默认:MarkSweepPolicy
UseSerialGC: MarkSweepPolicy
UseConcMarkSweepGC & UseAdaptiveSizePolicy : ASConcurrentMarkSweepPolicy
UseConcMarkSweepGC: ConcurrentMarkSweepPolicy


非堆

  • 非堆:包括方法区和code cache
注意,永久代和方法区是两个不同的概念
分代是属于gc范畴的概念,并且是基于hotspot的设计思想
方法区属于jvm设计规范的内容,方法区是否在堆中这点依赖于实现,至少hotpot就没有这么做


hotspot8 变迁

下面是r大给出的数据结构迁移,原文链接找不到了,有空补上

  • JDK7

    symbolOop (PermGen) -> Symbol* (native\ heap 但不是Metaspace)
    interned String,包括interned String对象以及其背后的char[] (PermGen) -> (普通Java heap)
    Java类的静态变量原本位于instanceKlass末尾 (PermGen) -> java.lang.Class对象末尾 (普通Java heap)
    应对上一条,新增instanceMirrorKlass来描述java.lang.Class对象实例

  • JDK8,从7移动到8的metaspace的结构,主要是与spec方法区相关的oop
受GC管理的非Java对象的基类跟Java对象一样是oopDesc -> 非Java对象(元数据对象)的基类改为MetaspaceObj 
klassOop -> Klass* 
xxxKlassKlass -> 没了,不需要了 
methodOop -> Method* 
methodDataOop -> MethodData* 
constMethodOop -> ConstMethod* 
constantPoolOop -> ConstantPool* 
constantPoolCacheOop -> ConstantPoolCache*