zoukankan      html  css  js  c++  java
  • 【JVM123】OOM分析和解决

    背景:项目在读入大数据并进行预处理拆分的过程中遇到OOM,亟待解决。

    解决方案:

    1. 拆分  2. 读入

    验证方案:分析GC

    参考:

    先要了解JVM内存区域

    https://blog.csdn.net/Dcwjh/article/details/105814720

    https://www.cnblogs.com/codingforum/p/6865617.html

    https://blog.csdn.net/ThinkWon/article/details/103827387?utm_medium=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.channel_param

    Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。

    非线程共享的那三个区域的生命周期与所属线程相同;而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说只发生在Heap上)的原因。

    OutOfMemoryError   |  StackOutflowError

    • 程序计数器(Program Counter Register):如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
    • Java虚拟机栈(JVM Stack):在Java虚拟机规范中,对这个区域规定了两种异常情况:
    1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。

    2. 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

        在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是StackOverflowError异常,而不会得到OutOfMemoryError异常。而在多线程环境下,则会抛出OutOfMemoryError异常。

        栈容量只能由-Xss参数来设定

      • 局部变量表
        • 局部变量表所需的内存空间在编译期间完成分配。
      • 操作数栈
        • 操作数栈的最大深度也是在编译的时候就确定了。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。
        • Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称Java虚拟机是基于栈的,这点不同于Android虚拟机,Android虚拟机是基于寄存器的。
        • 基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。
      • 动态链接
        • 每个栈帧都包含一个指向运行时常量池(在方法区中)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
        • 这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
      • 方法出口/返回地址
        • 方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
    • 本地方法栈(Native Method Stack):为使用到的本地操作系统(Native)方法服务。-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,
    • Java堆(Java Heap):是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。

        将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展。

    • 方法区(Method Area):是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。类的所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义。静态变量+常量+类信息(构造方法/接口定义) + 运行时常量池都存在方法区中。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。   

        可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量 -XX:MaxPermSize参数或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数

    • 直接内存(Direct Memory):直接从操作系统中分配,因此不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。

        通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致

    内存溢出

    在多线程情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。操作系统为每个进程分配的内存是有限制的,虚拟机提供了参数来控制Java堆和方法区这两部分内存的最大值,忽略掉程序计数器消耗的内存(很小),以及进程本身消耗的内存,剩下的内存便给了虚拟机栈和本地方法栈,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少。因此,如果是建立过多的线程导致的内存溢出,在不能减少线程数的情况下,就只能通过减少最大堆和每个线程的栈容量来换取更多的线程。

    内存泄漏(Memory Leak)

    内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。Java中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,当我们new了对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这边会造成内存泄露。

    对比内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。

    内存泄露:代码中的某个对象本应该被虚拟机回收,但因为拥有GCRoot引用而没有被回收。

    内存溢出:虚拟机由于堆中拥有太多不可回收对象没有回收,导致无法继续创建新对象。

    对象实例化分析

    对内存分配情况分析最常见的示例便是对象实例化:

    Object obj = new Object();

    • 这段代码的执行会涉及java栈、Java堆、方法区三个最重要的内存区域:
      •   在Java栈的本地变量表中保存obj作为引用类型(reference)的数据
      •   在Java堆中保存该引用的实例化对象
      •   在方法区中保存此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等)
    • 由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄池和直接使用指针。这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference中存放的是稳定的句柄地址,在对象呗移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前Java默认使用的HotSpot虚拟机采用的便是是第二种方式进行对象访问的。
      •   通过句柄池访问的方式如下:
      •   通过直接指针访问的方式如下:

    内存结构

    堆(线程共享):虚拟机启动时创建,用于存放对象实例,几乎所有的对象(包含常量池)都在堆上分配内存,当对象无法再该空间申请到内存时将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。可通过 -Xmx –Xms 参数来分别指定最大堆和最小堆。

    • 新生区(Yong Generation)
      类诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
      新生区分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?

    • 老年区(Old Generation)
      新生区经过多次GC仍然存活的对象移动到老年区。若老年区也满了,那么这个时候将产生MajorGC(FullGC),进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

    • 元数据区(MetaData Space) 元数据区取代了永久代(jdk1.8以前),本质和永久代类似,都是对JVM规范中方法区的实现,区别在于元数据区并不在虚拟机中,而是使用本地物理内存,永久代在虚拟机中,永久代逻辑结构上属于堆,但是物理上不属于堆,堆大小=新生代+老年代。元数据区也有可能发生OutOfMemory异常。

    Java虚拟机调优的目的:full gc的次数变少,full gc的时间越短越好。

     https://stackoverflow.com/questions/8209450/default-values-for-xmx-xms-maxpermsize-on-non-server-class-machines

    MappedByteBuffer

    https://www.jianshu.com/p/f90866dcbffc

    分析命令

    >jps

    https://www.cnblogs.com/shoshana-kong/p/11069146.html

    >jstat -gc xxxx

    https://blog.csdn.net/huiwen_82132000/article/details/84263040

    https://www.cnblogs.com/zhangfengshi/p/11342212.html

    https://docs.oracle.com/javase/6/docs/technotes/tools/share/jstat.html

    java Dump

    https://www.cnblogs.com/xiaoming0601/p/12268753.html

    Oracle官方:https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks002.html

    其他:https://www.kdgregory.com/index.php?page=java.outOfMemory

    https://www.kdgregory.com/index.php?page=java.refobj#ObjectLifeCycle

    http://blog.chinaunix.net/uid-26863299-id-3559878.html

  • 相关阅读:
    个人博客
    个人博客
    个人博客
    个人博客
    个人博客
    个人博客
    个人博客
    5.14
    5.13
    5.12
  • 原文地址:https://www.cnblogs.com/cathygx/p/13536368.html
Copyright © 2011-2022 走看看