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

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

作者: 小杰的快乐时光 | 来源:发表于2018-08-23 07:02 被阅读0次

    了解JVM内存结构的目的:
    明白堆内存空间大小的意义,在解决服务器性能问题,比如出现OutOfMemoryError等异常时,知道该怎么解决,以及出现该异常时会涉及到哪几个JVM内存区域,可以有针对性的对内存区域进行优化,快速解决问题。

    首先我们来看JVM内存模型布局,如下图所示

    JVM内存模型布局.png

    JVM内存模型主要分为:堆,方法区,虚拟机栈,本地方法栈,程序计数器
    注:永久代与方法区不能等同。
    Heap 堆是JVM内存结构中所占比例最大的一块,由新生代与老年代组成(为什么要把堆分代?)。而新生代又可以划分为 Eden区与Survivor区,Survivor区分为两个空间:From空间,To空间。Eden区,From空间,To空间的内存空间比例默认为8:1:1。

    答:首先我们需要知道新生代与老年代分别是什么意思。
    新生代:主要用来存放新生的对象,一般占据堆1/3的空间。老年代:主要存放应用程序中生命周期长的内存对象。
    分代的唯一理由是优化GC性能。如果不分代,那么所有对象都放在一起,那么GC回收时会扫描所有区域,查找哪些对象需要被回收,明显会降低性能。反之如果把分代,那么可以有效管理新生代与老年代,提高性能。

    简单的说明下内存模型中各区域的作用。
    堆:存放对象实例,几乎所有的对象实例都在这里分配内存。
    方法区:元空间存储已被虚拟机加载的类信息,常量,静态变量等数据,线程共享。别名叫做:Non-Heap(非堆),是为了与Heap 堆 区分开来;
    虚拟机栈:描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
    本地方法栈:本地方法栈则是为虚拟机使用到的Native方法服务。
    程序计数器:当前线程所执行的字节码的行号指示器

    通过控制参数来控制各区域的内存大小

    参数来控制各区域的内存大小.png

    控制参数总结:
    -Xms:设置堆的最小空间大小
    -Xmx:设置堆的最大空间大小
    -Xss:设置每个线程的栈大小
    -XX:NewSize:设置新生代最小空间大小。
    -XX:MaxNewSize:设置新生代最大空间大小。
    -XX:PermSize:设置永久代最小空间大小。
    -XX:MaxPermSize:设置永久代最大空间大小。
    老年代(Old Generation)空间大小 = 堆(Heap)空间大小 - 年轻代(Young Generation)空间大小
    -XX:NewRatio=n: 设置新生代和老年代的比值。如:为3,表示新生代与老年代比值为1:3,新生代占整个新生代老年代和的1/4
    -XX:SurvivorRatio=n : 新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个新生代的1/5
    -XX:maxDirectMemorySize:设置本机直接内存的空间大小,如不指定,则默认跟堆最大值一致。

    Hot Spot JVM 结构示意图

    Hot Spot JVM 结构示意图.png

    注:方法区与堆是所有线程共享的内存区域。Java栈,本地方法栈,程序计数器是运行时数据区线程私有的内存区域。

    接下来我们分析一下运行时数据区各区域的作用
    堆(Heap)
    JVM管理下最大的一块内存区域。用来存放对象实例与数组,被所有线程共享。堆是一种经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意的。

    几乎所有的对象实例都在这里分配内存,而大部分对象都是“朝生晚死”,因此堆是垃圾收集器管理的主要区域。而垃圾收集器采用的分代收集算法,所以还可以将堆分为:新生代与老年代。根据Java虚拟机的规定,堆可以实现固定大小的内存空间,也可以进行扩展空间,目前主流的虚拟机都是通过-Xmx和-Xms控制 设置扩展空间。如果堆空间无法再扩展,且堆没有多余内存完成实例分配,那么将会抛出 OutOfMemoryError 异常。

    栈(Stack)
    栈全称“虚拟机栈(JVM Stacks)”。存放基本型,对象引用,属于线程私有。栈是一种具有后进先出性质的数据结构,也就是说后存放的先取,先存放的后取。

    栈的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型。由于栈是私有的,所以在每个方法执行的时候都会创建一个“栈帧(Stack Frame)”,用于存储局部变量,操作数栈,动态链接,方法出口等信息,每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。每个栈帧中分配多少内存基本上是在类结构确定下来时就已知了。

    每个栈帧里对应的都维护一个“局部变量表(Loacl Variables)”和“操作数栈(Operand Stack),编译期可知的各种基本数据类型(boolean、byte、char、short、int、long、float、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。操作数栈是线程的实际操作台。

    64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    Stack的结构图.png

    比如计算加法:100+98,局部变量表(Loacl Variables)存储100和98,而操作数栈先把这两个值压进来再计算,完成后再把和弹出去


    48fde3be580dac7c9ff9fc89a93c35b0_r.jpg

    操作数栈与局部变量表的操作举例

    推荐阅读 https://www.zhihu.com/question/22739143/answer/113822382

    栈区域有包含两种异常:
    ①如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
    ②如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

    本地方法栈(Native Method Stacks)
    跟虚拟机栈(JVM Stack)所发挥的作用相似,区别在于:本地方法栈为虚拟机使用到的Native方法服务,而虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,可以将本地方法栈与虚拟机栈合二为一。
    本地方法栈也会跟虚拟机栈一样抛出StackOverflowError和OutOfMemoryError异常。

    程序计数器(Program Counter Register)
    包含较小的内存空间,作用是看作当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。因此,在分支,循环,跳转,异常处理,线程恢复等基础功能上都需要依赖这个计数器来完成。

    Java虚拟机会通过线程的上下文切换实现多线程,为了能线程切换后能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,因此程序计数器属于线程私有。如果线程执行的是一个Java方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果是一个Native方法,那么计数器的值为空。

    JVM中唯一一个不会报OutOfMemoryError异常的区域。

    方法区(Method Area)
    在方法区中有一个元空间,类被加载后的类信息,常量,静态变量,即时编译器编译后的代码存放在这,属于全局共享。在Java虚拟机规范中把方法区描述为堆的一个逻辑部分,但有一个别名叫做非堆(Non-Heap),目的是将Java堆区分开来。在HotSpot里也叫“永生代(Permanent Generation)”,但两者不能等同。之所以会有永久代的说法,是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区,这样就可以HotSpot的垃圾回收器可以像管理堆(Heap)一样管理方法区的内存,省去专门为方法区编写内存管理代码的工作。但是使用永久代来管理方法区更容易导致内存溢出(永久代有 -XX:MaxPermSize 上限),在JDK 8 中又彻底取消永久代。

    除了跟堆(Heap)一样不需要连续的内存和可以选择固定的大小或者进行扩展内存外,方法区还可以选择不实现垃圾收集,这样证明了方法区的垃圾回收行为较为少见,但并不代表没有,且并非数据进入方法区就如永久代名义一样“永久”的存在了。这个区域的回收目标主要是针对常量池的回收和对类型的卸载。类型的卸载条件非常苛刻,回收效果很差,但是方法区的回收却是非常有必要的,否则非常容易导致内存泄漏,抛出OutOfMemoryError异常。

    运行时常量池(Runtime Constant Pool)
    运行时常量池属于方法区的一部分。Class文件除了有类信息,字段,方法,接口等描述信息,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分将在类加载后进入方法区的运行时常量池。除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

    运行时常量池相对于Class文件常量池的重要特征是具有动态性。除了在编译器产生的常量可以进入运行时常量池外,还可以在运行期间将新的常量放入池中,比如String 的 intern方法。

    运行时常量区也会抛出OutOfMemoryError异常。

    另外还有一个不在JVM运行时数据区的一个内存空间,也不是Java虚拟机规范中定义的内存空间,那就是直接内存,也有可能导致OutOfMemoryError异常。

    直接内存(Direct Memory)
    在 JDK1.4 中新加入的NIO(New Input/Output)类,引入一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能避免在Java堆与Native堆中来回复制数据,显著提高性能。

    本机的直接内存不会受到Java堆大小的限制,但是既然是内存就会受到本机总内存的大小以及处理器寻址空间的限制。因此在配置虚拟机参数时,需注意直接内存,否则会抛出OutOfMemoryError异常。

    OutOfMemoryError异常分析

    首先我们需要明确 Java中内存溢出与内存泄漏各自的含义与区别
    内存溢出(OutOfMemory):是指程序在申请内存时没有足够的内存空间供其使用。
    内存泄露(MemoryLeak):是指程序在申请内存后无法释放已申请的内存空间。

    一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存迟早会被消耗尽,所以内存泄漏最终可能会导致内存溢出。所以内存泄漏一般问题不大,真正有危害的是内存泄漏的堆积而导致的内存溢出。

    (1)堆内存溢出

    Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space
    

    原因:对象不能被分配到堆内存中
    要解决这个异常,首先需要确认内存中的对象是否有必要存在,也就是要知道是出现了内存泄漏还是内存溢出。

    (2)虚拟机栈和本地方法栈溢出
    在虚拟机栈和本地方法栈中会出现两种异常
    ①如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
    ②如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

    但是在单线程下,无论是栈帧太大还是虚拟机内存容量太小,当内存无法分配时,均会抛出StackOverflowError异常。

    Exception in thread “main”: java.lang.StackOverflowError
    

    另外在多线程的情况下,如果不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程数。因为每个线程可以分配的栈容量内存越大,可以创建的线程数量就会减少。

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

    Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space
    

    原因:类或者方法不能被加载到老年代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库;

    还有一些其他的异常分析

    Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
    

    原因:创建的数组大于堆内存的空间

    Exception in thread “main”: java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?
    

    原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。

    Exception in thread “main”: java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)
    

    原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现

    收集器设置
    -XX:+UseSerialGC :设置串行收集器
    -XX:+UseParallelGC :设置并行收集器
    -XX:+UseParalledlOldGC :设置并行年老代收集器
    -XX:+UseConcMarkSweepGC :设置并发收集器

    垃圾回收统计信息
    -XX:+PrintHeapAtGC GC的heap详情
    -XX:+PrintGCDetails  GC详情
    -XX:+PrintGCTimeStamps  打印GC时间信息
    -XX:+PrintTenuringDistribution 打印年龄信息等

    参考文章
    https://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247483949&idx=1&sn=8b69d833bbc805e63d5b2fa7c73655f5&chksm=ebf6da52dc815344add64af6fb78fee439c8c27b539b3c0e87d8f6861c8422144d516ae0a837&scene=21#wechat_redirect

    相关文章

      网友评论

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

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