深入理解JVM之Java内存区域与内存溢出异常

在虚拟机自动内存管理机制下,不需要为new操作去写配对的delete/free代码,不容易出现内存泄漏。但是如果出现内存泄漏问题,如果不了解虚拟机的机制,便难以定位。

运行时数据区域

深入理解JVM之Java内存区域与内存溢出异常_第1张图片
Java内存分区

程序计数器

* 一块较小的内存,可以看做是当前执行的字节码文件的行号指示器;
* 在虚拟机的概念模型中(各虚拟机的实现方式可能不同),字节码解释器的工作就是改变这个计数器的值来选取执行下一条字节码指令;
* 程序计数器属于线程私有的内存;
* 如果执行的Java方法,计数器记录的是正在执行的字节码指定的地址,如果执行的是Native方法,计数器记录为空;
* 此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域;

Java虚拟机栈

* 线程私有;
* 描述的是Java方法运行的内存模型,每个方法在执行的过程中都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用到结束对应着栈帧在虚拟机从入栈到出栈的过程;
* 局部变量表存放Java方法执行的编译器各种已知的基本数据类型、对象引用和retureAddress类型,局部变量表所需的内存空间都是在编译期间完成分配,不会在运行期改变;
* 可能存在两种异常:OutOfMemoryError和StackOverFlowError;

本地方法栈

* 与虚拟机栈类似,只不过描述的是Native方法运行的内存模型;
* 线程私有;
* 可能存在两种异常:OutOfMemoryError和StackOverFlowError;

* 被所有线程共享,在虚拟机启动时创建;
* 此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例都是在这里完成分配的;
* 此内存区域是垃圾回收管理的主要区域,分为新生代和老年代;
* 此内存区域可以再物理上不连续,但是在逻辑上必须连续;
* 如果堆中没有内存完成对象实例分配且没有得到扩展时,会抛出OutOfMemoryError;

方法区

* 线程共享;
* 此区域主要存储已被虚拟机加载的类的信息、常量、静态变量、即时编译器编译产生的代码等数据;
* 此区域对垃圾收集的要求比较严格,但是有必要进行回收处理;
* 当无法满足内存分配需求时,会抛出OutOfMemoryError;

运行时常量池

* 是方法区的一部分;
* Class除了有类的版本、方法、字段、接口等描述信息外,还有一项信息是常量池,常量池用于存储编译产生的字面量和符号引用,这部分内容将在类加载后进入到方法区的运行时常量池存放;
* Java虚拟机规范要求较少,通常会把翻译产生的直接引用也加入到运行时常量池;
* 具有动态性,可以在运行期间将新的常量加入到运行时常量池,如String的intern方法;
* 存在OutOfMemoryError;

直接内存

* 不是Java运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域;
* JDK1.4的NIO引入了基于缓冲(Buffer)和通道(Channel)的IO方法,可以使用Native函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存区域的引用操作这块内存以提升性能;
* 存在OutOfMemoryError;

HotSpot虚拟机对象探秘

进一步了解虚拟机内存中数据的其他细节,比如它们是如何创建、如何布局以及如何访问的。下面以虚拟机HotSpot和常用的内存区域Java堆为例,深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。

对象的创建

1. 当虚拟机遇到一条new指令时,首先`检查`指令的参数是否能在常量池定位到一个类的符号,并且确定该符号引用的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应类加载的过程;
2. 当类加载检查通过后,虚拟机将为新生的对象`分配内存`。对象所需内存的大小在类加载完成后便完全确定,为对象分配空间等同于从Java堆中划分出一块确定大小的内存。内存分配主要有两种方式,取决于Java堆内存是否规整,规整的情况下,采用“内存碰撞”,不规整的情况,采用“空闲列表”。Java堆内存是否规整取决于垃圾收集器是否带有压缩整理功能,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞(内存绝对规整,只通过指针作为分节点标识);而使用CMS这种基于Mark-Sweep算法收集器时,通常使用空闲列表(内存不规整,通过维护一个列表记录哪块内存是可用的);
3. 需要考虑并发造成的`线程安全问题`,通常有两种方案:一是分配内存空间的动作进行同步锁定处理(实际虚拟机采用CAS+失败重试的方式保证更新原子性);二是将线程划分,为每一个线程分配一小块内存(称为本地线程分配缓存,TLAB),各个线程独立分配,只有TLAB耗尽需要重新分配新的内存时才需要同步锁定,虚拟机通过-XX:+/-UseTLAB参数来设定;
4. 内存分配完后,虚拟机将分配的内存空间都`初始化`为零值(不包括对象头),这保证了对象的实例字段在Java代码中可以不赋值直接使用,程序能访问到这些字段数据对应的零值;
5. 虚拟机对对象进行必要的设置。需`设置对象头`(Object Header)信息,包括对象是哪个类的实例、如何才能找到类的元数据信息、对象的Hash码,对象GC分带年龄等;
6. 到此虚拟机内部对象已经产生了,但Java对象的创建才刚刚开始。执行``方法,将对象按照意愿进行初始化,这样一个对象才真正可用;

