运行时数据区域

运行时数据区域

根据 <Java虚拟机规范>的规定, Java 虚拟机所管理的内存将包括以下几个部分:
image.png

  1. 堆 Heap。
  2. 虚机机栈 VM Stack。
  3. 本地方法栈 Native Method Stack。
  4. 方法区 Method Area。
  5. 程序计数器 Program Counter Register。

运行时区域

程序计数器 Program Counter Register

程序计数器是 每个线程独有的,是一块比较小的内存区域,他可以理解为当前线程所执行的字节码的行号的指示器。
Java的多线程是通过多线程轮流切换并分配处理器的执行时间来决定的,同一时刻一个处理器(多核心的处理器中的一个核心)只能执行一个线程。因此,为了线程切换后可以正确执行,每个线程都有一个独立的程序计数器,各线程之间的程序计数器互不影响。称这类内存为 私有内存
如果正在执行的是Java方法,程序计数器里是正在执行的虚拟机字节码的指令地址。如果执行的是Native方法,则程序计数器为空(Undefined)
程序计数器是Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError(内存溢出错误)的区域。

虚拟机栈 VM Stack

程序计数器一样,虚拟机栈也是线程私有的,他的生命周期与线程相同。虚拟机线描述的是Java方法执行的线程内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每个方法被调用完成的过程,就是一个栈帧从入栈到出栈的过程。Java程序员口中所说的 指的就是 Java 虚拟机栈。
局部变量表存放了编译时期各种 Java 虚拟机基本数据类型,对象引用类型,returnAddress 类型。这些数据类型在局部变量表中存储空间以局部变量槽(Slot)表示。64位长度的 long 和 double 都占有两个变量槽,其他类型只占用一个变量槽。
局部变量表所需要的空间在编译时间完成分配,进入一个方法时,这个方法所需要的内存是在进入方法之前就已经确定的,运行期间不会改变局部局部变量表的大小。
Java虚拟机规范中,对这个区域定义了2种情况的异常:

  1. 当程序调用的最大栈深度大于虚拟机所允许的深度时, 会抛出 StackOverflowError异常。
  2. 如果虚拟机栈允许动态扩展(HotSpot 的虚拟机栈容量是不允许动态扩展的),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈 Native Method Stack

本地方法栈的作用与Java方法栈的作用是一样的,不同的是 Java虚拟机栈执行Java方法,而本地方法栈执行 使用到Native的方法服务。
虚拟机规范中对本地方法栈中使用语言并没有强制规定,不同的虚拟机可以自由的实现他。甚至有的 Java 虚拟机直接把2个合成一个,比如 HotSpot 就把两个区域合成了一个。
与Java虚拟机栈一样,本地方法区也会抛出 StackOverflowErrorOutOfMemoryError

堆 Heap

对于大部分Java程序来说,堆是Java虚拟机所管理的最大的一块内存区域。这块区域是所有线程所共享的一块内存。此内存的唯一目的就是存储Java线程所分配的对象。
<Java 虚拟机规范>规定,Java 堆可以处于物理上不连续的内存空间中,但是在逻辑上应该被视为连续的。
设置 堆 Heap的参数: -Xmx,-Xms。
Java中几乎所有的对象都在Java堆中分配,但是随着JIT(是 just in time 的缩写,也就是即时编译编译器),栈上分配,标量替换优化手段及其他技术的发展,在堆上分配对象也变得不那么绝对了。
堆Heap 是垃圾收集的主要区域,从内存回收的角度看,由于现在垃圾回收都 采用 分代的 思想,所以Java堆大致又分为:新生代老年代永久代Eden, To Survivor, From Survivor等。这些区域仅仅是一部分垃圾收集器的共同特征,或者说设计风格,并非某个 Java 虚拟机实现的固有部局,更不是<Java 虚拟机规范>的一部分。十年之前,以G1的出现为界,HotSpot 虚拟机的垃圾收集算法全部都是采用的分代的设计思想实现的。
从对象分配的角度看,所有线程共享的 Java 堆中可以划分出多个线程线程私有的分配缓冲区(Thread Local Allocation Buffer TLAB) 以提升对象的分配效率。
如果Java虚拟机没有内存完成实例的分配,并且堆也没有办法再扩展,那么将抛出OutOfMemoryError异常。

