Java内存区域与内存溢出异常

一.Java内存区域与内存溢出异常

1.Java虚拟机运行时数据区

 

 

 (1)程序计数器

当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变计数器的值来选取下一条需要执行的字节码指令;如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是本地(Native)方法,这个计数器值则应为空;此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

(2)Java虚拟机栈

线程私有,生命周期和线程相同,虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,对应着一个栈帧在虚拟机中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种Java虚拟机的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小(大小指的是变量槽的数量,至于每个变量槽需要多大内存空间则由虚拟机决定)。

可能会抛出的异常:

StackOverflowError异常:线程请求的栈深度大于虚拟机所允许的深度。

OutOfMemoryError异常:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存。(注:HotSpot虚拟机的栈容量是不可以动态扩展的,所以只要申请栈内存成功了就不会有OOM,若栈内存不足,则在申请栈内存时出现OOM)。

(3)本地方法栈

 与虚拟机栈发挥的作用相似,区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是则是为虚拟机使用到的本地(Native)方法服务。HotSpot虚拟机将本地方法栈和虚拟机栈合二为一。

(4)Java堆(线程共享)

是虚拟机所管理内存中最大的一块,所有线程共享,在虚拟机启动时创建。此内存区的唯一目的是存放对象实例,大部分对象都在堆上分配内存(逃逸分析技术使对象可以在栈上分配、标量替换)。Java堆可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms设定)。

(5)方法区(线程共享)

用来存储已被虚拟机加载的类型信息(类名、访问修饰符、常量池、字段描述、方法描述等)、常量、静态变量、即时编译器编译后的代码缓存等数据。在JDk8以前,由于HotSpot虚拟机的垃圾收集器的分代设计扩展到了方法区,因此程序员称呼方法区为“永久代”。永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小。JDK7已经把原本放在永久代的字符串常量池、静态变量等移到Java堆。JDK8完全放弃了永久代的概念,使用在本地内存(堆外内存,受限于物理内存)中实现的元空间来代替,把JDK7中还剩余的内容(主要是类型信息)全部移到元空间中。

运行时常量池(方法区的一部分):Class文件中有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到方法区的运行时常量池中。由符号引用翻译出来的直接引用有存储在运行时常量池中。

2.虚拟机类加载机制

3.对象的创建

(1)当虚拟机遇到一个字节码new指令时,先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有就先执行相应的类加载过程。

(2)虚拟机为新生对象分配内存,对象所需内存大小在类加载完成后便可完全确定。分配内存有2种方式,一种是指针碰撞(适合内存规整),另一种是空闲列表(内存不规整)。Java堆是否规整,由所采用的垃圾收集器是否带有空间压缩整理的能力决定(Serial、ParNew带压缩整理,CMS不带压缩整理)。解决分配内存并发安全问题有2种方案:一是对分配内存空间的动作进行同步处理(虚拟机采用CAS算法配上失败重试机制保证更新操作的原子性),另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓冲区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

(3)虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。

(4)设置对象头

(5)对象通过构造方法初始化

4.对象在堆内存中的存储布局

(1)对象头:包含2类信息,第一类是存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。第二类信息是类型指针,即对象头指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例(直接指针访问,而句柄访问不会在对象头保留类型指针)。若对象是一个Java数组,对象头中还保存了数组的长度。

(2)实例数据:各种类型的字段内容(包括父类的字段)。

(3)对齐填充:起占位符的作用,Java虚拟机要求任何对象的大小都必须是8字节的整数倍,不足的部分使用对齐填充来补全。

5.对象的访问定位

Java程序会通过栈上的reference数据来操作堆上的具体对象,主流有2种访问对象的方式:

(1)使用句柄访问,Java堆中划分一块内存作为句柄池,reference存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。

(2)直接指针,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference存储的是对象的地址。

优缺点比较:使用句柄访问的最大好处是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是很普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。使用直接指针的最大好处是速度更快,它节省了一次指针定位的时间开销。(大多虚拟机采用)

6.OutOfMemoryError异常

(1)Java堆溢出

可以通过参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现内存溢出时Dump出当前的内存堆栈转储快照以便进行时候分析。

处理办法:首先通过内存映像分析工具对Dump出来的堆转储快照进行分析。第一步首先确认内存中导致OOM的对象是否是必要的,也就是先分清楚到底是出现了内存泄漏还是内存溢出。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。如果不是内存泄漏,换句话说内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器内存相比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

(2)虚拟机栈和本地方法栈溢出

HotSpot虚拟机不区分虚拟机栈和本地方法栈,栈容量由-Xss参数设定。HotSpot虚拟机不支持动态扩展栈容量,所以除非在创建线程申请内存时就因为无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配时,HotSpot虚拟机抛出的都是StackOverflowError异常。

注意:栈内存越大,可以建立的线程数就越少。针对建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆容量和减少栈容量来换取更多线程。

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

在JDK6或更早的HotSpot虚拟机中,常量池都是分配在永久代中,可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小。注意:在运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。常见的场景有:程序使用CGLib字节码增强、动态语言、大量JSP或动态生成JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用等。

JDK8以后,元空间作为永久代的替换者登场,HotSpot提供了一些参数作为元空间的防御措施:

-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存的大小。

-XX:MetaspaceSzie:指定元空间的初始大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值,如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize的情况下,适当提高该值。

-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。

-XX:MaxMetaspaceFreeRatio:用于控制最大的元空间剩余容量的百分比。

(4)本机直接内存溢出

直接内存的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值一致。

posted @ 2020-09-16 15:02  万丈天涯  阅读(135)  评论(0编辑  收藏  举报