• jvm理论-运行时数据区


    三大流行jvm

    sun HotSpot

    ibm j9

    BEA JRockit

    Oracle 会基于HotSpot整合 JRockit。

    jvm运行时数据区

    java虚拟机所管理的内存将会包括以下几个运行时数据区域

    程序计数器

    1、线程私有,随线程而生,随线程而灭。

    2、如果线程在执行java方法,计数器记录正在执行的虚拟机字节码指令的地址。

    3、如果线程在执行native方法,计数器值为空。

    java虚拟机栈

    1、线程私有,随线程而生,随线程而灭。

    2、java方法执行的内存模型:局部变量表、操作数栈、动态链接、方法出口等。

    栈帧

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)[1]的栈元素。

    栈帧存储了方法的局部变量表、操作数栈、栈帧信息(动态连接、方法返回地址、附加信息)。

    每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

    一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current StackFrame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

    局部变量表

    局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

    1、容量

    局部变量表的容量以变量槽(variable Slot)为最小单位。

    boolean,byte,char,short,int,float,reference,returnAddress占一个slot。

    long,double占两个slot。

    reference:表示对一个对象实例的引用。

    1)、从此引用可直接或间接找到对象在java堆中的数据存放的起始地址索引。

    2)、从此索引直接或间接找到对象所属类型在方法区中的存储的类型信息。

    returnAddress:为字节码指令jsr、jsr_w、ret服务的,指向一条字节码指令地址。

    2、容量大小和索引定位

    编译时已经确定。

    虚拟机通过索引定位的方式使用局部变量表。索引从0开始,最大值为slot的数量。

    在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static的方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

    3、局部变量与类变量

    类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值

    但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的。

    操作数栈

    操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

    当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。

    举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。

    Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

    动态链接

    每个栈帧都包含一个指向运行时常量池[1]中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

    Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接
    引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

    方法返回地址

    当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当
    前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。
    另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异
    常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常
    完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

    附加信息

    虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现,这里不再详述。在实际开发
    中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

    本地方法栈

    1、线程私有,随线程而生,随线程而灭。

    2、为虚拟机使用到的native方法服务。

    java堆

    1、线程共享。

    2、虚拟机启动时创建。

    3、存放对象实例:所有的对象实例以及数组都要在堆上分配。

    4、分为:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。

    年轻代又分为:Eden空间、From Survivor空间、To Survivor空间。

    java heap是GC管理的主要区域。

     由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GCFull GC

    Scavenge GC

        一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

    Full GC

        对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

    · 年老代(Tenured)被写满

    · 持久代(Perm)被写满 

    · System.gc()被显示调用 

    ·上一次GC之后Heap的各域分配策略动态变化

    方法区

    1、线程共享。

    2、存储已被虚拟机加载的类信息、常量、静态代码、jit后的代码等。

    3、HotSpot中,方法区也被称为“永久代”,被GC管理。

    运行时常量池:存放编译期生成的各种字面量和符号引用。可以是编辑期常量也可以是运行期生成的常量,如String的intern()方法。

    直接内测

    NIO引入的通道(channel)和缓存区(buffer)的I/O方式,可以使用Native函数在堆外分配内存,在java堆中通过DirectByteBuffer对象作为这块内存的引用进行操作。

    JVM对象

    对象创建(new)

    1、检测指令的参数能否在常量池中定位到一个类的符号引用。

    2、检测这个符号引用代表的类是否被加载、链接、初始化,如果没有先进行类加载。

    3、为新生对象分配内存。

    根据java heap是否规整,通过指针碰撞(Bump the pointer)或空闲列表(Free list)分配内存。

    内存是否规整,取决于GC是否有压缩整理功能。

    4、对对象进行必要设置,这些设置信息存放在对象头中。

    执行完new以后执行<init>,完成对象的初始化。

    对象的内存结构

    对象头 Header

    1、包含两部分数据:

    Mark Word:存储对象自身的运行时数据,如hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64的虚拟机(未开启指针压缩)中分别为4B和8B,官方称之为”Mark Word”。

    类型指针 KClass:即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

    数组长度:如果对象是一个Java数组,那再对象头中还必须有一块用于记录数组长度的数据。

    对象头在32位系统上占用8B,64位系统上占16B。 无论是32位系统还是64位系统,对象都采用8字节对齐。Java在64位模式下开启指针压缩,比32位模式下,头部会大4B(mark区域变位8B,kclass区域被压缩),如果没有开启指针压缩,头部会大8B(mark和kclass都是8B),换句话说,
    HotSpot的对齐方式为8字节对齐:(对象头+实例数据+padding)%8 等于0 且 0<=padding<8。以下说明都是以HotSpot为基准。

    32位:header (8B)=Mark Word(4B)+kclass(4b)

    64位没有开启指针压缩:header (16B)=Mark Word(8B)+kclass(8b)

    64位开启指针压缩:header (12B)=Mark Word(8B)+kclass(4b)

    实例数据 Instance Data

    1、存放字段数据。

    2、存放顺序:与虚拟机分配策略和java源码顺序相关。

    HotSpot分配策略:longs/doubles、ints、shorts/chars、bytes/booleans、oops。

    对齐填充 Padding

    1、对象的起始地址必须是8字节的整数倍(对象大小=8字节*整数),而对象头正好是8字节的整数倍,当对象实例数据没有对齐时,需要通过对齐填充来补全。

     

    对象访问

    定位

    1、通过栈上的reference数据来操作堆上的具体对象。

    2、reference类型:一个指向对象的引用。在各虚拟机实现中通过使用句柄和直接指针两种方式访问堆中对象。

    JDK1.2以后引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。

    四种引用强度依次逐渐解弱。

    强引用

    Object obj = new Object()。

    只要存在强引用,就不会被GC回收。

    软引用

    还有用但并非必要的对象。

    弱引用

    强度比软引用弱。

    虚引用

    也称为幽灵引用

    使用句柄

    这个时候,Java堆上会划分一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中又包含了对象实例数据和类型数据各自的具体地址,见下图 
    这里写图片描述
    句柄的好处:reference中存储的是稳定的句柄地址,对象被移动的时候,只会改变句柄中实例数据指针,reference本身不需要被修改 
    直接指针

    这个时候,reference存放的就是对象的地址,但是Java堆上的对象的布局就必须考虑如何存放对象类型数据的信息,见下图

    这里写图片描述

    指针的好处:指针的好处就是速度快,因为它的地址就是对象的地址,节省了一次指针定位的时间开销,随着对象访问的频繁,这种开销积少成多很可观

    HotSpot使用直接指针。

    自动内存管理

    内存分配

    1、对象优先分配在新生代的Eden区。

    2、如果Eden区空间不足,虚拟机发起一次 新生代GC(Minor GC)。

    3、大对象之间进入老生代。

    4、长期存活的对象进入老年代。

    5、对象年龄:对象经过一次Minor GC算一岁,默认15岁进入老年代。

    对象第一次经过Minor GC,从Eden进入Survivor,在Survivor中熬过一次Minor GC年龄增加1岁。

    6、动态对象年龄

    如果Suivivor中相同年龄所有对象总和大于Suivivor空间的1/2,年龄大于或等于该年龄的对象直接进入老年代。

    内存回收

    程序计数器、虚拟机栈、本地方法栈生命周期同线程,随着线程结束内存自然回收。所以这里主要关注java heap和方法区的内存回收。

    1、引用计数算法

    比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

    2、可达性分析(java,c#,List)

    1)、通过一系列“GC Roots”的对象作为起始点,从这些点开始向下搜索。

    2)、搜索的路径称为引用链(reference chain),当一个对象到GC Roots没有任何引用链,证明对象不可达,将被GC回收。

    3)、GC Roots对象:

    ①虚拟机栈帧中的本地变量表中引用的对象。

    ②方法区中类静态属性引用的对象。

    ③方法区中常量引用的对象。

    ④本地方法栈中JNI引用的对象。

    3、对象死亡

    真正宣告死亡,需要至少两次标记。

    1)、第一次标记:可达性标记后没有与GC Roots相连接的引用链。

    2)、第一次筛选:是否有必要执行finalize()方法,如果要执行finalize()方法,对象被放到F-Queue队列中,由虚拟机自动执行。

    3)、第二次标记:GC对F-Queue中的对象进行标记,判断是否与GC Roots可达。

    4、方法区回收(持久代回收)

    1)、废弃常量

    以String为例,没有任何String对象引用常量池中的”abc“常量。

    2)、无用的类

    同时满足下面三个条件:

    ①该类的所有实例都已被回收。

    ②加载该类的ClassLoader已经被回收。

    ③该类对应的java.lang.class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。

    GC算法

    标记-清除算法

    复制算法

    标记-整理算法

    分代收集算法

    当前主流虚拟机采用的算法。

    参考:http://pengjiaheng.iteye.com/blog/520228

    垃圾收集器

    HotSpot中的7中收集器。

    没有最好的收集器,只有针对具体应用最合适的收集器。

    参考:http://blog.csdn.net/canot/article/details/51050824

    G1收集器:JDK1.7u14以后的HotSpot虚拟机

    参考:http://blog.csdn.net/zhanggang807/article/details/45956325

    参考

    http://www.idouba.net/a-simple-example-demo-jvm-allocation-and-gc/

    http://pengjiaheng.iteye.com/blog/524024

    http://blog.csdn.net/hhb200766/article/details/46912325

    http://www.cnblogs.com/magialmoon/p/3757767.html

    http://yueyemaitian.iteye.com/blog/2033046

  • 相关阅读:
    hdu 1873 看病要排队
    母函数详解
    【RDMA】无损网络和PFC(基于优先级的流量控制)
    图解Linux网络包接收过程
    结对编程 <==> 断背山?
    CMPP和SMPP协议比较
    Berkerly DB、SQL Server的性能比较
    使用Berkeley DB遇到问题了
    重新学习C语言
    超长短信的处理办法
  • 原文地址:https://www.cnblogs.com/tenghoo/p/jvm_runtime.html
Copyright © 2020-2023  润新知