JVM学习——2.1 JVM运行时数据区域

14 篇文章 1 订阅

JVM数据区域

关于JVM运行的时候对内存的划分因为不同的虚拟机可能会有略微差异,甚至因为JDK版本的不同也会有差异。但是总体的结构差别不大。只有了解了JVM对内存的使用,才能出现内存泄漏或者溢出的情况下尽快排查出问题。

程序计数器

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。
字节码解释器工作时就是通过改变这个计数器的值来选
取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需
要依赖这个计数器来完成。

由于JAVA虚拟机实现多线程的时候通过线程的切换并分配处理器资源来实现的,这就要求线程在切换后能够恢复到正确的执行位置。
这就要求每条线程可以保持其执行的位置。

所以每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

但是注意,程序计数器只能记录JAVA代码编译后的字节码。

所以可以知道程序计数器有以下特征

  • 线程隔离性,每个线程工作时都有属于自己的独立计数器
  • 执行java方法时可以记录其执行位置。
  • 如果正在执行的是Native方法,这个计数器值则为空(Undefined)
  • 因为只用来记录位置所以保存的信息和占用的内存都不算多
  • 程序计数器的内存区域,也是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

Java虚拟机栈 (JAVA Virtual Machine Stacks)

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时
都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口
等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出
栈的过程。

虚拟机栈中局部变量表存放了编译期可知的各种数据:

  • 基本数据类型(boolean、byte、char、short、int、float、long、double)
  • 对象引用(reference类型,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
  • returnAddress类型(指向了一条字节码指令的地址)

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据
类型只占用1个

Java虚拟机栈有以下特性

  • Java虚拟机栈也是线程私有的
  • Java虚拟机栈生命周期与线程相同
  • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时需要在帧中分配多大的局部变量空间是完全确定的

异常

在Java虚拟机规范中,对这个区域规定了两种异常状况

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈 (Native Method Stack)

类似于虚拟机栈不过虚拟机栈为虚拟机执行Java方法服务,地方法栈则为虚拟机使用到的Native方法服务。

在虚拟机规范中对本地方法栈中方法使用的语言、使用方式
与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如
Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法
栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

Java堆 (JAVA Heap)

JAVA堆唯一目的就是存放对象实例,他是JVM中使用内存中最大的一块了,也是GC重点管理的区域。

为了方便GC处理JAVA内部还会被细分再次细分。根据存活时间会被细分为新生代和老生代;根据使用功能会被细分为Eden区、From区、To区。

此外还会给每个线程分配一个线程分配缓冲区(Thread Local Allocation Buffer, TLAB)。

此区域存在的意义是。对象在虚拟机中创建的很频繁,但是此时JVM并不能保证对象创建的线程安全。可能存在再给一个对象分配内存的时候,还没有完成指针修改,另外一个对象又使用指针分配了其他内存。这个时候将对象的创建放在每个线程所属的内存中。实现线程封闭。然后使用CAS和重试保证数据更新的安全。

JAVA堆的特性

  • JVM管理的内存区域中最大的一块
  • 垃圾收集器管理的主要区域
  • 所有线程共享的内存区域
  • 物理上可以不连续,但是逻辑上必须连续

方法区(Method Area) 或者叫 非堆(Non-Heap)

用来储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码

过去很多人习惯把方法区叫做“永久代”。那是因为之前HotSpot使用永久代来实现方法区。这是这时候存在问题,因为这部分区域很难进行GC,这样更容易遇见内存溢出。加之Java虚拟机规范对方法区的限制非常宽松,甚至可以选择不实现垃圾回收。

为了解决内存溢出的隐患,所以在1.7版本中HotSpot已经把原本放在永久代的字符串常量池移出。

而在1.8版本中,永久代正式被移除了,将JAVA类部分的放到Java堆 (JAVA Heap)中。字符串常量和类中的静态变量放到一个新的区域中——元空间(Metaspace)。

元空间(Metaspace)

JDK 8.HotSpot JVM开始使用本地化的内存存放类的元数据,这个空间叫做元空间(Metaspace)。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

元空间出现的结果

  • 现在大多数的类元数据分配在本地化内存中。
  • 我们用来描述类的元数据的klasses已经被移除。
  • 一些各种各样的数据已经转移到Java堆空间。这意味着未来的JDK8升级后,您可能会发现Java堆空间的不断增加。

元空间的特性

  • 默认情况下,类元数据分配受到可用的本机内存容量的限制

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table)

运行时常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池另外一个重要特征是具备动态性,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存的限制。也会出现OutOfMemoryError异常

对象

对象分配、布局和访问的全过程。

对象的创建

  1. 虚拟机遇到一条new执行的时候,先去检查这个指令的参数能否在常量池中定位到一个类的符号引用。
  2. 假如定位到,检查符号引用代表的类是否已被加载、解析、初始化过。
  3. 如果没有,必须先执行相应的类加载过程。
  4. 类加载通过后,为新生对象分配内存。

分配内存的策略:

  • 假如仅仅是将指针向空闲空间那边挪动一段与对象大小相等的距离,这种方式交“指针碰撞”
  • 但是假如内存不是连续的,此时虚拟机就必须维护一个列表用来记录内存状态,这种方式为“空闲列表”

TLAB
就像在Java堆 (JAVA Heap)中说的,因为创建对象的过程并非是线程安全的,为了保证对象创建安全,所以对象的创建其实是在TLAB上。只有TLAB用完并分配新的TLAB时,才需要同步锁定。当然这个选择可以通过-XX:+/-UseTLAB参数来设定。

此时对于JVM对象已经创建完成,但是对于JAVA,对象创建才刚刚开始,其init方法还没有执行,所有字段都还是初始值。之后执行init方法对象按照代码逻辑完成初始化,这样一个JAVA才算完成。

对象的内存布局

对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头主要包含的信息

  1. 存储对象自身的运行时数据(哈希值、GC年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)
  2. 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象时哪个类的实例。

ps.假如对象是一个JAVA数组,对象头中还必须记录数组长度。

实例数据

实例数据是对象真正存储的有效信息。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。

对齐填充

此部分不是必然存在,由于HotSpot VM的内存管理系统要求对象起始地址必须是8字节的倍数,因此当对象实例数据部分没有对齐,那需要通过对齐填充来补全。

对象的访问定位

目前主流的访问方式有使用句柄和直接指针两种

句柄
句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,JAVA栈的本地变量表中reference存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

指针

直接指针访问,JAVA栈的本地变量表中reference中存储的直接就是对象地址。

两种对象访问方式比较

使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

总结

看了上面的说明,再看看内存分布图会更加清晰。

1.8之前

在这里插入图片描述

1.8

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值