搜索历史 清空记录
分享到
垃圾回收-实战篇
环卫在线新闻组 浏览次
上文 GC理论颇受大家好评,学习了之后,相信大家对GC的工作原理有了比较深刻的认识,这一篇我们继续趁热打铁,来学习下GC的实战内容,主要包括以下几点

  • JVM参数简介

  • 发生OOM的主要几种场景及相应解决方案

  • OOM问题排查的一些常用工具

  • GC日志格式怎么看

  • jstat与可视化APM工具构建

  • 再谈JVM参数设置

  • JVM参数简介在开始实践之前我们有必要先简单了解一下JVM参数配置,因为本文之后的实验中提到的JVM中的栈,堆大小,使用的垃圾收集器等都需要通过JVM参数来设置

    先来看下如何运行一个Java程序

    public class Test {    public static  void main(String[] args) {        System.out.println("test");    }}
  • 首先我们通过 javacTest.java 将其转成字节码

  • 其次我们往往会输入 javaTest 这样的命令来启动JVM进程来执行此程序,其实我们在启动JVM进程的时候,可以指定相应的JVM的参数,如下蓝色部分

  • 指定这些JVM参数我们就可以指定启动JVM进程以哪种模式(server或client),运行时分配的堆大小,栈大小,用什么垃圾收集器等等,JVM参数主要分以下三类

    1、标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;例如 -verbose:gc(输出每次GC的相关情况)

    2、非标准参数(-X),默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容,栈,堆大小的设置都是通过这个参数来配置的,用得最多的如下

    参数示例表示意义-Xms512mJVM启动时设置的初始堆大小为512M-Xmx512mJVM可分配的最大堆大小为512M-Xmn200m设置的年轻代大小为200M-Xss128k设置每个线程的栈大小为128k3、非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用,-XX:-option代表关闭option参数,-XX:+option代表要关闭option参数,例如要启用串行GC,对应的JVM参数即为-XX:+UseSerialGC。非Stable参数主要有三大类

  • 行为参数(BehavioralOptions):用于改变JVM的一些基础行为,如启用串行/并行GC

  • 参数示例表示意义-XX:-DisableExplicitGC禁止调用System.gc();但jvm的gc仍然有效-XX:-UseConcMarkSweepGC对老生代采用并发标记交换算法进行GC-XX:-UseParallelGC启用并行GC-XX:-UseParallelOldGC对FullGC启用并行,当-XX:-UseParallelGC启用时该项自动启用-XX:-UseSerialGC启用串行GC
  • 性能调优(PerformanceTuning):用于jvm的性能调优,如设置新老生代内存容量比例

  • 参数示例表示意义-XX:MaxHeapFreeRatio=70GC后java堆中空闲量占的最大比例-XX:NewRatio=2新生代内存容量与老生代内存容量的比例-XX:NewSize=2.125m新生代对象生成时占用内存的默认值-XX:ReservedCodeCacheSize=32m保留代码占用的内存容量-XX:ThreadStackSize=512设置线程栈大小,若为0则使用系统默认值
  • 调试参数(DebuggingOptions):一般用于打开跟踪、打印、输出等JVM参数,用于显示JVM更加详细的信息

  • 参数示例表示意义-XX:HeapDumpPath=./java_pid.hprof指定导出堆信息时的路径或文件名-XX:-HeapDumpOnOutOfMemoryError当首次遭遇OOM时导出此时堆中相关信息-XX:-PrintGC每次GC时打印相关信息-XX:-PrintGCDetails每次GC时打印详细信息画外音:以上只是列出了比较常用的JVM参数,更多的JVM参数介绍请查看文末的参考资料

    明白了JVM参数是干啥用的,接下来我们进入实战演练,下文中所有程序运行时对应的JVM参数都以VMArgs的形式写在开头的注释里,读者如果在执行程序时记得要把这些JVM参数给带上哦

    发生OOM的主要几种场景及相应解决方案有些人可能会觉得奇怪,GC不是会自动帮我们清理垃圾以腾出使用空间吗,怎么还会发生OOM,我们先来看下有哪些场景会发生OOM

    1、Java虚拟机规范中描述在栈上主要会发生以下两种异常

  • StackOverflowError异常这种情况主要是因为单个线程请求栈深度大于虚拟机所允许的最大深度(如常用的递归调用层级过深等),再比如单个线程定义了大量的本地变量,导致方法帧中本地变量表长度过大等也会导致StackOverflowError异常,一句话:在单线程下,当栈桢太大或虚拟机容量太小导致内存无法分配时,都会发生StackOverflowError异常。

  • 虚拟机在扩展栈时无法申请到足够的内存空间,会抛出OOM异常在刨根问底---一次OOM试验造成的电脑雪崩引发的思考 一文中我们已经详细地剖析了此例子,再来看看

  • /** * VM Args:-Xss160k */public class Test {    private void dontStop() {        while(true) {        }    }    public void stackLeakByThread() {        while (true) {            Thread thread = new Thread(new Runnable() {                @Override                public void run() {                    dontStop();                }            });            thread.start();        }    }    public static  void main(String[] args) {        Test oom = new Test();        oom.stackLeakByThread();    }}运行以上代码会抛出「java.lang.OutOfMemoryError:unabletocreatenewnativethread」的异常,原因不难理解,操作系统给每个进程分配的内存是有限制的,比如32位的Windows限制为2G,虚拟机提供了参数来控制Java堆和方法的这两部内存的最大值,剩余的内存为「2G-Xmx(最大堆容量)=线程数*每个线程分配的虚拟机栈(-Xss)+本地方法栈」(程序计数器消耗内存很少,可忽略),每个线程都会被分配对应的虚拟机栈大小,所以总可创建的线程数肯定是固定的,像以上代码这样不断地创建线程当然会造成最终无法分配,不过这也给我们提供了一个新思路,如果是因为建立过多的线程导致的内存溢出,而我们又想多创建线程,可以通过减少最大堆(-Xms)和减少虚拟机栈大小(-Xss)来实现。

    2、堆溢出(java.lang.OutOfMemoryError:Javaheapspace)

    主要原因有两点

  • 1.大对象的分配,最有可能的是大数组分配

  • 示例如下:

    /*** VM Args:-Xmx12m */class OOM {    static final int SIZE=2*1024*1024;    public static void main(String[] a) {        int[] i = new int[SIZE];    }}我们指定了堆大小为12M,执行「java-Xmx12mOOM」命令就发生了OOM异常,如果指定13M则以上程序就能正常执行,所以对于由于大对象分配导致的堆溢出这种OOM,我们一般采用增大堆内存的方式来解决

    画外音:有人可能会说分配的数组大小不是只有2* 1024* 1024*4(一个int元素占4个字节)=8M,怎么分配12M还不够,因为默认新老代1:2即4,8M都装不下数组,JVM进程除了分配数组大小,还有指向类(数组中元素对应的类)信息的指针、锁信息等,实际需要的堆空间是可能超过12M的,12M也只是尝试出来的值,不同的机器可能不一样

  • 2.内存泄漏我们知道在Java中,开发者创建和销毁对象是不需要自己开辟空间的,JVM会自动帮我们完成,在应用程序整个生命周期中,JVM会定时检查哪些对象可用,哪些不再使用,如果对象不再使用的话理论上这块内存会被回收再利用(即GC),如果无法回收就会发生内存泄漏

  • /*** VM Args:-Xmx4m */public class KeylessEntry {    static class Key {        Integer id;         Key(Integer id) {            this.id = id;        }          @Override        public int hashCode() {            return id.hashCode();        }    }    public static void main(String[] args) {        Map<Key,String> m = new HashMap<Key,String>();        while(true) {            for(int i=0;i<10000;i++) {                if(!m.containsKey(new Key(i))) {                    m.put(new Key(i), "Number:" + i);                }            }        }    }}执行以上代码就会发生内存泄漏,第一次循环,map里存有10000个keyvalue,但之后的每次循环都会新增 10000个元素,因为Key这个class漏写了equals方法,导致对于每一个新创建的newKey(i)对象,即使i相同也会被认定为属于两个不同的对象,这样 m.containsKey(newKey(i)) 结果均为false,结果就是HashMap中的元素将一直增加,解决方式也很简单,为Key添加equals方法即可,如下

    @Overridepublic boolean equals(Object o) {   boolean response = false;   if (o instanceof Key) {      response = (((Key)o).id).equals(this.id);   }   return response;}对于这种内存泄漏导致的OOM,单纯地增大堆大小是无法解决根本问题的,只不过是延缓了OOM的发生,最根本的解决方式还是要通过 heapdumpanalyzer 等方式来找出内存泄漏的代码来修复解决,后文会给出一个例子来分析

    3、java.lang.OutOfMemoryError:GCoverheadlimitexceeded

    Sun官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常

    导致的后果就是由于经过几个GC后只回收了不到2%的内存,堆很快又会被填满,然后又频繁发生GC,导致CPU负载很快就达到100%,另外我们知道GC会引起「StopTheWorld」的问题,阻塞工作线程,所以会导致严重的性能问题,产生这种OOM的原因与「java.lang.OutOfMemoryError:Javaheapspace」类似,主要是由于分配大内存数组或内存泄漏导致的,解决方案如下:

  • 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。

  • dump内存(后文会讲述如何dump出内存),检查是否存在内存泄露,如果没有,可考虑通过-Xmx参数设置加大内存。

  • 4、java.lang.OutOfMemoryError:Permgenspace

    在Java8以前有永久代(其实是用永久代实现了方法区的功能)的概念,存放了被虚拟机加载的类,常量,静态变量,JIT编译后的代码等信息,所以如果错误地频繁地使用String.intern()方法或运行期间生成了大量的代理类都有可能导致永久代溢出,解决方案如下

  • 是否永久代设置的过小,如果可以,适应调大一点

  • 检查代码是否有大量的反射操作

  • dump之后通过mat检查是否存在大量由于反射生成的代码类

  • 5、java.lang.OutOfMemoryError:RequestedarraysizeexceedsVMlimit

    该错误由JVM中的nativecode抛出。JVM在为数组分配内存之前,会执行基于所在平台的检查:分配的数据结构是否在此平台中是可寻址的,平台一般允许分配的数据大小在1到21亿之间,如果超过了这个数就会抛出这种异常

    碰到这种异常一般我们只要检查代码中是否有创建超大数组的地方即可。

    6、java.lang.OutOfMemoryError:Outofswapspace

    Java应用启动的时候分被分配一定的内存空间(通过-Xmx及其他参数来指定),如果JVM要求的总内存空间大小大于可用的本机内存,则操作系统会将内存中的部分数据交换到硬盘上

    如果此时swap分区大小不足或者其他进程耗尽了本机的内存,则会发生OOM,可以通过增大swap空间大小来解决,但如果在交换空间进行GC造成的「StopTheWorld」增加大个数量级,所以增大swap空间一定要慎重,所以一般是通过增大本机内存或优化程序减少内存占用来解决。

    7、Outofmemory:Killprocessorsacrificechild

    为了理解这个异常,我们需要知识一些操作系统的知识,我们知道,在操作系统中执行的程序,都是以进程的方式运行的,而进程是由内核调度的,在内核的调度任务中,有一个「Outofmemorykiller」的调度器,它会在系统可用内存不足时被激活,然后选择一个进程把它干掉,哪个进程会被干掉呢,简单地说会优先干掉占用内存大的应用型进程

    如图示,进程4占用内存最大,最有可能被干掉

    解决这种OOM最直接简单的方法就是升级内存,或者调整OOMKiller的优先级,减少应用的不必要的内存使用等等

    看了以上的各种OOM产生的情况,可以看出:GC和是否发生OOM没有必然的因果关系! GC主要发生在堆上,而从以上列出的几种发生OOM的场景可以看出,空间不足无法再创建线程,或者存在死循环一直在分配对象导致GC无法回收对象或者一次分配大内存数组(超过堆的大小)等都可能导致OOM,所以 OOM与GC并没有必然的因果关系

    OOM问题排查的一些常用工具接下来我们来看下如何排查造成OOM的原因,内存泄漏是最常见的造成OOM的一种原因,所以接下来我们以来看看怎么使用工具来排查这种问题,使用到的工具主要有两大类

    1、使用mat(EclipseMemoryAnalyzer)来分析dump(堆转储快照)文件

    主要步骤如下

  • 运行Java时添加「-XX:+HeapDumpOnOutOfMemoryError」参数来导出内存溢出时的堆信息,生成hrof文件,添加「-XX:HeapDumpPath」可以指定hrof文件的生成路径,如果不指定则hrof文件生成在与字节码文件相同的目录下

  • 使用MAT(EclipseMemoryAnalyzer)来分析hrof文件,查出内存泄漏的原因

  • 接下来我们就来看看如何用以上的工具查看如下内存泄漏案例

    /*** VM Args:-Xmx10m */import java.util.ArrayList;import java.util.List;public class Main {    public static void main(String[] args) {        List<String> list = new ArrayList<String>();        while (true) {            list.add("OutOfMemoryError soon");        }    }}为了让以上程序快速产生OOM,我把堆大小设置成了10M,这样执行「java-Xmx10m-XX:+HeapDumpOnOutOfMemoryErrorMain」后很快就发生了OOM,此时我们就拿到了hrof文件,下载MAT工具,打开hrof,进行分析,打开之后选择「LeakSuspectsReport」进行分析,可以看到发生OOM的线程的堆栈信息,明确定位到是哪一行造成的

    如图示,可以看到Main.java文件的第12行导致了这次的OOM

    2、使用jvisualvm来分析

    用第一种方式必须等OOM后才能dump出hprof文件,但如果我们想在运行中观察堆的使用情况以便查出可能的内存泄漏代码就无能为力了,这时我们可以借助 jvisualvm 这款工具,jvisualvm的功能强大,除了可以实时监控堆内存的使用情况,还可以跟踪垃圾回收,运行中dump中堆内存使用情况、cpu分析,线程分析等,是查找分析问题的利器,更骚的是它不光能分析本地的Java程序,还可以分析线上的Java程序运行情况,本身这款工具也是随JDK发布的,是官方力推的一款运行监视,故障处理的神器。我们来看看如何用jvisualvm来分析上文所述的存在内存泄漏的如下代码

    import java.util.Map;import java.util.HashMap;public class KeylessEntry {    static class Key {        Integer id;         Key(Integer id) {            this.id = id;        }          @Override        public int hashCode() {            return id.hashCode();        }    }    public static void main(String[] args) {        Map<Key,String> m = new HashMap<Key,String>();        while(true) {            for(int i=0;i<10000;i++) {                if(!m.containsKey(new Key(i))) {                    m.put(new Key(i), "Number:" + i);                }            }        }    }}打开jvisualvm(终端输入jvisualvm执行即可),打开后,将堆大小设置为500M,执行命令 javaXms500m-Xmx500mKeylessEntry,此时可以观察到左边出现了对应的应用KeylessEntry,双击点击open

    打开之后可以看到展示了CPU,堆内存使用,加载类及线程的情况

    注意看堆(Heap)的使用情况,一直在上涨

    此时我们再点击「HeapDump」

    过一会儿即可看到内存中对象的使用情况

    可以看到相关的TreeNode有291w个,远超正常情况下的10000个!说明HashMap一直在增长,自此我们可以定位出问题代码所在!

    3、使用jps+jmap来获取dump文件

    jps可以列出正在运行的虚拟机进程,并显示执行虚拟机主类及这些进程的本地虚拟机唯一ID,如图示

    拿到进程的pid后,我们就可以用jmap来dump出堆转储文件了,执行命令如下

    jmap -dump:format=b,file=heapdump.phrof pid拿到dump文件后我们就可以用MAT工具来分析了。但这个命令在生产上一定要慎用!因为JVM会将整个heap的信息dump写入到一个文件,heap比较大的话会导致这个过程比较耗时,并且执行过程中为了保证dump的信息是可靠的,会暂停应用!

    GC日志格式怎么看接下来我们看看GC日志怎么看,日志可以有效地帮助我们定位问题,所以搞清楚GC日志的格式非常重要,来看下如下例子

    /** *  VM Args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps  -XX:+UseSerialGC -XX:SurvivorRatio=8 */public class TestGC {    private static final int _1MB = 1024 * 1024;    public static void main(String[] args) {        byte[] allocation1, allocation2, allocation3, allocation4;        allocation1 = new byte[2 * _1MB];        allocation2 = new byte[2 * _1MB];        allocation3 = new byte[2 * _1MB];        allocation4 = new byte[4 * _1MB];    // 这里会出现一次 Minor GC    }}执行以上代码,会输出如下GC日志信息

    10.080: 2[GC 3(AllocationFailure)0.080: 4[DefNew: 56815K->280K(9216K),6 0.0043690secs] 76815K->6424K(19456K), 80.0044111secs]9 [Times:user=0.00sys=0.01,real=0.01secs]

    以上是发生MinorGC的GC是日志,如果发生FullGC呢,格式如下

    10.088: 2[FullGC 3(AllocationFailure)0.088: 4[Tenured: 50K->210K(10240K), 60.0009420secs] 74603K->210K(19456K),[Metaspace:2630K->2630K(1056768K)], 80.0009700secs]9[Times:user=0.01sys=0.00,real=0.02secs]

    两者格式其实差不多,一起来看看,主要以本例触发的MinorGC来讲解,以上日志中标的每一个数字与以下序号一一对应

  • 开头的0.080,0.088代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数

  • [GC 或者 [FullGC 说明了这次垃圾收集的停顿类型,注意不是用来区分新生代GC还是老年化GC的,如果有 Full,说明这次GC是发生了 StopTheWorld 的,如果是调用System.gc()所触发的收集,这里会显示 [FullGC(System)

  • 之后的 AllocationFailure 代表了触发GC的原因,在这个程序中我们设置了新生代的大小为10M(-Xmn10M),Eden:S0:S1=8:1:1(-XX:SurvivorRatio=8),也就是说Eden区占了8M,当分配allocation4时,由于将要分配的总大小为10M,超过了Eden区,所以此时会发生GC

  • 接下来的 [DefNew,[Tenured,[Metaspace 表示GC发生的区域,这里显示的区域名与使用的GC收集器是密切相关的,在此例中由于新生代我们使用了Serial收集器,此收集器新生代名为「DefaultNewGeneration」,所以显示的是 [DefNew,如果是ParNew收集器,新生代名称就会变为 [ParNew`,意为「ParallelNewGeneration」,如果采用「ParallelScavenge」收集器,则配套的新生代名称为「PSYoungGen」,老年代与新生代一样,名称也是由收集器决定的

  • 再往后 6815K->280K(9216K) 表示「GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)」

  • 0.0043690secs表示该块内存区域GC所占用的时间,单位是秒

  • 6815K->6424K(19456K)表示「GC前Java堆已使用容量->GC后Java堆已使用容量(java堆总容量)」。

  • 0.0044111secs 表示整个GC执行时间,注意和6中 0.0043690secs 的区别,后者专指相关区域所花的GC时间,而前者指的GC的整体堆内存变化所花时间(新生代与老生代的的内存整理),所以前者是肯定大于后者的!

  • 最后一个[Times:user=0.01sys=0.00,real=0.02secs]这里的user,sys和real与Linux的time命令所输出的时间一致,分别代表用户态消耗的CPU时间,内核态消耗的CPU时间,和操作从开始到结束所经过的墙钟时间,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O,等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以user或sys时间是可能超过real时间的。

  • 知道了GC日志怎么看,我们就可以根据GC日志有效定位问题了,如我们发现FullGC发生时间过长,则结合我们上文应用中打印的OOM日志可能可以快速定位到问题

    jstat与可视化APM工具构建jstat是用于监视虚拟机各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程中的类加载,内存,垃圾收集,JIT编译等运行数据,jstat支持定时查询相应的指标,如下

    jstat -gc 2764 250 22定时针对2764进程输出堆的垃圾收集情况的统计,可以显示gc的信息,查看gc的次数及时间,利用这些指标,把它们可视化,对分析问题会有很大的帮助,如图示,下图就是我司根据jstat做的一部分gc的可视化报表,能快速定位发生问题的问题点,如果大家需要做APM可视化工具,建议配合使用jstat来完成。

    再谈JVM参数设置经过前面对JVM参数的介绍及相关例子的实验,相信大家对JVM的参数有了比较深刻的理解,接下来我们再谈谈如何设置JVM参数

    1、首先Oracle官方推荐堆的初始化大小与堆可设置的最大值一般是相等的,即Xms=Xmx,因为起始堆内存太小(Xms),会导致启动初期频繁GC,起始堆内存较大(Xmx)有助于减少GC次数

    2、调试的时候设置一些打印参数,如-XX:+PrintClassHistogram-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-XX:+PrintHeapAtGC-Xloggc:log/gc.log,这样可以从gc.log里看出一些端倪出来

    3、系统停顿时间过长可能是GC的问题也可能是程序的问题,多用jmap和jstack查看,或者killall-3Java,然后查看Java控制台日志,能看出很多问题

    4、采用并发回收时,年轻代小一点,年老代要大,因为年老大用的是并发回收,即使时间长点也不会影响其他程序继续运行,网站不会停顿

    5、仔细了解自己的应用,如果用了缓存,那么年老代应该大一些,缓存的HashMap不应该无限制长,建议采用LRU算法的Map做缓存,LRUMap的最大长度也要根据实际情况设定

    要设置好各种JVM参数,还可以对server进行压测,预估自己的业务量,设定好一些JVM参数进行压测看下这些设置好的JVM参数是否能满足要求

    总结本文通过详细介绍了JVM参数及GC日志,OOM发生的原因及相应的调试工具,相信读者应该掌握了基本的MAT,jvisualvm这些工具排查问题的技巧,不过这些工具的介绍本文只是提到了一些皮毛,大家可以在再深入了解相应工具的一些进阶技能,这能对自己排查问题等大有裨益!文中的例子大家可以去试验一下,修改一下参数看下会发生哪些神奇的现象,亲自动手做一遍能对排查问题的思路更加清晰哦

    欢迎关注公众号与笔者共同交流哦^_^

    参考

    https://www.cnblogs.com/w-wfy/p/6415856.html

    http://songkun.me/2018/12/19/2018-12-19-java-why-xmx-xms-should-be-same/

    https://juejin.im/post/5dc6bf65f265da4d4a306e81

    https://plumbr.io/outofmemoryerror

    https://yq.aliyun.com/articles/512832

    看完本文有收获?请转发分享给更多人

    关注「ImportNew」,提升Java技能

    好文章,我在看❤️


    来源:ImportNew
    相关阅读
    关于我们| 会员服务| 法律声明| 网站地图| 联系我们| 意见反馈
    环卫在线版权所有 Copyright © 2012-2022 360hw.cn ,All Rights Reserved . 本站发布的所有内容,未经许可,不得转载,详见《知识产权声明》。
    沪ICP备19018799号-5、沪公网安备 11010502031237号