对象的内存布局

* 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3个区域:`对象头(Objective Header)、实例数据(Instance Data)和对齐填充(Padding)`;
* 对象头分为两部分:第一部分用于存储对象自身运行时数据(`Mark Word`),32位虚拟机4B,64位虚拟机8B,如对象的哈希码、GC分带年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等;另一部分是`类型指针`,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例(并不是所有的虚拟机都必须在对象数据上保留类型指针),32位虚拟机4B,64位虚拟机8B。另外如果对象是一个Java数组,对象头还必须有一块用于记录`数组长度`的数据;
* 实例数据存储的是真正的`有效信息`,也就是在代码中定义的各种类型字段内容。无论是父类继承还是子类中定义的都需要记录下来。这部分存储的顺序会受到虚拟机分配策略参数和字段在Java源码定义的顺序影响;
* 对齐填充并不是必要存在的,起到占位符的作用,主要是由于HotSpot VM对自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象大小是8字节的整数倍。而对象头是8字节的倍数(1倍或2倍),因此,当实例数据部分没有对齐时,就需要通过对齐填充来补全;
深入理解JVM之Java内存区域与内存溢出异常_第2张图片
对象的组成

对象的访问定位

* 栈上的reference类型在虚拟机规范中只规定了一个指向对象的引用,并没有定义这个reference应该通过何种方式去定位、访问堆中对象的具体位置,对象的访问方式取决于虚拟机的具体实现,目前主流的访问方式有句柄和直接指针两种;
* 通过`句柄访问`:在Java堆中分配一块内存区域作为句柄池,reference存储`句柄地址`,句柄地址包含了对象实例数据和对象类型数据的地址信息;
深入理解JVM之Java内存区域与内存溢出异常_第3张图片
通过句柄访问
* 通过`直接指针访问`:Java堆对象的布局必须考虑如何放置访问类型数据的相关数据,而reference中直接存储`对象地址`。
深入理解JVM之Java内存区域与内存溢出异常_第4张图片
通过直接指针访问
* 二者各有优势,句柄访问更加稳定的存储对象的句柄地址,在对象被移动(垃圾收集时移动)只改变实例数据指针,而reference不需要修改;使用直接指针访问速度更快,节省了一次指针定位的开销。HotSpot采用直接指针方式进行对象访问,但其他语言和框架采用句柄访问的也很常见;

验证OutOfMemoryError异常

* 通过代码验证Java虚拟机规范中描述各个运行时区域存储的内容;
* 在实际遇到内存溢出异常时,能根据异常的信息快速判断是哪个区域内存溢出;

Java堆溢出

深入理解JVM之Java内存区域与内存溢出异常_第5张图片
参数及返回结果
解决思路:先通过内存映象分析工具对dump出来的堆转储快照进行分析,弄清楚是内存泄露还是内存溢出。
1. 如果是内存泄露,进一步查看泄露对象到GC Roots的引用链,从而确认无法回收的原因;
2. 如果是内存溢出,则应当检查虚拟机堆参数(-Xms与-Xmx)或检查是否存在对象生命周期过长、持有状态时间过长的情况。
3. 通过参数-Xms、-Xmx设定;

虚拟机栈和本地方法栈溢出

1. HotSpot不区分虚拟机栈和本地方法栈;
2. StackOverFlowError和OutOfMemoryError存在相互重叠的地方,本质上是对同一件事情的两种描述;
3. 通过参数-Xss设定;
深入理解JVM之Java内存区域与内存溢出异常_第6张图片
参数及返回结果
虚拟机默认的参数对于通常的方法调用(1000层~2000层)完全够用,通常根据异常的堆栈日志就可以定位到问题;

方法区和运行时常量池溢出

对于这个区域的测试,基本思路就是运行时产生大量的类去填满方法区(比如使用反射和动态代理),借助CGLib直接操作字节码运行时产生大量的动态类(很多主流框架如Spring、Hibernate、大量JSP或动态产生JSP文件的应用、基于OSGN的应用等都会采用类似的字节码技术)。在这里需要特别注意垃圾回收的状况。
深入理解JVM之Java内存区域与内存溢出异常_第7张图片
参数及返回结果

本机直接内存溢出

* 代码越过了DirectByteBuffer类,直接通过反射获取Unsafe的实例对象进行内存分配,DirectByteBuffer虽然也会抛出同样的异常,但其并没有真正向系统申请内存分配,而是通过计算得知内存无法分配;真正申请内存分配的方法是unsafe.allocateMemory。
深入理解JVM之Java内存区域与内存溢出异常_第8张图片
参数及返回结果
* DirectMemory导致的内存溢出,在Heap Dump里不会看见明显的异常。如果发现OutOfMemoryError之后Dump文件很小,程序又使用了NIO,那就可以检查下是否这方面出了问题。
* DirectMemory可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx)一致。

总结

深入理解JVM之Java内存区域与内存溢出异常_第9张图片
第二章总结

你可能感兴趣的:(深入理解JVM之Java内存区域与内存溢出异常)