方法区

方法区与栈Heap一样,是所有线程共享的区域。用来存储被虚拟机加载的类信息常量信息静态变量即时编译器编译后的代码等。
JDK8以前,在HotSpot虚拟机中,Java程序开发人员习惯把 方法区 称为 永久代,但本质上两者的并不是等价的。只是HotSpot虚拟机的设计团队,使用永久代来实现方法区的功能,或者说将 分代设计 扩展到方法区而已。这样的好处可以统一管理Java内存,使垃圾收集器能够像管理堆Heap一样管理这部分内存,从而省去了专门为方法区编写内存管理代码的工作。
但是其他虚拟机 J9,JRockit 等是没有永久代概念的。
在 JDK7时,HotSpot 已经把放在永久代的字符串常量池,静态变量等移出了永久代。到了 JDK8,就完全废弃了永久代的概念,改用元空间 Metaspace来代替,把 类型信息全部移除到元空间中
Java虚拟机规范对方法区的限制很宽松,和Java堆一样,不需要连续的存储空间,可以扩展,可以选择不实现垃圾收集。 方法区的垃圾回收,主要针对 常量池的回收 和 对类型的卸载。
<Java虚拟机规范规定>,如果方法区区无法满足内存分配时,将抛出OutOfMemoryError异常。

运行时常池 Runtime Constant Pool

运行时的常量池方法区的一部分。Class文件中除了类的版本,字段,方法,接口信息外,还有一部分内容是常量池表(Constant Pool Table),用于存放编译期生成的字面量符号引用
<Java 虚拟机规范>对运行时常量池没有作任何细节要求。
运行时常量池 对于 Class文件的常量池另一个好处是:具备动态性。Java语言并没有要求常量只能在类加载时生成,在程序运行时也可以生成,如果String.intern().
运行时常量池方法区的一部分,自然会受到方法区的内存限制,没有内存时,会抛出OutOfMemoryError异常。

直接内存 Direct Memory
  1. 直接内存不是Java虚拟机运行时数据区域的一部分
  2. 也不是Java虚拟机规范中定义的内存区域。但这块内存也经常使用,也会抛出OutOfMemoryError异常。

在1.4之后加入的NIO,引入了一种基于Channel(通道)Buffer(缓冲区)的I/O方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中里的DirectByteBuffer对象作为这块内存的引用进行操作。这能提高性能,减少 Java 堆和 Native 堆中来回复制数据。
直接内存不受限于 Java 堆,但是受限于本机总内存。

HotSpot 虚拟机对象探秘

对象的创建

一般对象的创建有3种方式

  1. new Object
  2. object.clone
    JAVA深复制(深克隆)与浅复制(浅克隆)
    深复制:利用串行化来做深复制
  3. 反序列化创建对象
  4. 使用反射创建对象
