美文网首页
Jvm 内存区域与内存溢出异常

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

作者: yxfshard | 来源:发表于2017-09-29 19:30 被阅读0次

                            

            java将内存控制的权利交给了java虚拟机,如果不了解虚拟机是怎么样使用内存的,一旦出现内存泄漏或者异常的问题,将无从下手排查问题。本文是对运行数据区域划分,对象创建布局访问定位,堆栈溢出三个点学习所得出的总结。

    1.运行时数据区域划分 

            Java虚拟机在执行java程序的时候会把内存分为若干个不同的数据区域,这些区域的创建时间,销毁时间以及作用都不仅相同。有的区域随着虚拟机进程的启动而存在,而有的区域会随着用户的线程启动而建立和用户线程的结束而销毁。其中,java虚拟机管理的内存会包括以下几个运行时数据区域。

    (1) 程序计数器

           程序计算器是一块较小的内存区域,属于线程,同时各线程之间的程序计数器互不影响,所以这块区域我们称之为线程私有的内存。程序计数器是当前线程所执行字节码的行号指示器,字节码解释器就是通过这个计数器的值来选取下一个需要执行的字节码指令,分支,循环,跳转,异常处理,线程回复等功能都需要依赖这个计数器来完成。程序计数器是唯一一个在java虚拟机规范中没有规定内存溢出情况的区域。

    (2) Java虚拟机栈

           java虚拟机栈也是属于线程私有的,它的生命周期和线程是相同的。java虚拟机栈描述的是java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧。栈帧(Stack Frame)是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟机栈数据区的组成元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。每一个栈帧在编译程序代码的时候所需要多大的局部变量表,多深的操作数栈都已经决定了,并且写入到方法表的Code属性之中,一次一个栈帧需要多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。一个线程中方法调用可能很长,很多方法都处于执行状态。对于执行引擎来说,只有处于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与之相关联的方法称为当前方法(Current Method)在虚拟机规范中定义了两种异常情况,当线程请求的栈深度大于了虚拟机允许的深度将抛出栈溢出异常,当无法申请到可以动态扩展的内存时将会抛出内存溢出异常。

    (3) 本地方法栈

           简单地讲,一个Native Method就是一个java调用非java代码的接口;一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。本地方法栈与虚拟机栈非常相似,它们的区别就是虚拟机栈为java方法服务而本地方法栈则为Native方法服务。同样本地方法栈也有栈溢出和内存溢出两种异常。

    (4) java堆

            java堆几乎是java虚拟机中所管理的内存最大的一块,它在虚拟机启动的时候就已经创建,被所有的线程共享,其作用就是存放对象实例。java虚拟机规定所有的对象实例都要在堆上分配,但是随着技术的发展,所有的对象在堆上分配也不是那么绝对了。java堆是垃圾收集器管理的主要区域,从内存回收的角度看可以分为新生代和老年代,或者分为eden空间、from survivor空间、to survivor空间等,这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。java虚拟机规范规定,java堆可以处于物理上不连续的区域只要逻辑上连续即可,在实现时既可以是固定大小的也可以是可扩展的。堆无法完成内存分配或者动态扩展时将抛出内存溢出异常。

    (5) 方法区

           方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。即时编译器(Just In Time Compiler)简称JIT,JAVA程序最初是通过解释器(Interpreter)进行解释执行的,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,就会将这些“热点代码”编译成与本地机器相关的机器码,进行各个层次的优化。完成这个任务的编译器就是即时编译器(JIT)。java虚拟机规范对方法区等限定非常宽松,除了与java堆一样可以拥有不连续的内存区域支持动态扩展外还可以选择不实现垃圾收集。相对而言,垃圾收集在这个区域比较少出现,java虚拟机规范规定,当方法区无法满足内存分配需求是将抛出内存溢出异常。

    (6) 运行时常量池

           运行时常量池时方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外还有一项信息是常量池,用于存放编译器产生的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中保存。

    2.对象创建、布局、访问定位

            一个简单的创建对象语句包含的主要过程包括了类加载检查、对象分配内存、并发处理、内存空间初始化、对象设置、执行ini方法等。类加载检查主要是根据在常量池中是否能定位到一个类的符号引用来判断类是否已经被加载,如果没有加载则贤执行类的加载过程。java对象的内存分配根据java堆是否规整有两种分配方式,一种是指针碰撞,一种是空闲列表。指针碰撞是指Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。空闲列表是指Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。对象的创建在并发情况下并不是线程安全的,虚拟机采用CAS和LTAB机制解决同步问题。虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行。虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。执行new指令之后会接着执行init()方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。

            在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。对象头包括两部分信息:运行时数据和类型指针,运行时数据用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。类型指针即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

           Java程序需要通过栈上的引用数据来操作堆上的具体对象。对象的访问方式取决于虚拟机实现,目前主流的访问方式有使用句柄和直接指针两种。句柄,可以理解为指向指针的指针,维护指向对象的指针变化,而对象的句柄本身不发生变化;指针,指向对象,代表对象的内存地址。

    具柄方式 直接指针方式

    3.堆栈溢出

           研究内存溢出在遇到内存溢出异常时可以根据异常信息快速定位是哪个区域出现了内存溢出,知道什么样的代码可能导致这些区域内存溢出,以及应对措施。

    (1) java堆溢出

           通过参数-Xms和-Xmx可以设置jvm堆内存的最小值与最大值,如果这两个值一样就可以避免堆自动扩展。java堆内存问题主要是内存溢出和内存泄漏。内存泄漏是指对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。举个例子A对象引用B对象,A对象的生命周期比B对象的生命周期长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从而导致内存问题,因为如果A引用更多这样的对象,那将有更多的未被引用对象存在,并消耗内存空间。如果发生了内存泄漏,要分析泄漏对象是通过怎样的路径与GC Root相关联并导致收集器无法自动回收它们的,从而定位泄漏代码的位置。如果不是内存泄漏,就说明内存中的对象都还必须活着,就应当检查虚拟机的堆参数设置得是否合理;检查代码是否存在某些对象的生命周期过长,尝试减少程序运行期的内存消耗。

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

            栈容量由参数-Xss参数设定,java虚拟机规定了两种异常。一种是线程请求的栈深度大于虚拟机所允许的最大深度此时抛出StackOverFlowError异常;一种是虚拟机在扩展栈时无法申请到足够的内存空间此时抛出OutOfMemoryError异常。在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。在多线程下,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽,因为操作系统分配给每个进程的内存是有限制的。

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

            由于运行时常量池是方法区的一部分,所以这两个区域的异常信息将一起分析。可以通过-XX:PermSize和-XX:MaxPermSize限制方法区域大小。方法区溢出是一种常见的内存溢出异常,因为一个类要被垃圾收集器回收掉判断条件是相当苛刻的。在经常动态生成class的应用中、大量jsp活着动态生产jsp文件的影,需要特别注意类的回收情况。

    相关文章

      网友评论

          本文标题: Jvm 内存区域与内存溢出异常

          本文链接:https://www.haomeiwen.com/subject/wmaeextx.html