java虚拟机_05_JVM故障处理分析

JVM中到底哪些区域是共享的?哪些是私有的?

  • Heap和Method Area是共享的,其他都是私有的

  • 为什么不建议在程序中显式的生命System.gc()?

    • 因为显式声明是做堆内存全扫描,也就是Full GC,是需要停止所有的活动的(Stop The World Collection),你的应用能承受这个吗?

为什么会产生OutOfMemory产生?

  • Heap(堆)内存中没有足够的可用内存了。
  • 这句话要好好理解,不是说Heap没有内存了,是说新申请内存的对象大于Heap空闲内存,比如现在Heap还空闲1M,但是新申请的内存需要1.1M,于是就会报OutOfMemory了,可能以后的对象申请的内存都只要0.9M,于是就只出现一次OutOfMemory,GC也正常了,看起来像偶发事件,就是这么回事。

- 年老代堆空间被占满

  • 原因分析:
    • 1.继承层次忒多了,Heap中 产生的对象是先产生 父类,然后才产生子类

持久代被占满

java.lang.OutOfMemoryError: PermGen space

Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。

OutOfMemory错误分几种?

分两种,分别是

  • “OutOfMemoryError:java heap size”
public class HeapOOM {
    public static void main(String []args) {
        List<String>list = new ArrayList<String>();
        while(true) {
            list.add("内存溢出呀,内存溢出呀!");
        }
    }
}

运行不久,就会报错,并看到这样的关键字

  • java.lang.OutOfMemoryError: Java heap space

这就是java堆空间的溢出,也就是说,Old区域剩余的内存,已经无法满足将要晋升到Old区域的对象大小,此时就会报出这样的错误。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:261)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
    at java.util.ArrayList.add(ArrayList.java:458)
    at com.doordu.soa.service.comm.HeapOOM.main(HeapOOM.java:10)

当程序运行几天后,发现系统超级慢,系统不断在做FULL GC,且每次做FULL GC的时间非常长,为何?因为绝大部分的内存对象都是活着的,所以在GC过程中标记活着的对象所需要的时间也很长。相应的,在GC过程中压缩环节由于存在大量存活的对象,所以空隙变得更小,那么压缩的时间也会变长。这个时候,系统所表现出来的状态是不断在做FULL GC,每次GC完后释放一点点内存,然后一下子就又满了,不断反复,当次数达到一定量,并且平均FULL GC时间达到一定比例时,就会报错

  • java.lang.OutOfMemoryError:GC over head limit exceeded

这种现场很难模拟,但是通常在发生这种现象前,系统会变得奇慢无比(值得注意的是,系统奇慢无比的原因不止这一种)
下面是一段不断做FULL GC的代码

public class GCOverHead {
    /*这里先占用掉Old区超过14M的空间*/
    public final static byte[]DEFAULT_BYTES = new byte[12 * 1024 * 1024];

    public static void main(String []args) {    
        List<byte[]>temp = new ArrayList<byte[]>();
        while(true) {
            temp.add(new byte[1024 * 1024]);
            if(temp.size() > 3) {
                temp.clear();
            }
        }
    }
}
  • ”OutOfMemoryError: PermGen space”
    这个很常见,检查应用或调整堆内存大小。
    永久代(PermGen space)是JVM实现方法区的地方,因此该异常主要设计到方法区和方法区中的常量池。永久代存放的东西有class和一些常量。perm是放永久区的。如果一个系统定义了太多的类型,那永久区可能会溢出。jdk1.8中,被称为元数据区。

A.常量池(JDK1.6,JDK1.7以后常量池不会放在永久代中了。)

string常量对象会在常量池(包含类名,方法名,属性名等信息)中以hash方式存储和访问,hash表默认的大小为1009,当string过多时,可以通过修改-xx:stringtableSize参数来增加Hash元素的个数,减少Hash冲突。

当常量池需要的空间大于常量池的实际空间时,也会抛出OutOfMemoryError: PermGen space异常。

例如,Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么可以通过String.intern方法来模拟一下运行时常量区的溢出.

B.class加载

由于class被卸载的条件十分的苛刻,这个class所对应的classLoader下面所有的class都没有活对象的应用才会被卸载。

方法区(Method Area)不仅包含常量池,而且还保存了所有已加载类的元信息。当加载的类过多,方法区放不下所有已加载的元信息时,就会抛出OutOfMemoryError: PermGen space异常。主要有以下场景:

使用一些应用服务器的热部署的时候,会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。

