深入理解JVM虚拟机读书笔记——自动内存管理(一)

第二章、内存区域与内存溢出异常

一、 java虚拟机运行时数据区:

在这里插入图片描述

程序计数器

一块较小内存空间,当前线程执行的字节码行号指示器。

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

java虚拟机栈

  1. 线程私有的、生命周期与线程相同。
  2. 描叙java方法执行的线程内存模型:每个方法执行时同步创建一个栈帧存储 局部变量表、操作数栈、动态链接、方法出口 等信息。每个方法调用到完毕,对应一个栈帧在虚拟机入栈出栈过程。

对比C++中的栈,通常说的就是虚拟机栈、或者只是指虚拟机栈中的局部变量表部分。

局部变量表:存放了编译期可知的各种Java虚拟机基本数据类型( boolean、byte、char、short、int、 float、long、 double)、对象引用( reference 类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnaddress 类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和 double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在械帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请读者注意,这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。

StackOverflowError 异常:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError 异常:线程申请栈失败。

本地方法栈

与虚拟机栈作用类似,为虚拟机用到的本地(Native)方法服务,即其他语言(如C、C++ 或其他汇编语言)编写,编译成和处理器相关的代码提供的方法。(HotSpot虚拟机把本地方法栈与虚拟机栈二合一)。

Java 堆

  1. 所有线程共享、虚拟机启动时创建、存放几乎所有对象实例。(Java 堆是垃圾收集器管理的区域,所以有称为GC堆,Garbage Collected Heap)。
  2. 可划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)提升对象分配效率。物理不一定连续、逻辑连续、有固定大小和可扩展。

OutOfMemoryError 异常:没有完成实力分配,并且堆也无法再扩展时。

方法区

  1. 线程共享、存储已被虚拟机加载的 类型信息、常量、静态变量、即时编辑器编译的后的代码缓存等数据。
  2. 永久代:HotSpot中早期用永久代来实现方法区,可以使HotSpot的垃圾收集器可以像管理内存堆一样管理这部分内存,有默认大小,易内存溢出。JDK6开始放弃,使用本地内存(Native Memory)实现方法区,JDK7把 字符串常量池、静态变量 等移出永久代。JDk8后HotSpot中废除,用本地内存中实现的元空间(Meta-space)代替剩余内容(主要是类型信息)。
  3. 垃圾回收在此较少出现(主要时针对 常量池 的回收和对 类型的卸载 ),但有时又是有必要的。

OutOfMemoryError 异常:方法区无法满足新的内存分配需求时。

运行时常量池(Runtime Constant Pool):方法区的一部分。Class 文件中除了有类的 版本、字段、方法、接口 等描述信息外,还有一项信息是常量池表(Constant Pool Tabke)用于存放编译期生成的各种字面量与符号引用,这部分在类加载后放到方法区的运行时常量池中。相对于 Class 常量池另一个重要特征是具备动态性,运行期间可以将常量放入池中,列如 String类 的 intern() 方法(调用此方法会先检查字符串池中是否有此字符串,有返回引用,否则添加进池中再返回此字符串引用)。
OutOfMemoryError 异常:常量池无法再申请到内存时。

直接内存

不是虚拟机运行时数据区部分,也不是《Java虚拟机规范》定义的内存区域。JDK1.4加入的NIO类引入了基于通道(Channel)与(Buffer)的I/O方式,可以用 Native函数库 直接分配堆外内存,然后用一个存储在Java堆中的 DirectByteBuffer 对象作为这块内存的引用对象进行操作。
直接内存会受到总内存(物理内存、SWAP分页或分页文件)大小以及处理器寻址空间限制。设置 -Xmx 参数时忽略直接内存可能导致内存区域综合大于物理内存限制导致动态扩展时出现 OutOfMemoryError异常 。

二、HotSpot 虚拟机对象

