Java运行时数据区:
程序计数器
线程私有,是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。没有规定任何OutOfMemoryError情况的区域。线程私有:每条线程都有一个独立的程序计数器,各条线程之间互不影响,独立存储,这类的内存区域称为“线程私有”内存。
如果线程执行的是Java方法, 程序计数器记录正在执行的虚拟机字节码指令的地址,如果执行Native方法,计数器的值为空(Undefined).
Java虚拟机栈
线程私有,生命周期与线程相同。描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。可通过-Xss参数设置大小。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型。
对象引用: reference类型,它不等于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。
returnAddress :它指向了一条字节码指令的地址。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
异常情况:线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈为虚拟机使用到的Native方法服务(-Xoss参数可设置其大小)。
在HotSpot虚拟机中直接把本地方法栈与虚拟机栈合而为一。
Java堆
Java堆是Java虚拟机所管理的内存中最大的一块数据区域,在虚拟机启动时创建并被所有线程共享。此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,例如对象实例和数组。
Java堆同样是垃圾收集器管理的主要区域,由于现在的收集器基本都采用分代收集算法,所以Java堆中还可以细分为新生代和老年代。还可细分为 Eden空间、Form Survivor空间、To Survivor空间等。
1)当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
2)如果堆中没有内存完成实例分配,并且对也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区
线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。可通过
-XX:PermSize 和 -XX:MaxPermSize设置内存大小。当方法区无法再申请到内存时抛出OutOfMemoryError异常。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,类加载后这些内容进入方法区的运行时常量池存放。当常量池无法再申请到内存时抛出OutOfMemoryError异常。
直接内存
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
NIO可以使用Native函数库直接分配堆外内存,通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作(避免了在Java堆和Native堆中来回复制数据,性能得到提高)
大小不受Java堆大小的限制,受本机(服务器)内存限制,系统内存不足时抛出OutOfMemoryError异常。
HotSpot虚拟机对象
1.对象的创建
虚拟机遇到一条new指令时,首先将去检查这个对象的参数是否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,必须先执行类的加载过程。
在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存大小再类加载完成后便可确定。
1)内存分配:
- 堆内存规整:指针碰撞(指针向空闲空间移动一段与对象大小相等的距离)
- 堆内存不规整: 空闲列表
Java堆内存是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
2) 同步控制:
(1)对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS和失败重试的方式来保证更新操作的原子性。
(2)每个线程在Java堆中预先分配一小块内存,即本地线程分配缓冲(TLAB)。每个线程在自己的TLAB上分配内存,只有TLAB用完并分配新的TLAB时,才需要同步锁定。是否使用TLAB,可通过-XX:+/-UseTLAB参数来设定。
3) 初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用 TLAB,这一过程就可以提前至 TLAB 分配时进行。
作用:保证对象的实例字段在 Java 代码中可以不赋初值就直接使用,程序能访问到这些字段的数据类型对应的零值。
4)对象头设置
对象头信息,如对象所属类的实例、能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
5)执行 <init> 方法
2.对象的内存布局
对象头(Header):
1) Mark Word:用于存储对象自身的运行时数据。
哈希码、对象分代年龄、锁状态标志、指向锁记录的指针、偏向线程ID、偏向时间戳等。
2) 类型指针(非必要):对象指向它的类元数据的指针,用于确定对象是哪个类的实例。
实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
对齐填充(非必要):占位符,因为对象必须是8字节的整数倍。
3. 对象的访问定位
java程序需要通过栈上的reference数据来操作堆上的具体对象。访问方式有使用句柄和直接指针两种。
句柄访问: java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
优点:稳定的句柄地址,对象移动(垃圾收集时移动对象是非常普遍的行为)时会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针访问: java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference中存储的就是对象地址。
优点:速度快,节省指针定位的开销。
OOM异常解决思路
生成Dump快照文件:
1)通过jvm参数—XX:-HeapDumpOnOutOfMemoryError可以让JVM在出现内存溢出异常时Dump出当前的内存转储快照以便事后分析。
2)通过 -XX:+HeapDumpOnCtrlBreak参数,使用 [Ctrl] + [Break] 键 让虚拟机生成dump文件。
3)用jmap生成dump文件。
先通过内存映像分析工具(如Eclipse的Memory Analyzer)进行分析,常见的情况有:
内存泄露,对象已经死了,无法通过垃圾收集器进行自动回收,可进一步通过工具查看泄露对象到GC Roots的引用链,找出泄露的代码位置和原因,然后确定解决方案;
内存溢出,内存中的对象都还必须存活着,这说明Java堆分配空间不足,检查堆设置大小(-Xmx与-Xms)是否可以调大,从代码上检查代码是否存在对象生命周期太长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。