一、运行时数据区
Java把内存分成: 栈内存,堆内存,方法区,本地方法区和寄存器等
方法区和堆为线程共享区,虚拟机栈、本地方法栈及程序计数器为线程独占区。
1、程序计数器
程序计数器是一块较小的空间,它可以看作是当前线程所执行的字节码的行号指示器。
如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址(可以理解为上图所示的行号),如果正在执行的是native方法,这个计数器的值为undefined。
JVM的多线程是通过线程轮流切换并分配CPU执行时间片的方式来实现的,任何一个时刻,一个CPU都只会执行一条线程中的指令。为了保证线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的程序计数器独立存储,互不影响。
此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作。
2、虚拟机栈
①每个线程有一个私有的栈,随着线程的创建而创建,生命周期与线程相同。
②虚拟机栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表、操作数栈、动态链接、方法出口等信息。
- 局部变量表存放了编译期可知的各种基本数据类型和对象引用类型。通常我们所说的“栈内存”指的就是局部变量表这一部分
- 64位的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。
- 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,运行期间不会改变局部变量表的大小。
③方法的调用到执行完毕,对应的就是栈帧的入栈和出栈的过程。
④栈的大小可以固定也可以动态扩展。
- 在固定大小的情况下,当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError异常。
- 在动态扩展的情况下,若扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
⑤栈内存溢出模拟
public class MainTest {
public static void main(String[] args){
new MainTest().test();
} private void test() {
System.out.println("run...");
test();
}
}
报错如下:
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
在使用递归的时候经常会抛出StackOverflowError,顾名思义就是栈满了,而我们这里所说的栈在java中通常就是虚拟机栈(vm stack),在每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈,动态链接,方法出口等信息,每一个方法从调用直到执行结束的过程就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
虚拟机栈的深度是固定的,所以虚拟机栈中所装的栈帧的多少取决于栈帧的大小,而栈帧的大小就取决于
局部变量表、操作数栈,动态链接,方法出口四个的大小,而我们最熟悉的应该是局部变量表,用于存储方法中定义的局部变量的,因此局部变量越多栈所能装下的栈帧也就越少。
3、本地方法栈
①和虚拟机栈类似,两者的区别就是虚拟机栈是为虚拟机执行java方法服务,本地方法栈为虚拟机执行native方法服务 。
②HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。
③与虚拟机栈一样,本地方法栈也会抛StackOverflowError和OutOfMemoryError异常。
4、堆
①JVM管理的最大的一块内存区域,存放着对象的实例,是线程共享区。
②堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”
③JAVA堆的分类:
- 从内存回收的角度上看,可分为新生代(Eden空间,From Survivor空间、To Survivor空间)及老年代(Tenured Gen)。
- 从内存分配的角度上看,为了解决分配内存时的线程安全性问题,线程共享的JAVA堆中可能划分出多个线程私有的分配缓冲区(TLAB)。
④JAVA堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
⑤可通过参数 -Xmx -Xms 来指定运行时堆内存的大小,堆内存空间不足也会抛OutOfMemoryError异常。
5、方法区
每次调用方法,伴随着“方法入栈”操作,也就是栈中为该方法分配了一块空间,用于保存该方法中涉及到的变量。
每次方法调用结束,伴随着“方法出栈”操作,也就是栈中分配的空间被释放了。
在类的方法调用过程中,首先判断方法区是否存在该方法,存在则方法入栈,调用结束后出栈。
注:Java中方法传递参数是按值传递:如果是基本类型,则值 代表元素内容;如果是引用类型,则值 代表地址号。
①方法区也是线程共享区,用于存储【虚拟机加载的类信息(类的版本、字段、方法、接口),常量,静态变量,即时编译器编译后的代码等数据】
②方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
③HotSpot虚拟机使用永久代来实现方法区,使得HotSpot虚拟机的垃圾收集器可以像管理堆内存一样来管理这部分内存,能省去专门为方法区编写内存管理代码工作。所以开发者喜欢将方法区称为永久代,本质上两者并不等价,对于其他虚拟机来说不存在永久代的概念。
④方法区可选择不实现垃圾收集,一般来说,这个区域对内存回收的条件较为苛刻,但是这部分区域的回收确实是必要的。
⑤当方法区无法满足内存分配需求时,将会抛OutOfMemoryError异常
运行时常量池
- 运行时常量池是方法区的一部分。
- class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后加入方法区的运行时常量池中存放。
- 运行时常量池相于class文件中的常量池所不同的是其具备了动态性。class文件中常量池中的常量在编译期间就已经定义好了,而运行时常量池在程序运行期间也可以将常量放入该常量池中,最常见的做法就是调用String类的intern()方法。
二、对象的创建
一个对象的创建主要包括了一下几大流程
①在JVM在堆中为对象分配内存阶段,通常有以下两种分配方式,虚拟机选择哪种分配方式是由JAVA堆是否规整决定的,而JAVA堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
- 指针碰撞:要求堆中内存绝对规整,所有用过的内存都放一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅只是将该指针向空闲空间那边挪动一段与对象大小相等的距离。
- 空闲列表:针对的是堆中内存不规整的情况,虚拟机维护着一个列表,记录哪些内存块是可用的,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
②在为对象分配内存时,还需要考虑的一点就是线程安全性问题。可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。针对这种问题,有以下两种解决方案。
- 对分配内存空间的动作进行同步处理,保证更新操作的原子性(采用CAS + 失败重试机制保障原原子性),但效率较低。
- 使用本地线程分配缓冲(TLAB),哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并需要分配新的TLAB时,才需要同步锁定(可通过
-XX:+/-UseTLAB
参数来设定虚拟机启用TLAB)。
三、对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头、实例数据和对齐填充。
①对象头,由 Mark Word 和类型指针所组成。
Ⅰ》Mark Word,用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等。
- Mark Word在32位的JVM中对应的长度是32bit,在64位的JVM中长度是64bit。
- 由于对象需要存储的运行数据很多,其实已经超出了32位和64位的限制,为了在极小的空间内存储尽量更多的信息,Mark Word 会根据对象状态的不同来存储不同的信息。如在32位的HotSpot虚拟机中,如对象处于未被锁定的状态,那么 Mark Word 将使用25bit用于存储对象的哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,其他状态如下表所示:
Ⅱ类型指针,虚拟机通过这个指针来确定对象是哪个类的实例,该指针指向对象的类元数据。(由于查找对象的元数据信息并不一定要经过对象本身,所以并不是所有的虚拟机实现都必须在对象数据上保留类型指针)
在32位系统下,存放Class指针的空间大小是4字节,Mark Word空间大小也是4字节,因此就是8字节的头部。
在64位系统及64位JVM下,开启指针压缩(参数是 -XX:+UseCompressedOops),那么头部存放Class指针的空间大小还是4字节,而Mark Word区域会变大,变成8字节,也就是头部最少为12字节。
若未开启指针压缩,那么保存Class指针的空间大小也会变成8字节,那么对象头部会变成16字节。另外,在64位模式下,若未开启压缩,引用也会变成8字节。
如果对象是一个JAVA数组,那么在对象头中还必须有一块用于记录数组长度的数据,4字节来表示数组的长度。因为普通对象的大小可通过元数据信息来获取,而数组不行。
②实例数据,对象真正存储的有效信息,也就是在代码中所定义+的各种类型的字段内容。
- 无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
- 存储顺序收虚拟机分配策略参数(FieldsAllocationStyle)和字段在java源码中定义顺序的影响。
- HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),相同带宽的字段总是被分配到一起,在此之后,父类中定义的变量会出现在子类之前。
- 如果CompactFields参数的值为true(默认为true),那么子类中较窄的变量也可能会插入到父类变量的空隙中。
在HotSpot VM中,有继承关系的对象在创建时,父类的属性会被分配到相应的对象中,由于父类的属性不能和子类混用,所以它们必须单独排布在一个地方,可以认为它们就是从上到下的一个顺序。以两重继承为例,对象继承属性排布规则如下图所示。
这里的对齐有两种:一是整个对象的8字节对齐;二是父类到子类的属性对齐。在32位及64位压缩模式下,会按照4字节对齐。
例如:
class A {byte b;}
class B extends A {byte b;}
class C extends B {byte b;}
③对齐填充,无特殊含义,仅仅起到占位符的作用。HotSpot虚拟机要求起始地址必须是8字节的整数倍,因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。