《深入理解JVM》笔记-1-Java内存区域

Java内存区域

20190615203917.png

程序计数器

程序计数器(Program Counter register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器.字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令.分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器.

为了切换线程后恢复到正确的执行位置,每条线程都需要有一个独立的线程私有的程序计数器.

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是一个Native方法,这个计数器值则为空.

异常情况

此内存区域是唯一一个没有任何OutOfMemoryError情况的区域

Java虚拟机栈

Java虚拟机栈(Java Virtual Machin Stacks) 也是线程私有的,它的生命周期与线程相同.
虚拟机栈描述的是Java方法执行的内存模型: 每个方法在执行的同时都会创建一个栈帧(Stack Frame).
栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息.

局部变量表

局部变量表存放了编译器可知的各种基本数据类型(byte,boolean,short,int,long,double,float,char),对象引用,和returnAdress类型(指向了一条字节码指令的地址).

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

局部变量表所需的内存空间在编译期间完成分配,在进入一个方法时,这个方法需要在栈中分配多大的局部空间是完全确定的.

异常情况

Java虚拟机栈有两种异常状况: 如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈无法申请到足够的内存,将抛出OutOfMemoryError异常.

本地方法栈

本地方法栈为虚拟机使用到的Native方法服务,其余与Java虚拟机栈相似.

Java堆

对大多数应用来说,Java堆 是占内存最大的一块.
java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例 .这一点在java虚拟机规范中的描述是: 所有对象实例以及数组都要在堆上分配.

Java堆是垃圾收集器管理的主要区域.

从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer) (简称TLAB ).TLAB如何划分都和存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分是为了更好地回收内存 ,或者更快地分配内存 .

异常情况

Java堆可以出在物理上不连续的空间中,只要逻辑上是连续的即可.在实现时,主流的虚拟机都是按照可扩展来实现的.
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常.

方法区

方法区(Method Area) 是线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.

这一区域的内存回收目标主要是针对常量池的回收和对类型的卸载.

运行时常量池

运行时常量池(Runtime Constant Pool) 是方法区的一部分.

Class文件中有一项信息是常量池(Constant Pool Table) ,用于存放编译器生成的各种字面量符号引用 ,这部分内容将在类加载后进入方法区的运行时常量池中存放.

运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性, Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入仿佛运行时常量池,运行期间 也可能进将新的常量放入池中(这种特性被开发人员利用得比较多的是String类的intern()方法).

异常情况

当方法区无法满足内存分配的需求时,将抛出OutOfMemoryError异常.

HotSpot虚拟机中的对象

对象的创建

1-类加载检查

当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过.如果没有,那必须先执行响应的类加载过程.

2-分配内存

对象所需的内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来.

如果Java堆中内存是绝对规整的,则使用指针碰撞(Bump the Pointer)
如果Java堆中内存并不是规整的,就要使用空闲列表(Free List)

3-空间置零

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行.

这一操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的的零值.

4-对象头设置

接下来,虚拟机要对对象进行必要的设置,例如这个对象时哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息.
这些信息存放在对象的对象头(Object Header) 之中.

5-init

在上面的工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的角度来看,对象的创建才刚刚开始: init方法还没有执行,所有的字段都还为零.所以,执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来.

对象的内存布局

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

对象头

对象头包括两部分信息:

1-Mark Word

存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向进程ID,偏向时间戳等.

2-类型指针

对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.

另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据.

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容.

无论是从父类继承下来的,还是在子类中定义的字段内容,都需要记录起来.
这部分的存储顺序会收到虚拟机分配策略参数(FieldAllocationStyle)和字段在Java源码中定义顺序的影响.
HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,OOP(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起.
在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前.
如果CompactFields参数值为true(默认为true),那么子类中较窄的变量也可能会插入到父类变量的空隙之中.

对齐填充

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用.

这个占位符的存在是因为,HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍

对象的访问定位

Java程序需要通过栈上的reference数据来获取到堆上的具体对象.
对象访问方式取决于虚拟机实现.目前主流的访问方式有使用句柄和直接指针两种.

通过句柄访问对象

Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息.

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

20190615225054.png

通过直接指针访问对象

Java堆对象的布局中必须考虑如何放置访问类型数据的相关类型,而reference中存储的直接就是对象地址.

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销.

HotSpot使用的是直接指针.

20190615225303.png