JAVA GC(Garbage Collection)及OOM那些事

亡梦爱人 提交于 2019-12-03 16:29:59

JAVA运行时内存区域

 

    程序计数器

    一块很小的内存空间

  • 当前线程所执行的字节码的行号指示器
  • 当前线程私有
  • 不会出现OutOfMemoryError情况

java虚拟机栈

      通常存放基本数据类型,对象引用(一个指向对象起始地址的引用指针或一个代表对象的句柄),reeturnAddress类型(指向一条字节码指令的地址)

  • 线程私有,生命周期与线程相同
  • java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,存储局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息
  • StackOverflowError异常:当线程请求的栈深度大于虚拟机所允许的深度
  • OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存

本地方法栈

        与虚拟机栈相似,主要为虚拟机使用到的Native方法服务,在HotSpot虚拟机中直接把本地方法栈与虚拟机栈二合一

        和虚拟机栈一样可能抛出StackOverflowError和OutOfMemoryError异常。

Java堆(Java Heap)

        java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存储对象实例。java堆是垃圾收集器管理的主要区域。java堆还可以细分为:新生代与老年代。再细一点有Eden空间、Form Survivor空间、To Survivor空间等。

  • 可以通过-Xmx和-Xms控制堆的大小
  • OutOfMemoryError异常:当在堆中没有内存完成实例分配,且堆也无法再扩展时。

方法区

  • 线程间共享
  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • OutOfMemoryError异常:当方法区无法满足内存的分配需求时

运行时常量池

  • 方法区的一部分
  • 用于存放编译期生成的各种字面量与符号引用
  • OutOfMemoryError异常:当常量池无法再申请到内存时

直接内存

    直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,是jvm外部的内存区域,这部分区域也可能导致OutOfMemoryError异常。

  • NIO可以使用Native函数库直接分配堆外内存,堆中的DirectByteBuffer对象作为这块内存的引用进行操作
  • 大小不受Java堆大小的限制,受本机(服务器)内存限制
  • OutOfMemoryError异常:系统内存不足时

Java GC(Garbage Collection)

GC基本回收算法

其实这些算法和OOM没有太大关系,OOM产生的原因很简单,就是需要内存的时候没有内存了,但是对垃圾回收机制的理解可以让我们知道-Xmx  -Xms Xmn – MaxPermSize参数该怎么样设置,避免频繁full-GC(Full-GC会造成app短暂停顿,此时app是不会响应任何客户端请求的),并且合理设置参数.(1对于缓存过多的系统可以增大-Xmx,通过减小Xmn调整新生代年老代比例,2对于瞬时对象过多的系统,年老代的heap可以不用分配那么大)

  1. 引用计数(Reference Counting)
    比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
  2. 标记-清除(Mark-Sweep)
    此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
  3. 复制(Copying)
    此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
  4. 标记-整理(Mark-Compact)
    此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
  5. 分代(Generational Collecting)
    基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

