虚拟机内存区域

143 阅读8分钟

内存区域

分类

java虚拟机在执行java程序时会把它管理的内存划分为不同的区域,每个区域都有各自的用途,以及创建和销毁时间

程序计数器

当前线程所执行的字节码的行号指示器

同一时刻,一个处理器只会执行一条线程中的指令,为了切换线程后能恢复到指定的位置,都要有个独立的程序计数器,各个线程互不影响,独立存储,线程私有

如果执行的时native方法,计数器值为空

此区域时内存中唯一在虚拟机规范中没有任何OutOfMemoryError情况的区域

java虚拟机栈

描述的是java方法执行的内存模型,每个方法执行都会创建一个栈帧,存储局部变量表,操作数栈,动态链接,方法出口等信息

每个方法的调用执行到完成,对应着一个栈帧在虚拟机栈中入栈出栈过程

线程私有

局部变量表存放了编译器可知的基本数据类型,对象引用(reference类型,不等同于对象本身,是指向对象起始地址的指针,或者代表对象句柄或其他此对象相关的位置),和returnAddress类型(指向字节码指令地址)

虚拟机规范中,这个区域异常有两种,如果线程请求的栈深度过大于虚拟机所允许深度时,抛出StackOverflowError异常;如果虚拟栈可动态扩展,但扩展时无法申请到足够的内存,报OutOfMemoryError异常

本地方法栈(Native Method Stack)

和虚拟机栈作用相似,区别在于,上面为执行java方法服务,它服务native方法

同样抛出StackOverFlowError 和OutOfMemoryError

java堆

java虚拟机所管理内存中最大的一块

所有线程共享,虚拟机启动时创建;java虚拟机规范中描述:所有的对象实例及数组都在堆上分配,现在随着JIT编译器发展和逃逸分析技术发展,每那么绝对了

java堆是垃圾收集器管理的主要区域,也叫做“GC堆”,多采用分代收集算法,也可以分为新生代和老生代

可以是物理上不连续的空间,可以是固定的,或者可扩展的,主流的虚拟机都是可扩展的(-Xmx 和-Xms控制)

堆中没有内存完成实例分配,堆也无法再扩展时会抛出OutOfMemoryError异常

方法区

存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

线程共享

java虚拟机规范把它描述为堆的逻辑部分,但它有个别名叫非堆,就是为了却别于堆

不需要连续内存,可选择固定大小或者可扩展,主要的内存回收时常量池的回收和对类型的卸载

无法满足内存分配时,会抛出OutOfMemoryError异常

运行时常量池

属于方法区,Class文件除了有类的版本、字段,方法、接口信息。还有常量池存放编译期间产生的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

运行时常量池相对应Class常量池来说具备动态性,java语言并不要求常量一定在编译期生成,也就是并非预置入Class文件常量池的内容才能进入方法区运行时常量池,,运行期也能将新的常量放入池中,比如String的intern() 方法

无法再申请到内存时,报OutOfMemoryError异常

直接内存

不是虚拟机运行时数据区的一部分,但也被经常用,也可能导致OutOfMemoryError异常

JDK1.4加入的NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后 通过一个堆中的DirectByteBuffer对象作为这块内存的引用进行操作,可以提高性能,因为避免了再Java堆和Native堆中来回复制数据。

不受Java堆大小的限制,受计算机内存或操作系统级的限制,导致OutOfMemoryError异常

对象的创建

1.检查

遇到new,检查这个指令的参数(也就是类名),是否在常量池中定位到一个类的符号引用

检查类是否已经加载,解析,初始化过,如果没有,先执行类加载过程

2.分配内存

检查通过后,开始为新生对象分配内存,类加载完成后,所需大小便可确定

分配有两种方式,1.“指针碰撞”:如果内存绝对规整(占用的在一边,空闲在另一边,中间有个分界点指示器指针),分配内存就是把指针向空闲边挪动一段和对象大小相等的距离;2.“空闲列表”:如果内存不规则,虚拟机维护一个表,说明哪些内存时可用的,分配上选一块足够大的给对象实例

选择哪种方式由堆是否规整决定,堆是否规整由采用的垃圾收集器是否带有压缩整理功能决定

堆是线程共享的,所以还要考虑并发情况,正在给A分配内存,指针没来得及修改,B又同时使用了原来的指针来分配内存

解决方案1:对分配内存空间进行同步处理,虚拟机采用CAS配上失败重试;2.按线程把内存分配动作划分到不同空间进行,每个线程在堆上预先分配一小块内存,叫做本地线程分配缓冲(TLAB),通过-XX:+/-UseTLAB

3.初始化为零值

虚拟机将分配到的额内存空间初始化为零值(不包括对象头),这个操作也可提前至TLAB分配时进行

目的是保证对象的实例字段可以不赋初值使用

4.对对象进行设置

设置这个对象是那个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息,存放在对象的对象头中,以及根据虚拟机状态,是否启用偏向锁

5.执行init方法

把对象按程序员意愿初始化

对象的内存布局

HotSpot虚拟机中,对象在内存中存储分为3个区域:对象头,对象数据,对齐填充

对象头

1.自身运行的数据(Mark Word)

哈希码,GC分代年龄,锁状态标志,线程持有锁,偏向线程id,偏向时间戳,在32位和64位操作系统中占32bit和64bit

如果对象处于未锁定状态,32位HotSpot虚拟机中,25bit存哈希码,4bit存对象分代年龄,2bit存锁标志位,1bit存0

HotSpot虚拟机对象头 Mark Word

存储内容标志位状态
哈希码,分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记
偏向线程ID,分代年龄,偏向时间戳01可偏向

2.类型指针

对象指向它的类元数据的指针

非必须,查找元数据不一定要经过对象本身

3.数组长度

如果对象是一个java数组,必须有一块记录数组长度,因为:虚拟机可以通过普通java对象的元数据确定java对象的大小,但无法从数组的元数据中确定数组的大小

对象数据

HotSpot相同宽度的字段总是被分配在一起,满足这个前提下,父类定义的变量会出现在子类之前;

如果CompactFields参数位true,子类中较窄的变量也可能插入到父类变量的空隙中

对齐

HotSpot自动内存管理要求对象起始地址必须是8个字节的整数倍,也就是对象大小必须是8个字节的整数倍,对象头刚好8个字节的整数倍(1倍或2倍),所以实例数据部分没有对齐时,就需要通过填充补全。

对象的访问定位

建立对象是为了使用对象

java程序中通过栈上的reference数据来操作对象,主流方式又两种

1.句柄访问

使用句柄访问,java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含对象实例数据和类型数据各自地址(这也就是上面所说的对象头中不一定要包含类元数据的原因)

优点:对象移动时,只改变句柄的实例数据指针,reference本身不需要移动

2.直接指针访问

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

优点:速度更快,节省了一次指针定位的时间开销。Sun HotSpot的使用方式