天天看点

深入java虚拟机 第四版_深入理解Java虚拟机——Java内存区域

1.运行时数据区域

深入java虚拟机 第四版_深入理解Java虚拟机——Java内存区域

由上图可知,运行时的内存划分为线程共享的数据区和线程隔离的数据区。

  • 程序计数器:是一块较小的内存空间,它是当前线程所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值 选取当前线程中下一条需要执行的字节码指令 。Java虚拟机的多线程是通过 线程轮流切换并分配处理器执行时间的方式 实现的,在一个确定的时刻一个处理器只执行一个线程中的指令, 每个线程有一个独立的计数器 ,线程间隔离。(如果是Native方法,计数器值为空)
  • Java虚拟机栈:Java虚拟机栈也是 线程私有的 ,它的生命周期与线程相同。每个方法在执行的同时都会创建一个 栈帧 用于存储 局部变量表,操作数栈,动态链接,方法出口 等信息,每个方法从调用直至执行完成,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。从大的角度来分,Java内存区分为堆内存和栈内存,这里的栈说的就行虚拟机栈。局部变量表存放了编译期可知的各种 基本数据类型和对象引用 当一个方法进入时,需要在帧中分配多大的局部变量空间是完全确定的。 在线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError。
  • 本地方法栈:它的作用与Java虚拟机栈类似,只不过把字节码换为Native方法。
  • Java堆:对于大多数应用来说,堆内存是Java虚拟机所管理的内存中最大的一块,它是 线程共享 的,它的唯一目的是 存储对象实例 ,几乎所有对象实例都在这分配内存。堆内存是垃圾收集器管理的主要区域,因此也被称为 GC堆
  • 方法区:它也是线程共享的区域。它用于存储已被虚拟机加载的 类信息 常量,静态变量 ,即时编译器编译后的代码数据。Java虚拟机规范把方法区描述为堆的一个逻辑部分,根据虚拟机实现不同,垃圾回收器可以选择是否实现对这部分的垃圾回收,但这部分的垃圾回收需求较小。( 常量池的回收 )运行时常量池是方法去的一部分。Class文件中除了由类的版本,字段,方法,接口等描述外,还有常量池。常量不一定只有编译器才能产生,运行期间也可能放入新的常量。
直接内存

:网络通讯中(NIO)引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,避免了在Java堆和Native堆来回复制数据。

2.HotSpot

  • 对象的创建:

当虚拟机遇到一条new指令时,

首先

检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号代表的类是否已被加载,解析和初始化过。如果没有,那必须执行相应的

类加载过程

当类加载检查通过后,虚拟机为新生对象

分配内存

。内存所需的大小在类加载完后可完全确定,如果Java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,则采用一个分界点的指针表示可使用内存的起点,这种方式叫做“

指针碰撞

”。如果Java堆中内存不是规整的,已使用内存和空闲内存相互交错,那虚拟机就必须维护一个队列,记录那些内存块是可用的,这种分配方式叫做“

空闲列表

”。

还有一个需要考虑的问题是在并发情况下,分配内存不是线程安全的。一种方法是堆分配内存空间的动作进行同步处理——

采取CAS加失败重试的方式保证操作原子性

;另一种是把内存分配的动作按照线程划分在不同空间中进行,即每个线程在Java堆预先分配一小块内存(称为本地线程分配缓冲,TLAB)。哪个线程需要分配内存就为在哪个TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁定。

内存分配结束后

,虚拟机将分配到的内存空间初始化零值,如果使用TLAB,这一工作可提前到TLAB分配时进行,这一步保证了对象的

实例字段

在Java代码中可以不赋初值直接使用。

(局部变量不可以!) 接下来,

虚拟机对对象进行必要的设置,如这个对象是哪个类的实例,对象哈希码等等。这些信息放在对象头中。

最后

,执行<init>方法初始化所有字段。这样一个真正可用的对象就产生了。

  • 对象的内存布局

在HotSpot中,对象在内存中存储的布局可以分为三部分:

对象头,实例数据和对齐填充

对象头包括两部分信息:第一部分存储对象自身运行时的数据,如哈希码,GC分代年龄,线程持有的锁等,另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。

实例数据部分是对象真正存储的有效信息,也就是各种类型的字段内容,无论是从父类继承的还是子类定义的。相同宽度的字段总是被分配到译器,父类变量会在子类之前。

对齐填充仅起占位符的作用,对象大小必须是8字节的整数倍。

  • 对象的访问定位

我们在Java程序中通过栈上的reference数据来操作堆上的具体对象,目前主流的访问方式由使用句柄和直接指针两种。

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

深入java虚拟机 第四版_深入理解Java虚拟机——Java内存区域

如果是直接指针,那么reference存储的就直接是Java堆中的对象地址。

深入java虚拟机 第四版_深入理解Java虚拟机——Java内存区域

使用句柄的好处就是在对象被移动时只需改变句柄中的实例数据指针,而reference不需改变;直接指针访问方式就是速度快,它节省了一次指针定位的时间开销。HotSpot就是采用的直接指针访问。

参考资料:《深入理解Java虚拟机》

继续阅读