JVM内存结构

JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。
JVM运行时数据区:
  1、堆
    虚拟机中最大的一块内存区域是线程共享的内存区域,用于存放对象的实例,数组内存在此分配(所有的对象实例和数组都在堆上分配),可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
    堆内又划分为新生代和老年代,默认比例为1:2,新生代又分为Eden, Survivor from,Survivor to区,默认比例为8:1:1
    
    堆内存由各子线程共享使用,堆由垃圾收集器自动回收。
 
  2、栈
    JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。
    栈中的元素用于支持虚拟机进行方法调用,每个方法执行的时候都会在栈中创建一个栈帧,每个方法从开始调用到执行完成的过程,就是对应着从入栈到出栈的过程。在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。
 
    栈帧包括局部变量表、操作栈、动态连接、方法返回地址等
      1)局部变量表:(存放局部基本类型变量的值和引用类型在堆中的地址值,方法参数),方法的返回值等信息
      2)操作数栈:操作栈是一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写人和提取信息
      3)动态连接:每个栈帧 中包含一个在常量池中对当前方法的引用 , 目的是支持方法调用过程的 动态连接。
      4)方法返回地址(方法出口):方法的退出的过程相当于弹出当前栈帧,方法执行结束将返回方法当前被调用的位置
 
  3、元空间
    JDK1.7及之前使用方法区,方法区内包含:类信息(类名,访问修饰符、字段描述、方法 描述等)、常量、静态变量、即时编译后的class文件等。在GC时用永久代来实现方法区
      
    JDK1.8取消了方法区,取而代之的是元空间,并将字符串常量池移到堆内存中,其他内容包括类元信息、字段、静态属性、方法,常量等都移动到元空间内。
 
   运行时常量池:
    是方法区的一部分,存放编译期生成的各种字面量和符号引用(字面量就是实际的值,如1,"abc",符号引用是不知道实际引用对象的实际地址而抽象出的一种引用)。

    字面量如:文本字符串,声明为final的常量值;
    符号引用包括了三种常量,分别是:类和接口的全限定名,字段的名称和描述符,方法的名称和修饰符

 

  4、本地方法栈
    线程私有,主要用于执行本地Native方法
 
  5、程序计数器寄存器
    线程私有区域,用于记录当前线程下虚拟机正在执行的字节码的指令地址。
    CPU 只有把数据装载到寄存器才能够运行。寄存器存储指令相关的 现场信息,由于 CPU 时间片轮限制,众多线程在并发执行过程中,任何一个确定的 时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。 这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和枝帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常
    
 
 
直接内存:Direct Memory
  Direct Memory容量可通过-XX:MaxDirectMemorySize指定,如果不指定则默认和Java堆的最大值-Xmx一样。
  不受到Java堆内存大小的限制,只会受到计算机总内存(包括RAM以及SWAP或者分页文件)大小以及处理器寻址空间的限制,若直接内存和JVM各个区域占用总内存大小超过物理内存限制则会出现OutOfMemoryError。
 
NIO:JDK1.4引入的一种基于通道channel和缓冲区Buffer的IO方式;它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据,因此能在一些场景中显著提高性能。
 

使用堆外内存的原因

  • 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
  • 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

 

 

    

 
 
Java堆中对象分配,布局和访问过程
1、对象创建(普通Java对象,不包括数组和Class对象)
  虚拟机遇到一条new指令时,首先去常量池中检查这个指令的参数能否定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过,如果没有先执行类的初始化过程。
   在类加载检查通过后,对象所需的内存的大小在类加载完成后便可完全确定,接下来虚拟机将为新生对象分配内存,即把一块确定大小的内存从堆中划分出来。
  (若堆中内存是规整的,即用过的和空闲的各放在一边,指针指向分界点,内存分配即指针向空闲移动对象所需的内存大小。这时内存分配采用指针碰撞(bump the pointer)的方式
  若堆中的内存是不规整的,虚拟机需维护一张列表记录哪些内存块是可用的,在分配的时候找到一块足够大的内存块划分给对象,并更新列表,这种方式称为空闲列表(free list))
  因此使用哪种方式取决于堆内存是否规整,是否规整又取决于采用的垃圾收集器是否带有压缩整理功能决定。因此采用Serial、ParNew采用的是指针碰撞,采用CMS收集器使用的是空闲列表。
 
  给对象分配内存需要考虑线程安全的问题,需要同步进行,避免两个对象分配一块内存;解决这个问题有两种方案:
  1、对内存分配空间的动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方式保证更新的原子性
  2、把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),线程先在TLAB上分配,
  只有TLAB用完分配新的TLAB时才需要同步锁定。
  内存分配完成后JVM将分配到的内存空间都初始化为0值,然后进行必要的设置,类的元数据信息,对象的哈希码,对象的GC分代年龄都存在对象头中,最后进行init初始化。
 
 2、对象的内存布局
  在HotSpot 虚拟机中,对象在内存中的存储被分成了3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
  对象头分成两部分:
    1、用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
    2、类型指针,即对象指向它的类元数据的指针。JVM通过这个指针来确定这个对象是哪个类的实例。如果是数组对象,对象头中还需记录数组的长度
  实例数据:对象真正存储的有效信息,即是代码中定义的字段内容,包括从父类继承下来的和自身定义的
  对齐填充:并不是必然存在的,仅仅起着占位符的作用。JVM要求对象的大小必须是8字节的整数倍。
 
3、对象的访问定位
  建立对象是为了使用对象,Java程序通过栈上的reference数据来操作堆上的具体对象。reference只是指向对象的一个引用,如何访问定位对象在堆上的具体位置呢?
  1、句柄:Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息。
    好处是稳定:如垃圾收集时若对象被移动,只需修改句柄中的实例数据指针,reference无需修改。
  2、直接指针:reference中存放的直接就是对象地址。好处是速度快,节省了一次指针定位的开销,HotSpot就是通过这种方式访问的。
 
4、Java对象模型
 
 
  线程共享:堆,方法区(常量池)
  线程私有:栈,程序计数器,本地方法栈
 
END.
posted @ 2020-03-18 09:48  杨岂  阅读(275)  评论(0编辑  收藏  举报