GC垃圾回收器

        HotSpot JVM一共有4个垃圾回收器:Serial(串行)、Parallel / Throughput(并行)、CMS(并发)、and the new kid on the block G1(G1)。HotSpot默认使用Parallel / Throughput回收器,但它常常不是你运行程序的最佳选择。比如CMS和G1会使GC停顿(GC pause)发生的频率降低,但是对于每次停顿所花费的时间,很可能比Parallel回收器更长。在使用相同大小堆内存的情况下,Parallel回收器能带来更高的吞吐量。所以,需要根据可接受的GC停顿频率和持续时间,选择合适的垃圾回收器。

  GC算法本身可以是串行的(单线程),也可以是并行的(多线程)。因此当我们提到并发的GC时,并不代表它是并行完成的,相反当提到串行GC时,也并不意味着就一定会出现GC停顿。在GC的世界中,并发和并行是两个完全不同的概念。并发针对的是GC周期,而并行针对GC算法自身。

        Java 7中引入了G1回收器,它是JVM垃圾回收器中最新的组件。G1最大的优势就是解决了CMS中常见的内存碎片问题:GC周期会从老年代(Old Generation)中释放内存块,结果内存变得像瑞士奶酪那样千疮百孔,直到JVM对其无从下手了,才不得不停下来处理这些碎片。但是某些情况下其他回收器可能比G1有更好的表现,这完全取决于你的需求(可以通过GC日志分析

分代垃圾回收详述


如上图所示,为Java堆中的各代分布。

  1. Young(年轻代)复制算法
    年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
  2. Tenured(年老代)标记清除|标记整理
    年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
  3. Perm(持久代)很难发生GC
    用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
  4. OOM导致的原因:Full GC后,发现剩余的(heap,方法区)空间还是不够程序使用,这样就会导致OOM,  能导致Full GC:
    1. Tenured被写满
    2. Perm域被写满
    3. System.gc()被显示调用
    4. 上一次GC之后Heap的各域分配策略动态变化

常见内存溢出错误

OutOfMemoryError异常

        除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能,

        Java Heap 溢出

        一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess

        java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。

        出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

        如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。

        如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。

虚拟机栈和本地方法栈溢出

        如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

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

        这里需要注意当栈的大小越大可分配的线程数就越少。

运行时常量池溢出

        异常信息:java.lang.OutOfMemoryError:PermGen space

        如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

方法区溢出

        方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

        异常信息:java.lang.OutOfMemoryError:PermGen space

        方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点

OOM异常的解决思路

生成Dump快照文件:

  • 通过jvm参数—XX:-HeapDumpOnOutOfMemoryError可以让JVM在出现内存溢出是Dump出当前的内存转储快照
  • 用jmap生产dump文件,win通过任务管理器查看tomcat的进程pid,linux用ps命令查看进程pid,然后用jmap命令 

先通过内存映像分析工具(如Eclipse的Memory Analyzer)进行分析,常见的情况有:

  • 内存泄露,对象已经死了,无法通过垃圾收集器进行自动回收,通过找出泄露的代码位置和原因,才好确定解决方案;
  • 内存溢出,内存中的对象都还必须存活着,这说明Java堆分配空间不足,检查堆设置大小(-Xmx与-Xms),检查代码是否存在对象生命周期太长、持有状态时间过长的情况。

利用jmap分析CPU利用率高的原因

    1 先采用jps 或ps -ef|grep xxx找到 JAVA 程序的PID

    2 利用ps -Lfp pid或者ps -mp pid -o THREAD, tid, time或者top -Hp pid 找到占用CPU最高的的线程列表

    如 ps -mp 2633 -o THREAD,tid,time | sort -rn |head -10

    3 将线程ID转化为十六进制  printf "%x\n" 21742,比如得到: aaee

    4 打印线程的堆栈信息    jstack 2633 |grep e18 -A 30   如果运行在64位JVM上,可能需要指定-J-d64命令选项参数。

     5 根据提示,调整代码,重新发布程序

使用 jmap(Memory Map)和jhat(Java Heap Analysis Tool)查看堆栈情况

    1 jmap -permstat pid

    打印进程的类加载器和类加载器加载的持久代对象信息,输出:类加载器名称、对象是否存活(不可靠)、对象地址、父类加载器、已加载的类大小等信息

    2 jmap -heap pid

    使用jmap -heap pid查看进程堆内存使用情况,包括使用的GC算法、堆配置参数和各代中堆内存使用情况

    3 jmap -histo[:live] pid

    使用jmap -histo[:live] pid查看堆内存中的对象数目、大小统计直方图,如果带上live则只统计活对象

    4 jmap -dump:format=b,file=dumpFileName pid 

        用jmap把进程内存使用情况dump到文件中,再用jhat分析查看。jmap进行dump命令格式如下 jmap -dump:format=b,file=/tmp/dump.dat 21711 

    注意如果Dump文件太大,可能需要加上-J-Xmx512m这种参数指定最大堆内存,即jhat -J-Xmx512m -port 9998 /tmp/dump.dat

jstat(JVM统计监测工具)

    jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]

    vmid是Java虚拟机ID,在Linux/Unix系统上一般就是进程ID。interval是采样时间间隔。count是采样数目。比如下面输出的是GC信息,采样时间间隔为250ms,采样数为4:

    jstat -gc pid 60000 60

    root@ubuntu:/# jstat -gc 21711 250 4  S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT    192.0  192.0   64.0   0.0    6144.0   1854.9   32000.0     4111.6   55296.0 25472.7    702    0.431   3      0.218    0.649 192.0  192.0   64.0   0.0    6144.0   1972.2   32000.0     4111.6   55296.0 25472.7    702    0.431   3      0.218    0.649

    各列的含义:

    S0C、S1C、S0U、S1U:Survivor 0/1区容量(Capacity)和使用量(Used) EC、EU:Eden区容量和使用量 OC、OU:年老代容量和使用量 PC、PU:永久代容量和使用量 YGC、YGT:年轻代GC次数和GC耗时 FGC、FGCT:Full GC次数和Full GC耗时 GCT:GC总耗时