以最常用的虚拟机 HotSpot 和最常用的内存区域 Java堆 为例,探讨 HotSpot 虚拟机在Java堆 中对象 分配、布局和访问 的全过程。

对象的创建

语言层面创建对象通常(例外:复制、反序列化)仅仅是 new ,虚拟机中对象(仅限普通对象,不包括 数组和Class对象)的创建过程又是怎样的?

  1. 首先检查指令参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,先执行类加载过程。
  2. 类加载过后,分配内存并初始化为零。分配内存有 如何划分并发情况 的问题。
  3. 对对象进行必要设置,例如:对象是哪个类的实例、如何找到类元数据信息、对象哈希码(实际哈希码是懒操作,调用Object::hashCode()方法时才会计算)、对象的GC分代年龄等信息,信息存放
    对象头 中。
  4. 虚拟机中新对象产生了,Java程序中才刚刚开始——构造函数,Class文件中的()还没执行、对象需要的其它资源和状态信息也还没构造。在()方法初始化后,一个真正可用的对象才构造出来。

如何划分: 为对象分配空间任务实际上等同把一块确定大小的内存块从 java堆 中划分出来。分为 规整内存块不规整内存块 两种情况。

  • 规整内存块——指针碰撞: 假设 Java堆 中内存绝对规整,即使用过放一边,未使用放另一边,中间放这个指针作为分界点指示器。那分配内存仅仅是把指针移动需要的大小,称为 “指针碰撞”(Bump The Pointer)。
  • 不规整——空闲列表: 已使用和空闲内存块交错,需要维护一个列表,记录哪些内存块可用,分配时找到一块足够的分给对象,并更新表,称为 “空闲列表”(Free List)。

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理( Compact)的能力决定。因此,当使用 Serial,Parnew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除(Swp)算法的收集器时,理论上就只能采用较杂的空闲列表来分配内存。

并发情况: 对象创建十分频繁,仅仅修改指针指向,也并不线程安全。解决有两种可选方案:

  • 一种是对分配内存空间的动作进行同步处理一一实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性。
  • 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲( Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/- USETLAB参数来设定。TLAB在前面Java堆的说明中出现过。

对象的内存布局

HotSpot虚拟机中对象在堆内存的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Date)和 对齐填充(Padding)。
1、头部份包括两类信息:

  • 第一类存储对象自身运行时数据,如哈希码( Hashcode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,长度为32比特和64比特(32和64位机器虚拟机,未开启压缩指针),官方成为 “Mark Word”。对象所存储的运行时数据超出了32和64 Bitmap 记录限度,所以 Mark Word 被设计动态定义,根据对象动态复用空间。
存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁
指向重量级锁的指针10膨胀(重量级锁)
空,不需要记录信息11GC标记
偏向线程ID、偏向时间戳、对象分代年龄01偏向锁
  • 另一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身,这点我们会在下一节具体讨论。) 如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据(因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,无法通过元数据中的信息推断出数组的大小)。

2、实例数据部分:
对象真正存储的有效信息,即代码中定义的字段内容,无论是父类继承的,还是子类定义的都要记录下来,存储顺序会受虚拟机分配策略影响。
3、对齐填充:
不一定存在、无含义、占位。

由于 Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

对象的访问定位

Java程序会通过栈上的 reference 数据来操作堆上具体对象,reference 类型在《Java虚拟机规范》中只规定是个指向对象引用,没定义应该用什么方式定位、访问到堆中对象的具体位置,所以具体访问方式由虚拟机实现而定。主流方式有 使用句柄直接定位 两种。

  • 如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池, reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
    在这里插入图片描述

使用句柄来访问的最大好处就是 reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference本身不需要被修改。

  • 如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息, reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。
    在这里插入图片描述

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机 Hotspot而言,它主要使用直接指针方式进行对象访问(有例外情况,如果使用了 Shenandoah收集器的话也会有一次额外的转发,具体可参见第3章),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值