如果应用程序本身比较大,涉及的类库比较多,但分给永久代的内存(-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。

解决方法:在每次CGlib动态创建时,都重新给它设置一个classLoader,这样在运行代码就不会出现OOM,会发现大量的class被卸载。

VisualVm工具,查看PermGen标签页、类加载标签页中的趋势。随着类装载的数量增加,最终会出现了java.lang.OutOfMemoryError: PermGen space。

示例:如果不断产生新类,而没有回收,那最终很可能会导致永久区溢出。
解决的话从几方面入手:
● 增加MaxPermSize
● 减少系统需要的类数量
● 使用classloader合理的装载各个类,并定期进行回收

为什么会产生StackOverflowError

因为一个线程把Stack内存全部耗尽了,一般是递归函数造成的

堆栈溢出

异常:java.lang.StackOverflowError
说明:一般就是递归没返回,或者循环调用造成

线程堆栈满

异常:Fatal: Stack size too small
说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。

系统内存被占满

  • 异常:java.lang.OutOfMemoryError: unable to create new native thread
  • 说明:这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。
  • 分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。

  • 解决:

      1. 重新设计系统减少线程数量。
      1. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。

常见配置汇总

堆设置

-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小

收集器设置

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

垃圾回收统计信息

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

堆大小设置

年轻代的设置很关键
JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
典型设置(java):

-Xmx3550m -Xms3550m -Xmn2g –Xss128k

-Xmx3550m:设置JVM最大可用内存为3550M。

-Xms3550m:设置JVM促使内存为3550m。
此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xmn2g:设置年轻代大小为2G。
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss128k:设置每个线程的堆栈大小。

JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

-Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。
设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxPermSize=16m:设置持久代大小为16m。

-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

吞吐量优先的并行收集器

典型配置java:
-Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。

-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -
XX:+UseParallelOldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -
XX:+UseAdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

响应时间优先的并发收集器

典型配置:
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。

-XX:+UseParNewGC: 设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

年轻代大小选择

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
1. 并发垃圾收集信息
2. 持久代并发收集次数
3. 传统GC信息
4. 花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率

辅助信息-gc打印信息

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

-XX:+PrintGC:
输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails:
输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用。
输出形式:Application time: 0.5291524 seconds

-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用。
输出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC: 打印GC前后的详细堆栈信息。
输出形式:34.702: [GC {Heap before gc invocations=7:

-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。

OOM排查过程

一、外在表现
前段时间系统经常出现OOM,服务很不稳定,偶尔会有java进程不存在的情况,临时解决方案只能是重启。

二、辅助工具
1.top
用top查看,发现内存占用(%MEM)挺多,其他指标均正常。

2.dmesg
如果发现自己的java进程突然消失了,那么就要借助dmesg来查看开机之后的系统日志
命令为dmesg | grep -i ‘kill’或者搜索oom(out of memory),如果能搜索到相关信息,则说明java进程是被操作系统kill了,操作系统有一种机制,它会在机器的内存耗尽前,挑选几个占用内存较大的进程杀死(实际也是有一定的计算规则),通常被杀死的就是java进程,那么接下来就是看看是什么原因造成内存这么大。dmesg 输出的格式不易查看,可以通过命令进行转换。date -d “1970-01-01 UTC echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+12288812.926194"|bc seconds”

三、排查过程
OOM的原因一般为内存泄露,创建了对象不能释放,也有可能是突然间创建了大对象,有时加载过多的class也是原因。线上遇到OOM需要做两件事情第一个是dump内存,第二个是看下GC日志。

  1. jps -mlvV
    找出当前java进程号1234
    linux环境下可能要先执行
    export JAVA_HOME=//
    export PATH= J A V A H O M E / b i n : PATH

  2. jstat -gcutil 1234 1000 10
    从这一步查出,full gc次数频繁,由此可见原因是老年代空间不足

  3. dump内存
    接下来就是排查问题最重要的一步,dump内存最容易想到的是
    jmap -dump:format=b,file=heap.hprof 1234。
    注意如果用jmap来dump的话,一来非常慢,二来可能会出异常,在linux JDK1.6某个版本里使用jmap可能会让系统挂掉,可以通过-d64来解决(jmap -J-d64 -dump:format=b,file=dump.bin PID)。而jdk7的某个版本则会抛出异常,这是jdk的bug.
    一般dump下来的内存有几个G,而有时候dump下来只有一两百兆,说明jmap有问题,需要多执行几次jmap -dump才能得出正常结果,这个时候可以选用
    gcore 把整个内存dump出来,然后再使用jmap把core dump转换成heap dump。
    做法就是用gcore 1234命令来生成c版的core文件,再用命令jmap -dump:format=b,file=heap.hprof /bin/java core.1234.

  4. 使用eclipse MAT或者visaul VM查看dump文件分析原因
    在用eclipse分析dump文件时有可能因为文件过大而报异常,这时要调大eclipse本身占用的堆内存大小,在eclipse.ini文件里面,因为eclipse本身也是一个java程序。
    通过自动dump下来的内存文件很快发现有一个对象占用内存非常大,解决后系统恢复正常。

  5. 查看gc日志
    排查OOM通常要结合tomcat的日志、gc日志来查看。如果没有任何JVM参数设置,gc日志默认打印在stdout.log文件里,里面可能会打其他的日志,而且GC日志也不会输出时间,所以在JVM启动参数里最好加以下命令,规范下GC日志输出到/home/admin/logs/gc.log,并且打印GC时间。
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/home/admin/logs
    -Xloggc:/home/admin/logs/gc.log
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps

猜你喜欢

转载自blog.csdn.net/hardworking0323/article/details/81086734