通过new关键字创建对象
  1. 当Java虚拟机遇到new关键字时,首先去检查这个指令的参数是否能在常常量池中定位到一个 “类的符号引用”,并且检查这个符号引用代表的类是否已经被加载,解析,初始化。如果没有,则对类进行,加载,解析,初始化等操作

  2. 在类信息检查通过后,接下来将为类对象分配内存空间。分配的内存大小,在类加载时就已经确定,分配空间的任务等同于开辟一块确定大小的内存从Java堆中划分出来。
    Java对象内存分配为2种方式:

  • 指针碰撞:意思是 整个Java堆的空间是规整的,使用的在一边,未使用的在另一边,中间通过一个指针作为分界点。对象内存分配时,只需要移动 分界点 的指针即可。
  • 空闲列表:意思是 整个Java堆的空间不是规整的,通过一个Table来记录哪些空间已经使用,哪些空间没有使用。找一个满足对象内存的空间内存,对对象进行分配。
    具体使用哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的 垃圾收集器 是否带有 压缩整理功能有关。
    Serial,ParNew,带有Compact功能,系统所采用的是指标碰撞,简单高效。
    CMS这种基于 Mark-Sweep算法时,采用比较复杂的空闲列表
  • 因为对象创建非常频繁,即使是修改一个指针,并发时仍然有并发风险,也不是线程安全的。这个问题有两个解决办法:A: 对分配内存空间的动作进行同步处理,实际上虚拟机采用的是 CAS 加 失败重试的方式保证新操作的原子性;B: 另一种是把内存分配动作按照线程划分在不同的空间中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲区 Thread Local Allocation Buffer, TLAB,哪个线程分配内存,就在线程分配缓冲区中分配,只有本地缓冲区分配完了,分配新的缓冲区时才需要使用同步锁。-XX:+/-UseTLAB参数决定是否开始本地线程分配缓冲区
  1. 内存划分完之后,需要对这块内存的空间都初始化为0,如果启用了 TLAB,初始化工作也可以提前到 TLAB 分配时进行。但不包括 对象头,这一步操作就保证了 Java对象的属性值在不赋初值时就直接使用。
  2. Java虚拟机要对 对象 进行必要的设置,包括 对象是哪个类的实例,对象的GC年龄对象的Hash代码如何才能找到对象所属类的元数据 。这些信息都是存放在对象的 Object Header之中。
  3. 完成上述工作后,从虚拟机的角度看,对象的创建已经完成,但从Java业务的角度看,还没有完成,所有属性还都为0,这时需要调用对象的方法,按业务的要求对对象进行初始化。
对象内存布局:

对象在内存中存储主要分为3个部分:

  1. 对象头 Header
  2. 实例数据 Instance Data
  3. 对齐填充Padding
Header

HotSpot 虚拟机对象头Header 中存储两类信息。

  • 存储对象自身运行时数据:对象的 Hash-Code,GC年龄,锁状态标志,线程持有的锁,偏向线程 ID等。
  • 类型指针: 即对象指向他的类型元数据的指针,Java 虚拟机通过这个指针来确定该对象是哪个类的实例。查找对象的元数据信息并不一定要经过对象本身。
    如果对象是一个数组,那么对象头中还有一块区域用于存储 数组的长度。
Instance Data

Instance Data存储程序代码中定义的类型的字段的内容,包括从父类继承的,子类自己定义的。字段的存储顺序会受到 分配策略参数-XX:FieldsAllocationStyleJava 中源码定义顺序的影响。HotSpot虚拟机将 相同宽度字段总是被分配到一起。在满足这个条件的前提下,父类的字段会出现在子类的子段之间,子类的字段也允许插入到父类的空隙中。
HotSpot 虚拟机的默认分配顺序是 long/double, int, short, char, byte/boolean, oops(Ordinary Object Pointers),OOPs。

Padding

Padding不是必然存储的,只是起到占位符,对齐的作用。HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍。对象头已经被设计成了8字节的整数倍,因此,如果对象数据部分没有对齐的话,就需要通过对齐来补全。

对象访问定位

Java程序通过栈上的 reference 数据来操作堆上的具体对象。但是<Java 虚拟机规范>只是规定了他是一个指向对象的引用,但并没有说明这个引用如何访问具体对象。所以具体对象的访问是根据虚拟机来实现的,常用的对象访问有2种方式:句柄直接指针2种。

  • 句柄: 句柄访问时,Java虚拟机会在堆上分配一块区域作为 句柄池,Reference中存储的是句柄的地址,而句柄中存储的是 对象真实内存地址。优点是:Reference中存储是对象稳定的句柄地址,在GC之后,不会影响Reference中的值。缺点是:增加了一次寻址开销。
  • 直接指针: Reference中存储的就是对象的真实内存地址。HotSpot 虚拟机就是通过 直接指针访问的方式来实现的。优点是:访问速度快。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值