JVM之Java内存区域及内存溢出异常 …

来源:互联网 发布:金融投资网络销售好吗 编辑:程序博客网 时间:2024/06/07 01:48
一、Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
1、程序计数器
占用较小内存空间,可以看做当前线程所执行的字节码的行号指示器,线程拥有独立的计数器。
如果线程正在执行java方法,计数器记录正在执行的虚拟机字节码指令的地址;如果正在执行native方法,计数器值则为空。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2、Java虚拟机栈
与程序计数器一样,也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈和出栈的过程。
局部变量表存放了编译期可知的各种数据类型(可能是指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象有关的位置)和returnAddress类型(指向了一条字节码指令的地址)
这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
3、本地方法栈
与虚拟机栈的区别是,本地方法栈用于为native方法服务。
虚拟机规范没有对本地方法栈中方法使用的语言、使用方法和数据结构并没有强制规定,因此具体的虚拟机可以自由实现它,甚至有的虚拟机(如SunHotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
4、Java堆
对于大多数应用来说,Java堆(JavaHeap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
5、方法区
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-heap(非堆),目的应该是与Java堆区分开来。在HotSpot上习惯把方法区称为“永久代”(PermanentGeneration),本质上两者不等价,仅仅因为HotSpot实现把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。
使用永久代来实现方法区,并不好,因为这样更容易遇到内存溢出问题(永久代有-XX:MaxPermSize的上线,否则只要不达到内存上限就不会出问题),因此JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出。
垃圾收集行为在这个区域是比较少出现的,这区域的内存回收目标主要是针对常量池的回收和对类型的卸载
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将跑出OutOfMemoryError异常。
6、运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
7、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Natvie堆中来回复制数据。
二、以虚拟机HotSpot和常用的内存区域Java堆为例,深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
1、对象的创建
语言层面,创建对象(例如克隆、反序列化)都是使用new关键字而已,对象仅限普通Java对象,不包括数组和Class对象等。虚拟机遇到new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
加载完后,接下来虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可完全确定,给对象分配空间等同于把一块确定大小的内存从Java堆中划分出来。内存分配有“指针碰撞”和“空间列表”两种方式,具体哪个取决于垃圾收集器是否带有压缩整理功能。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始---init方法还没有执行,所有的字段都还为零。执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
2、对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。
对象头包括两部分信息:第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳;第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
3、对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
三、实战:OutOfMemoryError异常
在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能性。下面通过若干实例来验证异常发生的场景。
在实际工作中,要根据异常的信息快速判断是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理。
1、Java堆溢出
只要不断创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
将堆的最小值 -Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展。
通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出时Dump出当前的内存堆转储快照以便事后进行分析。
Java堆内存溢出时,异常堆栈信息会进一步提示“Java heapspace”,要解决这个区域的异常,一般的手段是先通过内存映像分析工具(如Eclipse MemoryAnalyzer)对Dump出来的堆转储快照进行分析,重点分析师内存泄漏(Memory Leak)还是内存溢出(MemoryOverflow)
如果是内存泄漏,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。如果不存在泄露,换句话说,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长。
内存泄漏检测工作“eclipse memoryanalyzer”,地址“http://archive.eclipse.org/mat/1.2/update-site/”
2、虚拟机栈和本地方法栈溢出
-Xoss参数用于设置本地方法栈大小,-Xss参数用于设置虚拟机栈大小,但HotSpot中栈容量只由-Xss设定。
有两种异常:线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
实验结果标明:在单线程下,无论由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StakOverflowError异常。如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常。
如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大栈和减少栈容量来换取更多的线程。如果没有这方面的经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。
3、方法区和运行时常量池溢出
常量池测试可以用使用String.intern()方法在池中创建大量的字符串
方法区测试可以在运行时产生大量的类去填满方法区
4、本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样




































0 0
原创粉丝点击