java虚拟机_05_JVM故障处理分析

    • 一、JVM中到底哪些区域是共享的?哪些是私有的?
    • 二、OutOfMemory错误分几种?
      • 2.1 “OutOfMemoryError:java heap size”
      • 2.2 java.lang.OutOfMemoryError:GC over head limit exceeded
      • 2.3 ”OutOfMemoryError: PermGen space”
        • 2.3.1 A.常量池(JDK1.6,JDK1.7以后常量池不会放在永久代中了。)
        • 2.3.2 B.class加载
      • 2.4 系统内存被占满
    • 四、堆栈溢出
      • 4.1 异常:java.lang.StackOverflowError
      • 4.2 异常:Fatal: Stack size too small
    • 五、常见配置汇总
      • 5.1 堆设置
      • 5.2 收集器设置
      • 5.3 垃圾回收统计信息
      • 5.4 并行收集器设置
      • 5.5 并发收集器设置
    • 六、OOM排查过程
      • 6.1 外在表现
      • 6.2 辅助工具
          • 6.2.1 top
          • 6.2.2 dmesg
      • 6.3 排查过程
        • 6.3.1 jps -mlvV
        • 6.3.2 jstat -gcutil 1234 1000 10
        • 6.3.3 dump内存
        • 6.3.4 使用eclipse MAT或者visaul VM查看dump文件分析原因
        • 6.3.5 查看gc日志

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

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

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

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

二、OutOfMemory错误分几种?

2.1 “OutOfMemoryError:java heap size”

public class HeapOOM {
    public static void main(String []args) {
        Listlist = new ArrayList();
        while(true) {
            list.add("内存溢出呀,内存溢出呀!");
        }
    }
}

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

  • java.lang.OutOfMemoryError: Java heap space

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

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

  • 年老代堆空间被占满

  • 原因分析:
    • 1.继承层次忒多了,Heap中 产生的对象是先产生 父类,然后才产生子类
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时间达到一定比例时,就会报错

2.2 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();
            }
        }
    }
}

2.3 ”OutOfMemoryError: PermGen space”

这个很常见,检查应用或调整堆内存大小。
永久代(PermGen space)是JVM实现方法区的地方,因此该异常主要设计到方法区和方法区中的常量池。永久代存放的东西有class和一些常量。perm是放永久区的。如果一个系统定义了太多的类型,那永久区可能会溢出。jdk1.8中,被称为元数据区。

Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。

更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。

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

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

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

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

2.3.2 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合理的装载各个类,并定期进行回收

2.4 系统内存被占满

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

  • 解决:

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

四、堆栈溢出

4.1 异常:java.lang.StackOverflowError

说明:一般就是递归没返回,或者循环调用造成

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

4.2 异常:Fatal: Stack size too small

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

五、常见配置汇总

5.1 堆设置

-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:设置持久代大小

5.2 收集器设置

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

5.3 垃圾回收统计信息

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

5.4 并行收集器设置

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

5.5 并发收集器设置

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

六、OOM排查过程

6.1 外在表现

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

6.2 辅助工具

6.2.1 top

用top查看,发现内存占用(%MEM)挺多,其他指标均正常。

6.2.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”

6.3 排查过程

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

6.3.1 jps -mlvV

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

6.3.2 jstat -gcutil 1234 1000 10

从这一步查出,full gc次数频繁,由此可见原因是老年代空间不足

6.3.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.

6.3.4 使用eclipse MAT或者visaul VM查看dump文件分析原因

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

6.3.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

你可能感兴趣的:(JAVA进阶)