学习JVM首先要关注虚拟机运行时的内存分布和内存管理,这样在遇到OOM时才能调试相应的参数获得解决办法。
一、内存区域:
以上图为概要,一一介绍各个内存区域:
1、 程序计数器:
是一块较小的内存空间,它可以看作当前线程所执行额字节码的行号执行器。简单地说,计数器内记录值是字节码的位置,而记录值的变化则决定了程序执行的流程(变化 地看一段字节码到另一端字节码就是程序的跳转)此内存区域是唯一没有规定任何OutOfMemoryError(之后以OOM代替)情况的区域。
2、 虚拟机栈:
Java方法执行的内存模型。方法执行时都会创建一个栈帧(用于支持虚拟机进行方法调用和方法执行的数据结构),存放局部变量表、操作数栈、动态链接、方法出口等 信息,每个方法从调用直至执行就对应着一个栈帧在虚拟机栈里入栈再出栈的过程。
该区域规定两种异常情况:
1)线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常
2)如果虚拟机栈可以动态扩展,但是无法请求到足够的内存,抛出OOM异常
3、 本地方法栈
虚拟机使用的Native方法服务,发挥作用。其他作用与虚拟机栈类似,同样会抛出StackOverflow和OOM异常。
4、 Java堆
线程共享的一块内存区域,在虚拟机启动时被创建,作用是存放对象实例。GC工作的主要区域。根据现在GC所采用的粉黛算法,Java堆还可以分为新生代和老年代,虽然 名称不同,但都是为了更好的回收对象,其实存放的都是对象实例。如果堆中没有内存分配给实例,且无法再扩展时,会抛出OOM异常
5、 方法区
各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,由于HotSpot虚拟机设计团队选择使用永久代实现方法 区,因此又被成为永久代。方法区内很少会涉及到GC操作,但是当内存不足够分配时,会抛出OOM异常。
6、 运行时常量池
存放编译期生成的各种字面量和符号引用,属于方法区的一部分。主要特征是动态性,即运行期间也可以将新的常量放入常量池内。同样当内存不足够分配时,会抛出 OOM异常。
7、 直接内存
某种场景可以使用Native函数库直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用直接操作,可以避免在Java堆和Native堆来回复制数 据,提高性能。
如果忽略直接内存,导致各个内存总和大于物理内存限制,会导致动态扩展时抛出OOM异常。
二、虚拟机内对象
1、 对象的创建
a) 虚拟机遇到一条new指令,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否被加载,解析,初始化,如 果没有,则要进行类加载过程
b) 为新生对象分配内存,即把确定大小的内存从Java堆中划分出来。根据内存是否规整,分别区分为指针碰撞(将内存指针前移一段对象大小的距离),如果不 规整,则需要记录那些内存块可用,并划分内存空间给对象实例。成为空闲列表
c) 分配完内存后,将内存空间都初始化为零值。
d) 设置对象信息。包括这个对象是哪个类的实例,如果找到类的元数据信息等。
e) 再执行init方法(即构造方法),将对象初始化成程序员想要的样子
2、 对象的内存布局
a) 对象头
有两部分信息:
1) 存储对象本身的运行时数据,即哈希吗,GC分代年龄,锁状态标志等。
2) 类型指针,虚拟机通过这个指针判断是哪个类的实例。
如果对象是数组,还需要保存数组的长度。
b) 实例数据
真正存储的有效信息,也是程序代码中定义的字段。
c) 对齐填充
没有特殊含义,只是为了让对象大小是八字节的整数倍。
3、 对象的访问定位
a) 使用句柄
在Java堆中额外划分出句柄池,reference内保存的是句柄池地址。句柄中包含了对象实例数据和类型数据的地址信息。
优点:当对象被移动时,reference不需要修改,而只需要修改句柄内对象实例的地址即可。
b) 直接指针
Reference内存储的是对象地址,而对象要保存到类型数据的指针。
优点:节省了一次指针定位时间,直接由reference到对象。
三、OOM异常
1、 Java堆溢出
不断的创建对象,且保证GC Roots到对象之间有可达路径避免回收,那么到对象所占内存超出时,会产生OOM异常。
2、 虚拟机栈和本地方法栈溢出
a) 如果线程请求的栈深度大于允许的最大深度,将抛出StackOverflow
b) 如果扩展时无法申请到足够的内存空间,则抛出OOM。