zoukankan      html  css  js  c++  java
  • 深入理解Java虚拟机-第三版-第二章JVM内存区域笔记

    第二部分
    2章节内容

    Program Counter Register
    占用空间很小,当前线程执行的字节码行号指示器。一般的JVM,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
    为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,线程私有。

    Java Virtual Machine Stack
    每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    这个栈帧是一种重要的数据结构。

    ---我的理解:我们应该都有在IDE中对一个代码的逻辑链进行debug的体会,在一个顺序执行的方法中,我们可以在一个连续的代码块里,不断地进入某一行的具体实现,在进入到最核心的实现之后,debug的顺序又会沿着我们进入的顺序逆向从各自方法中还原出来,直到我们最初debug的位置。从这个过程中,我们不难理解为什么会用栈这种结构来存放整个执行的方法链。

    另外,这个栈帧其实我的理解就是为了统一整个管理过程而特意设置的一种结构,里面存放了所有JVM在管控这个方法链的过程中需要的信息。
    在我们利用IDE的进行代码debug的过程里,其实应该可以根据一些debug控制台的信息看到这个栈帧的实例的,从这些实例中,我们可以察觉出一些栈帧结构的端倪。

    每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

    这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。
    局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
    虚拟机使用多大的内存空间来实现一个变量槽完全是虚拟机自行决定的。
    方法运行期间不会改变局部变量表的大小。

    --疑问:如果是虚拟机自行决定一个变量槽占用多少,那long和double类型的一定是两个变量槽?

    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展[2],当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

    HotSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机倒是可以。所以在HotSpot虚拟机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的。
    《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它。
    ------------------------------------------------------------------------------
    Java Heap

    1.Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”现在已经能看到些许迹象表明日后可能出现值类型的支持。但即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
    2.Java堆是垃圾收集器管理的内存区域。Java虚拟机也没有指定JVM中一定要按照经典分代来设计垃圾回收的设计风格,到了现代,很多垃圾收集器已经不采用这种设计风格了。
    3.所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。但无论如何划分,Java堆中存放的都是对象的实例。对堆进行划分,也只是为了提升效能,或者是更好地回收,或者是更快的分配内存。
    4.Java堆处于一个不连续的物理内存空间上,但应该被视作一个整体。大的对象和数组一般是连续存放的。
    5.Java堆可以固定大小也可以动态扩展。这个主要看JVM怎么去实现。如果没法动态扩展,且堆占用也超出了上限,会报OOME。
    ———————————————————————————————
    方法区/运行时常量池

    类型信息(版本,字段,方法、接口,常量池表(在编译时放入各种字面量与符号引用,类加载后放入运行时常量池中)),常量,静态变量,即时编译器的缓存代码。
    具体怎么实现方法区不受虚拟机规范的掌控,很多JVM是没有永久代这个说法的。永久代有默认的上限大小,会造成溢出。Hotspot在JDK1.8时完全放弃了永久代,改用元空间来替代(Meta-space)。
    虚拟机允许这个区域不做垃圾收集。但是很多JVM还是会处理成有垃圾回收,主要针对的是常量池的回收和类型的卸载。
    这个区域如果无法满足新的内存分配需求,会报OOM。

    运行时产生的常量也会被加入运行时常量池中。如String的intern()方法。

    ———————————————————————————————
    直接内存
    --DirectByteBuffer
    在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区
    (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的
    DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了
    在Java堆和Native堆中来回复制数据。

    在设置内存大小时(-Xmx)时要考虑堆外内存,不能把-Xmx设置得接近物理极限。否则动态扩展时也会曝出OOME。
    ———————————————————————————————
    Hotspot虚拟机对象

    对象的创建--普通对象new时,在JVM会发生什么?
    1.JVM遇到new指令时,检查指令参数是否能定位到符号引用,且符号引用代表的类是否已经被JVM加载解析初始化过;(没有则先执行此过程)
    2.为新生对象分配内存--指针碰撞的分配方式/空闲列表的分配方式
    》采用哪种方式由GC算法决定,如果是带压缩整理过程的,那么可以采用指针碰撞的方式;如果是不带压缩整理过程,则需要采用空闲列表的方式
    》安全问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
    一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
    3.将分配到的内存空间(不包括对象头)初始化为零值。保证对象实例的字段在Java中可以不赋值就使用(默认值)。
    4.必要的设置:类信息,元数据信息,哈希码,GC分代年龄...放入对象头(Header)中。
    5.构造函数的执行(Class文件中的<init>()方法),执行完这一步,才算完全地构造出一个新的对象。(这一步由new指令后面是否跟着invokespecial指令决定,Java编译器会在new关键字的地方同时生成两条指令,如果是通过其它方式产生则不一定如此)。

    --符号引用(String s = new String("123");//s即符号引用)

    ------------------------------------------------------------------------------

    对象内存布局
    对象头(Header),实例数据(Instance Data),内存填充(Padding)
    对象头部分1:自身的运行时数据。如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。长度在32bits或者64bits。Mark Word。为了在极小的空间内尽量多的存储数据,在对象状态不同时,这个Mark Word的内部内容是不同的。
    对象头部分2:类型指针。指向类型元数据的指针。(并不是所有的JVM都这么设计的)
    另外,如果对象是个数组,那么Header中会有一块内存用于记录数组长度的数据。

    实例数据:存储顺序会收到-XX和字段在Java代码中定义顺序的影响。无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
    相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
    对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

    ------------------------------------------------------------------------------
    对象的访问定位:
    如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
    如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。

  • 相关阅读:
    单链表
    白话经典算法系列之中的一个 冒泡排序的三种实现
    QoS令牌桶工作原理
    BackTrack5 (BT5)无线password破解教程之WPA/WPA2-PSK型无线password破解
    [Django] Base class in the model layer
    MATLAB中导入数据:importdata函数
    联想A798T刷机包 基于百度云V6 集成RE3.1.7美化版 精简冗余文件
    改动symbol link的owner
    利用HttpOnly来防御xss攻击
    【NOIP2014 普及组】螺旋矩阵
  • 原文地址:https://www.cnblogs.com/bruceChan0018/p/15043802.html
Copyright © 2011-2022 走看看