JVM - 内存模型

原创
2017/08/21 10:51
阅读数 9

内存模型

程序计数器

当前线程所执行的字节码的行号指示器。通过改变计数器的值来选取下一条执行的字节码指令。每条线程都有独立的程序计数器,线程间互不影响。如果正在执行的是Native方法,则计数器为空。该区域是唯一一个没有没有规定OutOfMemeryError的区域。

 

Java虚拟机栈

线程私有它的生命周期与线程相同。每个方法在执行的同时会创建一个栈帧用于存储局部变量、操作数栈、动态链接、方法入口等信息。每个方法从调用直到执行完成的过程,对应这栈帧在虚拟机栈中入栈到出栈的过程。

    局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、double)、对象引用和returnAddress类型。 

    64位的long和double站两个局部变量空间,其余的站一个。局部变量表所需的空间在编译器完成分配。

    该区域规定了两种异常:StackOverflowError异常、OutOfMemoryError异常。  

 

Java堆

是JVM管理的最大一块内存。堆是被所有线程共享的一块内存,在虚拟机启动时创建。该内存区域的唯一目的就是存放对象实例。几乎所有的对象都在这里分配内存(所有的对象以及数组都要在堆上分配)。从内存分配的角度看,线程共享的堆可能划分出多个线程私有的分配缓冲区(TLAB)。 

 

方法区

线程共享,用于存放JVM加载的类信息、常量、静态变量、即时编译器编译的代码等数据。HotSpot将GC分代收集扩展至方法区,或者说用永久代来实现方法区而已,JDK1.7已经将原本存放在永久代的字符串常量池移出。

 

运行时常量池

是方法区的一部分。Class文件除了有类的版本、字段、方法、接口等信息,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

    常量池的另外一个重要特征是具备动态性,jvm并不要求常量一定只有在编译期才能产生,运行期间也可能将新的常量放入池中,比如String类的intern()方法。

 

直接内存

并不是JVM运行时的数据区的一部分,但这部分内存被频繁地使用。 JDk1.4加入了NIOl类,引入通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景显著提高性能。

可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与对大小(-Xmx)一样

 

 

对象

对象的创建

  • 检查new指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果过没有,那么先执行类加载过程。
  • 为新生对象分配内存。对象所需的内存大小在类加载完成后可完全确定,在Java堆中分配确定大小的空间。假设堆内存是绝对规整的,占用的内存在一遍,空闲的内存在另外一边,中间放着一个指示器,那所分配内存仅仅是将指示器挪动对象大小的距离,这种方式称为“指针碰撞”。如果Java堆不规整,JVM就必须维护一个列表,记录哪些内存是可用的,分配的时候在列表中找到一块足够大的内存划分给对象实例,这种分配方式称为“空闲列表”。选择哪种方式取决与垃圾回收器是否带有压缩功能。
  • 内存划分还要考虑对象创建是否频繁的操作,即使是一个一个指针的修改,在并发情况也并不是线程安全的。解决方案有两种,一种是分配内存空间的动作进行同步处理--实际上JVM采用CAS配上失败重试的方案保证更新操作的原子性;另外一种是内存的分配按照线程划分在不同的空间进行,即每个线程在堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。只有在TLAB用完并分配新的TLAB时,才需要同步。虚拟机是否使用TLAB,可通过-XX:UserTLAB参数来决定。
  • 内存分配后,虚拟机需要将分配的内存空间初始化为零值(不包括对象头),如果采用TLAB,这部分工作也可以提前至TLAB分配时进行。
  • 虚拟机需要对对象进行必要的设置,例如对象是哪个类的实例。如何才能找到类的元信息、对象的哈希吗、对象的GC分代年龄等。这些信息存放在对象的对象头中。
  • 一个新的对象已经产生,但从java程序的角度看,对象创建才刚刚开始----<init>方法还没有执行,所有的字段还是零。根据字节码后是否跟有invokespecial指令所决定,执行new指令后会接着执行<init>方法。

对象的内存布局

在HotSpot中,对象在内存中的布局划分为3个区域:对象头、实例数据和对齐填充。

对象头包括两部分信息,第一部分用于存储对象自身运行的数据,如哈希吗、GCf分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32和64位的虚拟机中分别为32bit和64bit,官方称为“Mark Word”。对象需要存储的运行时数据已经超出32bit和64bit,    Mark Word被设计为一个非固定的数据结构以便在极小的空间存储尽量多的信息;对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过该指针确定这个对象是哪个类的实例。另外,如果对象是数组,在对象头中还需要记录数组长度。

对象真正存储的有效信息。无论是从父类继承下来的还是子类定义的,都需要在这里记录下来。  HotSpot虚拟机默认的分配策略为long/doubkle、ints、shorts/chars、bytes/boolean、oops(Ordinary Object Pointers),从分配策略上看,相同宽度的字段总是被分配到一起。在满足这个前提下,父类定义的变量会出现在子类前。如果CompactFields参数为true(默认为true),那么子类之中的变量可能会插入到父类变量的空隙中。

第三部的对齐填充并不是必要的。HotSpot VM 自动管理要求对象起始地址是8字节的整数倍。

对象的访问定位

java 程序通过栈上的reference数据来操作堆上的具体对象,jvm规范只规定了一个指向对象的引用,并没有规定这个引用通过何种方式去定位、访问堆中的对象的具体位置。目前主流的访问方式有使用句柄和直接指针两种。

如果使用句柄访问的话,堆中将会划分出一块内存作为句柄池,reference中存储的就是句柄地址,而句柄中包含对象实例数据与类型数据各自的具体信息。 

如果使用直接指针访问,那么堆对象的布局就必须考虑如何放置访问类型数据的相关数据,而reference中存储的就是对象的直接地址。

这两种对象访问方式各有优势,使用句柄的最大好处是reference中存储的是稳定的句柄地址,在对象移动(垃圾回收)时只会改变句柄中的实例数据指针。

使用直接访问的最大好处是速度更快,HotSpot使用第二种。

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部