如果分析结果表明执行GC的时间只有0.1-0.3秒,那你就没必要浪费时间去进行GC优化。但是,如果GC的执行时间是1-3秒,或者超过10秒,GC将势在必行。

hprof(Heap/CPU Profiling Tool)

         hprof能够展现CPU使用率,统计堆内存使用情况。

         很少使用,经常使用java core来分析

OOM产生的文件及分析工具

    Java程序运行时,有时会产生JavaCore及HeapDump文件,它一般发生于Java程序遇到致命问题的情况下。

    为了能够保留Java应用发生致命错误前的运行状态,JVM在死掉前产生两个文件,分别为JavaCore及HeapDump文件。

HeapDump:内存使用情况

     可以使用IBM的HeapAnalyzer工具分析

    HeapDump文件是一个二进制文件,它保存了某一时刻JVM堆中对象使用情况,这种文件需要相应的工具进行分析,如IBM Heap Analyzer这类工具。这类文件最重要的作用就是分析系统中是否存在内存溢出的情况。

JavaCore:CPU使用情况

    可以使用IBM的jca工具分析

    通常情况下,频繁发生core dump是由于以下两类原因导致: 内存泄漏、内存碎片的问题

    JavaCore文件主要保存的是Java应用各线程在某一时刻的运行的位置,即JVM执行到哪一个类、哪一个方法、哪一个行上。它是一个文本文件,打开后可以看到每一个线程的执行栈,以stack trace的显示。通过对JavaCore文件的分析可以得到应用是否“卡”在某一点上,即在某一点运行的时间太长,例如数据库查询,长期得不到响应,最终导致系统崩溃等情况。

native_stderr.log:详细垃圾回收信息

      可以使用IBM的ga(IBM Pattern Modeling and Analysis Tool for Java Garbage Collector)  工具分析

    只要在WebSphere管理控制台的java进程属性里勾选“详细垃圾回收”

影响GC性能的参数

GC优化的最基本原则是将不同的GC参数用于2台或者多台服务器,并进行对比,并将那些被证明提高了性能或者减少了GC执行时间的参数应用于服务器。请谨记这一点。

GC优化需要考虑的Java参数

定义

参数

描述

堆内存空间

-Xms

Heap area size when starting JVM

启动JVM时的堆内存空间。

 

-Xmx

Maximum heap area size

堆内存最大限制

新生代空间

-XX:NewRatio

Ratio of New area and Old area

新生代和老年代的占比

 

-XX:NewSize

New area size

新生代空间

 

-XX:SurvivorRatio

Ratio ofEdenarea and Survivor area

伊甸园空间和幸存者空间的占比

我在进行GC优化时经常使用-Xms,-Xmx和-XX:NewRatio。-Xms和-Xmx是必须的。你如何设定NewRatio 会对GC性能产生十分显著的影响。有些人可能会问如何设定Perm区域的大小?你可以通过-XX:PermSize 和-XX:MaxPermSize参数来设定,

当OutOfMemoryError 错误发生并且是由于Perm空间不足导致时,另一个可能影响GC性能的参数是GC类型。下表列出了所有可选的GC类型(基于JDK6.0)

GC类型可选参数

分类

参数

备考

Serial GC

-XX:+UseSerialGC

 

Parallel GC

-XX:+UseParallelGC
-XX:ParallelGCThreads=value

 

Parallel Compacting GC

-XX:+UseParallelOldGC

 

CMS GC

-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=value
-XX:+UseCMSInitiatingOccupancyOnly

 

G1

-XX:+UnlockExperimentalVMOptions
-XX:+UseG1GC

在JDK6中这两个参数必须同时使用

除了G1 GC,可以通过每种类型第一行的参数来切换GC类型。最常用的GC类型是Serial GC。他专门针对客户端系统进行了优化。

影响GC性能的参数有很多,但是上面提到的参数会带来最显著的效果。请牢记,设定过多的参数不一定会减少GC执行时间

参考:

成为Java GC专家—如何优化Java垃圾回收机制的系列文章

http://www.importnew.com/author/wangxiaojie

http://www.importnew.com/1993.html

http://www.importnew.com/2057.html

http://www.importnew.com/3146.html

http://www.importnew.com/3151.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!