深入理解JVM 之 自动内存管理机制(一)

一、运行时数据区域

在这里插入图片描述

1、程序计数器

程序计数器(Program Counter Register,PCR)是一块较小的内存空间,此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。PCR可以看作是当前线程所执行的字节码的行号指示器。

Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了使线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的PCR,且各条线程之间计数器互不影响,独立存储。

如果线程执行Java 方法,则PCR 记录的是正在执行的虚拟机字节码指令的地址;如果线程执行Native 方法,则PCR为空值(Undefind)。

2、Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stacks)的生命周期与线程相同。虚拟机栈为虚拟机执行Java 方法(字节码)提供服务。

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时的同时都会创建一个栈帧(Stack Frame)存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的调用执行过程对应栈帧的入栈到出站过程。

局部变量表存放编译期可知的基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型,不等同于对象本身,可能是指向对象起始地址的引用指针,或是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向一条字节码指令的地址)。

其中64位的 long 和 double 类型的数据占用2个局部变量空间(Slot)。局部变量表所需的内存空间在编译期间完成分配。【即进入方法时帧中局部变量空间是确定的,并且运行期间不改变大小】

在Java虚拟机规范中,对虚拟机栈区域规定2种异常机制:①线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;       ②虚拟机栈动态扩展时无法申请到足够的内存,抛出OutOfMemoryError异常。

3、本地方法栈

本地方法栈(Native Method Stack)为虚拟机使用到的Native 方法提供服务。与虚拟机栈作用类似(有时可能会合二为一),亦会抛出StackOverflowError异常和OutOfMemoryError异常。

4、Java 堆

Java 堆(Java Heap)是Java虚拟机管理的内存中最大的一块,其在虚拟机启动时创建。唯一目的是存放对象实例(无论如何细分,都与存放内容无关),所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术的成熟,栈上分配、标量替换优化技术等会使在堆上分配对象不再“绝对”。

Java 堆是垃圾收集器管理的主要区域,称作“ GC 堆(Garbage Collection Heap)”。Java 堆可以处于物理上不连续的内存空间中,只要保证逻辑上连续即可。如果在堆中没有内存完成实例分配,并且堆也无法扩展时,抛出OutOfMemoryError 异常。

5、方法区

方法区(Method Area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

Java虚拟机规范对方法区限制非常宽松,在堆的特性基础上增加了垃圾收集的可选性,此区域内存回收目标主要是针对常量池的回收和对类的卸载,只是效果不怎么样,但是此区域的回收是必要的。如果方法区无法满足内存分配需求时,抛出OutOfmemoryError异常。

6、运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,常量池(Constant Pool Table)用于存放编译期生成的各种字面量和符号引用,而这部分内容将在类加载后进入方法区的运行时常量池中存放。一般来说,运行时常量池中除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在其之中。

运行时常量池的另外一个特征是具备动态性,即并非预置入Class 文件中常量池的内容(Java语言不要求常量一定只有在编译期才能产生)才能进入方法区的运行时常量池,运行期间也可以将新的常量放入池中,比如 String 类的 intern() 方法。当常量池无法再申请到内存时,抛出OutOfMemoryError 异常。

Class文件简述(后面会详述…)
Class 文件中包括类的版本、字段、方法、接口等描述信息和常量池。Java 虚拟机对Class 文件每一部分的格式都有严格定义,每一个字节用于存储哪种数据都必须符合规范才会被虚拟机认可、装载和执行。

7、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是虚拟机规范定义的内存区域,但是也可能会导致OutOfMemoryError 异常。

本机直接内存的分配不会受到Java 堆大小的限制,但是会受到本机总内存(包括RAM以及SWAP区或分页文件)大小以及处理器寻址空间的限制。有时忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),导致OutOfMemoryError 异常。

二、HotSpot虚拟机对象

1、对象的创建

在这里插入图片描述
解析:
(1)为新对象分配内存
1)内存规整引发的分配方式问题
内存绝对规整,用过的内存放在一边,空闲的内存放在一边,中间用指针作为分界点的指示器,则分配内存仅把指针指向空闲空间那边挪动一段与对象大小相等的距离,此方式称为“指针碰撞”(Bump the Pointer)。

内存并不规整,已使用的内存和空闲内存相互交错,此时虚拟机必须维护一个列表,记录可用内存块,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,此方式称为“空闲列表”(Free List)。

java 堆的规整由垃圾收集器是否带有压缩整理功能决定。使用Serial、ParNew 等带Compact 过程的收集器时,分配算法采用指针碰撞;使用CMS 这种基于Mark-Sweep 算法的收集器时,分配算法采用空闲列表。

2)并发情况下的线程安全问题
①对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS 配上失败重试的方式保证更新操作的原子性;

②把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java 堆中预先分配一小块内存,称为“本地线程分配缓冲”(Thread Local Allocation Buffer,TLAB)。要为哪个线程分配内存,就在哪个线程的TLAB 上分配,只有TLAB 用完并且分配新的TLAB 时,才需要同步锁定。是否使用TLAB ,可以通过-XX:+/-UserTLAB 参数来设定。

(2)内存空间初始化为零值
如果使用TLAB,此过程可以提前至TLAB 分配时进行。这一步操作保证了对象的实例字段在Java 代码中可以不赋初始值就直接使用。

(3)设置对象
例如本对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC 分代年龄等信息。这些信息存放在对象头(Object Header)中,根据虚拟机当前运行状态不同,对象头会有不同的设置。

2、对象的内存布局

对象在内存中存储的布局分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(padding)
(1)对象头
1)用于存储对象本身运行时的数据:如哈希码、GC分代年龄、线程持有的锁、偏向线程ID、偏向时间戳等。对象头信息是额外存储成本,与对象自身定义的数据的存储无关。

Mark Word :数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit。为提高空间效率,Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
在这里插入图片描述
2)类型指针:即对象指定他的类元数据的指针,用这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针(查找对象的元数据信息并不一定要经过对象本身)。另外,对象为Java 数组,则在对象头中必须有一块用于记录数组长度的数据。

(2)实例数据
实例数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。其存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。

默认分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),在这基础上,在父类定义的变量会出现在子类之前。如果CompactFields参数为true(默认),则子类中较窄的变量也可能会插入到父类变量的空隙之中。
(3)对齐填充
仅仅起着占位符的作用,虚拟机内存管理系统要求对象起始地址必须是8字节的整数倍(对象的大小必须是8字节的整数倍),对象头部分正好是8字节的整数倍(1或2倍),因此,当对象实例数据部分没有对齐时,须通过对齐填充来补全。

3、对象的访问定位

原因:Java 程序需要通过栈上的reference 数据来操作堆上的具体对象,而reference 类型在java虚拟机上只规定了一个指向对象的引用,没有定义这个引用应该通过何种方式去定位、访问堆中对象的具体位置。因此,对象的访问方式取决于虚拟机实现而定,即主流两种方式。

(1)句柄访问
会在Java 堆中划分一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。如图:
在这里插入图片描述
(2)直接指针访问
要在Java 堆对象的布局中必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象地址。如图:
在这里插入图片描述
总结:
①使用句柄访问,优点为reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时会移动对象)时只会改变句柄中的实例数据指针,而reference 本身不需要修改。
②使用直接指针访问,优点为速度更快,节省了一次指针定位的时间开销,这种开销积少成多后是非常可观的执行成本。

JVM 内存详细解析
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值