java虚拟机运行时数据区域与虚拟机对象

一、运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,已经创建和销毁时间,有的区域随着虚拟机进程的启动而创建,有些区域则依赖用户线程的启动和结束而创建和销毁。
在这里插入图片描述

1、程序计数器

它是一块较小的内存空间,可看做线程执行字节码的行号指示器。字节码解释器通过改变计数器的值来选取下一条要执行的指令。
每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,所以这类内存是线程私有内存。

2、Java虚拟机栈

Java虚拟机栈也是线程私有的。虚拟机栈描述的是java方法执行的内存模型,方法在执行时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

其中局部变量表就是常说的Java内存区中的栈内存。这里存放了编译期可知的各种基本数据类型、对象引用类型和returnAddress类型。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间使完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对这个区域规定了两种异常状况。
1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
2)如果虚拟机可以动态扩展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。(常讨论的异常是这个,因为大多数虚拟机都可以动态扩展)

3、本地方法栈

这个是非常类似Java虚拟机栈的。区别是Java虚拟机栈用于Java方法,本地方法栈用于虚拟机用到的本地方法。
Hotspot虚拟机已经把这两个合二为一了。

4、Java堆

java堆是Java内存中最大的一块,它是被所有的线程共享的一块内存区域,在虚拟机启动时创建。
Java堆的唯一目的就是存放对象实例,为对象实例分配内存,以及进行垃圾回收管理。Java堆也可以叫做“GC堆”。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的就可以。如果在堆中没有内存来完成实例分配,并且这个堆也不能扩展时,将会抛出OutOfMemoryError异常。

5、方法区

方法区域是在虚拟机启动时创建的,并在所有Java虚拟机线程之间共享,并且在逻辑上是堆区域的一部分。
它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码。Hotspot设计团队把GC分代收集扩展到方法区,这样垃圾收集器就可以将管理Java堆一样管理方法区的内存,但这样容易遇到内存溢出问题,所以在jdk1.7中,已经把字符串常量池移出。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

6、运行时常量池

常量池用于存放编译期生成的各种字面量和符号。java虚拟机对运行时常量池没有做什么细节的要求,不同的虚拟机可以按照自己的需求来实现这个内存区域。运行时常量池还有个动态性的特征,不一定要在编译器才能产生。
当常量池无法再申请内存时,将抛出OutOfMemoryError异常。

7、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也可能导致OutOfMemoryError异常出现。
在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方法,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

二、Hotspot虚拟机对象
1、对象的创建

虚拟机在遇到一条new指令时,首先回去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,虚拟机将为新对象分配内存。对象内存的大小在类加载后就可以完全确定,为对象分配内存等同于把一块确定大小的内存从Java堆中划分出来。有两种分配方法:
1)“指针碰撞”:通过将指针向空闲空间挪动一段与对象大小相等的距离
2)“空闲列表”:虚拟机维护一个列表,记录上哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表记录。

对于分配内存是不安全的解决方案:
1)进行同步处理:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
2)每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有在TLAB用完并分配新的TLAB时,才需要同步锁定。

内存分配完成后,虚拟机需要将分配的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这个过程也可以提前到TLAB分配时进行。这一步操作保证了对象的实例字段可以在Java代码中不赋初始值就可以使用,程序能访问到这些字段的数据类型所对应的的零值。

接下来,再对对象进行必要的设置。一个新的对象以及产生了,然后还需要对执行方法,把对象按照程序员的意愿进行初始化,这样,一个对象就创建成功,可以使用了。

2、对象的内存布局

在Hotspot虚拟机中,对象的内存布局可以分成三块:对象头、实例数据、对齐填充

  • 对象头。对象头又包括两部分信息
    第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。官方称他们为“Mark Word”.
    考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的信息,它会根据对象的状态服用自己的存储空间。
    第二部分是类型指针,即指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
    如果对象是一个Java数组,那么在对象头中还必须有一块用于记录数组长度的数据,虚拟机将通过这个来确定Java对象的大小(因为数组的元数据中无法确定数组的大小)。

  • 实例数据
    这是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。Hotspot虚拟机默认的分类厕所是把长的放前面,宽度相同的放到一起。父类定义的变量在子类之前,如果compactField参数值为true(默认为true),子类中较窄的变量可能在父类之前。

  • 对齐填充
    这个不是一定需要的,它只是站位的作用。虚拟机规定对象的大小必须是8字节的整数倍,因为对象头正好是8字节的整数倍,所以就取决于实例数据,如果实例数据没有对齐,就需要用对齐填充来补全。

3、对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象。reference类型在java虚拟机规范中只规定了一个指向对象的引用,主流的访问方式有两种。

1)使用句柄
java堆中将会划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包括了对象实例数据与类型数据各自的具体地址信息。
优点:在对象被移动时只会改变句柄中的实例数据指针,reference本身不需要改变
2)直接指针访问
reference中存储的直接就是对象地址。
优点:访问速度快,因为他节省了一次指针定位的开销。Hotspot虚拟机就是用的这种方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值