zoukankan      html  css  js  c++  java
  • 《Understanding the JVM》读书笔记之一——JVM内存模型

    一、JVM内存模型——概念说明

      1. 程序计数器
        • 程序计数器:内存占用很小,是当前线程所执行的字节码的行号指示器,每一个线程都需要一个独立的程序计数器。
        • 如果该线程正在执行java方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是native方法,则这个计数器的值为空。
        • *程序计数器是唯一一个再Java虚拟机规范中没有outofmemory情况的区域
      

      2. 虚拟机栈
        • 虚拟机栈内存中的数据与线程的生命周期相同。
        • 在每个线程中,每个方法执行的同时都会在这里(虚拟机栈)创建一个栈帧,每一个方法从调用到执行完成都对应一个栈帧的入栈和出栈过程。
        • 当所有栈帧都出栈则代表该线程的所有方法都执行完成,线程执行完毕
        • 每个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。
        • **异常:如果线程请求的栈深度 > jvm允许的最大栈深度,抛StackOverflowError;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,抛OutOfMemoryError;
      

      3. 本地方法栈
    • 当虚拟机执行Native方法时使用本地方法栈,功能和虚拟机栈相似
    • 在Sun的HotSpot虚拟机中,将本地方法栈和虚拟机栈合并了

      4. 堆
        • 用于存放(几乎)所有的对象实例(不是绝对,在栈上分配、标量替换技术上不是),也被称为GC堆(因为是垃圾收集器管理的主要区域)
        • Java堆内存只要逻辑上连续即可,物理上可以不连续
        • 当堆中已经没有内存用于存放新的对象时(堆也无法扩展时),会抛出OutOfMemoryError
        • 可以通过-Xmx和-Xms控制堆内存大小

      5. 方法区
        • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。
        • 一般来讲,不会对这个区域进行垃圾回收(GC),因为回收的效率不高,只能回收常量池(jdk1.7以前)和进行类型卸载,但类型卸载的条件相当高
        • 当方法区无法满足内存分配的需求时会抛出OutOfMemoryError

      6. 常量池

        • 在jdk1.7之前,运行时常量池包含字符串常量池,保存在永久代(方法区)中。
        • 在jdk1.7之后,运行时常量池中的字符串常量池被存放在堆内存中,运行时常量池中的其他内容仍然放在方法区中。

        • 在jdk1.8之前,方法区称为“永久代”,在1.8之后,用元空间取代了永久代,此时字符串常量池还在堆中,运行时常量池还在方法区。

      7. 直接内存
        • 这部分不是虚拟机运行时数据中的一部分
        • 在NIO包中的通道Channel中会使用Native函数库直接申请堆外内存,这样能显著提升性能
        • 在通过-Xmx等参数分配虚拟机内存时,需要考虑直接内存,不要出现jvm各个区域内存综合大于物理内存的情况,这种情况会抛出OutOfMemoryError

     

    二、对象的创建过程

    当JVM检测到new语句时,会按照如下过程进行对象的创建:

      1. 检查这个指令的参数是否能在常量池中定位到类的符号或引用,并检查这个对象代表的类是否已经被加载、解析和初始化?如果没有,需要先执行类的初始化过程;
      2. 类加载完成后,对象所需要的内存大小已经确定,这时候虚拟机将为新创建的对象分配内存。有两种分配方式:“指针碰撞”和“空闲列表”,具体选择哪一种方式由所采用的垃圾收集器是否具有压缩整理功能决定。
      3. 内存分配完成后,虚拟机将分配到的内存空间都初始化为0(不包括对象头),如果是TLAB,这一操作会提前到TLAB分配时执行。
      4. 虚拟机对对象进行必要的设置,包括:属于哪个类、元数据信息位置、哈希码、GC分代年龄等,这些数据都保存在对象头中。
      5. 以上工作完成后,对虚拟机来说一个新的对象已经产上了,但对于程序来说,对象的创建才刚刚开始。
    接着执行<init>方法,将对象初始化。

    整个过程总结如图:

      

    三、对象在内存中的布局

    对象在内存中的布局可以分为3块:对象头,实例数据,对齐填充;
      1. 对象头(Header),包含两部分
        • 第一部分用于存储对象自身的运行时数据,包括:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
        • 第一部分数据长度为64bit(在32位虚拟机中为32bit),官方称为MarkWord。
        • 第二部分是类型指针(指向类数据的指针),通过这个指针来确定这个对象属于哪个类实例。
        • 如果对象是一个数组,在对象头中还需要一块用于记录数组长度数据的空间。
      2. 实例数据(InstanceData)保存对象真正存储的有效信息,也就是在代码中定义的各种类型的字段内容(包括在父类继承的和类本身的)。
        • 数据的存储顺序受到虚拟机分配策略参数FieldsAllocationStyle和字段在java源码中的定义顺序影响(longs/doubles,ints,shorts/chars,bytes/booleans,oops)
        • 一般在父类中定义的参数会在前边存储,如果参数CompactFields参数设置为true,则子类中长度较小的变量可能会插入到父类的变量空隙中。
      3. 对齐填充(Padding),这部分不是必要的。
        • 因为在hotspot虚拟机中要求,对象的起始地址必须是8字节的整数倍,而对象头正好是8字节的整数倍,因此,在当对象实例数据部分没有对齐时,用对其填充进行补齐。

    内容总结如图:

      

    四、对象的访问定位
      Java程序通过栈上的reference数据来操作堆上的具体对象,对象的具体访问方式取决于虚拟机的实现,主流的访问方式有句柄访问和直接指针访问两种,hotspot虚拟机使用直接指针访问的方式。
      • 句柄访问:在Java堆中划分出一块区域作为句柄池,本地变量表中存储的实际上是对象的句柄池地址,而句柄中记录了对象的实例数据和类型数据的具体地址。
      • 直接指针访问:reference中存储的就是对象的直接地址。

      句柄访问的好处是:reference中存储的是稳定的句柄地址,在对象被移动时,只改变句柄中的实际地址,reference本身不需要修改。
      直接指针访问速度块,节省了一次指针定位的时间,在对象的数量多时会节省大量时间

    END====解释
      内存分配方式:

        • 指针碰撞——当Java堆中的内存是规整的,所有有对象的内存放在一边,空闲的内存放在另一边,中间通过一个指针作为分界点的指示器,那么这时为对象分配内存的操作就是把指针向空闲的一端移动对应的距离即可。
        • 空闲列表——当Java堆中的内存是不规整的,已使用和未使用的内存交叉在一起,这时候虚拟机必须维护一个列表,来记录那些内存时可用的那些内存是不可用的,在分配对象内存时直接在表中选择一个足够大的内存划分给该实例,并维护列表即可。
        • 在使用Serial、ParNew等带有Compact功能的收集器时系统采用的是“指针碰撞”,在使用CMS的收集器通常采用“空闲列表”的方式(CMS基于Mark-Sweep算法)。
        • 在分配内存空间时还需要考虑另外一个因素:在并发的情况下,可能正在给A对象分配内存,指针或列表还没来的及修改,对象B又同时使用了本来分配给A的内存位置,解决这个问题有两种方案。
        方案一:虚拟机采用CAS+失败重试的方式来保证分配内存操作的原子性;
        方案二:把内存分配的动作按照线程划分在不同的空间中进行,即为每个线程在堆中分配一小块内存(称为:本地线程分配缓冲ThreadLocalAllocationBuffer TLAB),在TLAB内存用完时才需要同步锁定。可以通过参数-XX:+/-UseTLAB参数来设置TLAB的大小

      TLAB——Thread Local Allocation Buffer(线程本地分配缓冲区)
        1. 如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
        2. TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
        3. TLAB简单来说本质上就是三个指针:start,top 和end,其中start 和end 是占位用的,标识出Eden 里被这个TLAB 所管理的区域,
    而top 就是里面的分配指针。

      关于Java堆内存的说明
      由于现代垃圾收集器主要基于分代收集算法实现,因此,堆内存可以划分为:新生代和老年代。更详细的可以将新生代划分为Eden空间、FromSurvivor空间、ToSurvivor空间。
      从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。但是,无论如何划分、目的都是为了更好的回收内存或者更快的分配内存

     

    ****文章内容出自《Understanding the JVM》一书,部分内容来源于网络,部分内容为作者原创

  • 相关阅读:
    形象理解ERP(转)
    禁用windows server 2008 域密码复杂性要求策略
    How to adding find,filter,remove filter on display method Form
    Windows Server 2008 R2激活工具
    How to using bat command running VS development SSRS report
    Creating Your First Mac AppGetting Started
    Creating Your First Mac AppAdding a Track Object 添加一个 Track 对象
    Creating Your First Mac AppImplementing Action Methods 实现动作方法
    Creating Your First Mac AppReviewing the Code 审查代码
    Creating Your First Mac AppConfiguring the window 设置窗口
  • 原文地址:https://www.cnblogs.com/logic-hatten/p/11321341.html
Copyright © 2011-2022 走看看