1.内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
2.内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
3.线程泄漏:指系统中动态分配的线程,在使用完毕后未关闭,导致相关资源未释放,结果导致一直占据系统资源,直到系统结束。直白点说,就是线程使用完毕后没有关闭或者正常停止,即线程泄漏。线程泄漏的原因:线程使用完后没有正常关闭导致。连接没关也会导致(如jdbc链接)
4. 线程溢出:指系统中达到了线程分配的极限,无法再创建新的线程时,还在收到新创建线程的请求,无法创建的一种状态,即线程溢出。

jvm参数类型

标准参数:
-version
-server
-help
-cp -classpath
例如:java -version

x参数:
非标准化参数。
-Xint :解释执行
-Xcomp : 第一次使用就编译成本地代码
-Xmixed : 混合模式,jvm自己来决定是否编译成本地代码
例如:
java -version

java version "1.8.0_11"
Java(TM) SE Runtime Environment (build 1.8.0_11-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, mixed mode)
 mixed mode 表示混合模式  默认是混合模式
java -Xint -version
修改成解释执行
java version "1.8.0_11"
Java(TM) SE Runtime Environment (build 1.8.0_11-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, interpreted mode)

XX参数
非标准化参数,相对不稳定,主要用于jvm调优和Debug。
Boolean类型:格式:-XX:[±]表示启用或者禁用name属性。
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
非Boolean类型:格式:-XX:=表示name属性的值是value
例如:XX:GCTimeRatio=19 -XX:MaxGCPauseMillis=500注意:

-Xmx -Xms 它们不是x参数,而是xx参数
-Xms等价于-XX:InitialHeapSize
-Xmx等价于-XX:MaxHeapSize
-Xmn 设置新生代大小
-XX:SurvivorRatio=4 设置年轻代中Eden区与一个Survivor区的大小比值 eden区在堆中占2/3,两个Survivor占用1/3,一个占1/6。默认值为8
-XX:NewRatio=4 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代).设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:MetaspaceSize=512m 设置metaspace大小
-XX:MaxMetaspaceSize=512m 设置metaspace最大大小

查看jvm运行时的值

服务器上启动一个项目,启动时创建一个jvm。
常用的:
-XX:+PrintFlagsInitial 查看初始值
-XX:+PrintFlagsFinal 查看最终值(初始值可能被修改掉)
-XX:+UnlockExperimentalVMOptions 解锁实验性参数
-XX:+UnlockDiagnosticVMOptions 解锁诊断参数
-XX:+PrintCommandLineFlags 打印命令行参数

例如:启动一个java进程并将结果重定向到一个文件

java -XX:+PrintFlagsFinal -version > flags.txt

android线程泄露排查 线程泄露是什么_List


=表示默认值;

:=表示被用户或JVM修改后的值

jps

jps  查看java进程
jps -l  显示应用main函数的包

jstat
查看jvm统计信息,查看类加载信息,垃圾回收信息,JIT编译信息。
1.查看类装载

[root@localhost bin]# jps -l
6884 sun.tools.jps.Jps
6762 org.apache.catalina.startup.Bootstrap
[root@localhost bin]# jstat -class 6762
Loaded  Bytes  Unloaded  Bytes     Time   
  2945  5765.6        0     0.0       3.08

loaded类的个数,bytes加载了多少个kb,uploaded卸载的类的个数,第二个bytes卸载了多少kb,time总共花费的时间

2.查看垃圾回收信息

[root@localhost bin]# jstat -gc 6762
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
896.0  896.0   0.0   896.0   7744.0   477.2    18992.0    15790.8   18048.0 17302.5 2176.0 1988.2     31    0.218   2      0.042    0.260

android线程泄露排查 线程泄露是什么_内存溢出_02

jstat -gc 6762 1000 10 1000表示一秒钟 10表示输出10次

3.查看JIT编译

jstat -compiler 6762
Compiled Failed Invalid   Time   FailedType FailedMethod
    1830      0       0     3.95          0

jinfo
查看已经在运行的jvm的参数值

[root@iz2ze5pjtwp4umzvw3ikwyz ~]# jinfo -flags  23634
Attaching to process ID 23634, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=536870912 -XX:MaxNewSize=178913280 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=89456640 -XX:OldSize=178978816 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops 
Command line:  -Xms256M -Xmx512M

查看单个参数:

[root@iz2ze5pjtwp4umzvw3ikwyz ~]# jinfo -flag MaxHeapSize 23634
-XX:MaxHeapSize=536870912

jmap
用于生成堆转储快照。
如果不使用jmap,想要获取堆转储快照,可以暴力的使用-XX:+HeapDumpOnOutOfMemoryError参数,表示让虚拟机在出现内存溢出oom的时候,自动生成dump文件;通过-XX : +HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成dump文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“吓唬”一下虚拟机,也能拿到dump文件。

jmap+MAT实战内存溢出

  • 堆内存溢出
/**
     * -Xmx32M -Xms32M  最大 最小堆内存
     * 构造一个堆内存溢出
     * java.lang.OutOfMemoryError: GC overhead limit exceeded
     */
    @Test
    public void  testHeap(){
        List<Teacher> teacherList=new ArrayList<Teacher>();
        int i=0;
        while(true){
            teacherList.add(new Teacher(i++,0, UUID.randomUUID().toString()));
        }
    }
  • 非堆内存溢出(Metaspace)
/**
     * -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
     * 构造一个非堆区内存溢出,Metaspace溢出
     * 出现:java.lang.OutOfMemoryError: Metaspace
     */
    public static void main(String[] args) {
        List<Class<?>> classesList=new ArrayList<Class<?>>();
        while(true){
            //动态的生成class
            classesList.addAll(Metaspace.createClasses());
        }
    }

Metaspace:

import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/*
 - 
 - 继承ClassLoader是为了方便调用defineClass方法,因为该方法的定义为protected
 - */
public class Metaspace extends ClassLoader {
    public static List<Class<?>> createClasses() {
        // 类持有
        List<Class<?>> classes = new ArrayList<Class<?>>();
        // 循环1000w次生成1000w个不同的类。
        for (int i = 0; i < 10000000; ++i) {
            ClassWriter cw = new ClassWriter(0);
            // 定义一个类名称为Class{i},它的访问域为public,父类为java.lang.Object,不实现任何接口
            cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
                    "java/lang/Object", null);
            // 定义构造函数<init>方法
            MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
                    "()V", null, null);
            // 第一个指令为加载this
            mw.visitVarInsn(Opcodes.ALOAD, 0);
            // 第二个指令为调用父类Object的构造函数
            mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
                    "<init>", "()V");
            // 第三条指令为return
            mw.visitInsn(Opcodes.RETURN);
            mw.visitMaxs(1, 1);
            mw.visitEnd();
            Metaspace test = new Metaspace();
            byte[] code = cw.toByteArray();
            // 定义类
            Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
            classes.add(exampleClass);
        }
        return classes;
    }
}

如何导出内存映像文件

  • 内存溢出自动导出
    虚拟机在出现内存溢出oom的时候,自动生成dump文件到目标地址
/**
     *-Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\FromC\新桌面\
     * 构造一个堆内存溢出
     *java.lang.OutOfMemoryError: GC overhead limit exceeded
     */
    @Test
    public void  testHeap(){
        List<Teacher> teacherList=new ArrayList<Teacher>();
        int i=0;
        while(true){
            teacherList.add(new Teacher(i++,0, UUID.randomUUID().toString()));
        }
    }
  • 使用jmap命令手动导出
    启动项目,然后jps -l 查找项目的进程号
[root@iz2ze5pjtwp4umzvw3ikwyz ~]# jmap -dump:format=b,file=heap.hprof 23634
Dumping heap to /root/heap.hprof ...
Heap dump file created

生成地址Dumping heap to /root/heap.hprof …

MAT分析内存溢出
使用eclipse独立版的mat->MemoryAnalyzer-1.5.0.zip

  • 使用mat分析内存镜像文件
    file->openfile 打开之前生成的镜像文件

    Problem Suspect 1:可以看到com.example.lzh.jvmtest.controller.MemoryController对象占了49%内存
    Problem Suspect 2:6,441 个class被java.lang.Class载入,占了10.89%,当然这是正常的,项目启动时加载的类
  • mat其他功能扩展
    查看对象数量:


    Objects:表示对象数量
    Shallow Heap:Shallow Size是对象本身占据的内存的大小,不包含其引用的对象。对于常规对象(非数组)的Shallow Size由其成员变量的数量和类型来定,而数组的ShallowSize由数组类型和数组长度来决定,它为数组元素大小的总和。集合类型的shallow heap的大小则指的是集合所包含的所有对象的大小的总和
    Retained Heap:Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C,C就是间接引用) ,并且排除被GC Roots直接或者间接引用的对象。
    例如:

    obj1的retainde size为obj1+obj2+obj4的shallow size之和
    obj3因为被GCRoots引用,不算。

查看哪个GCRoot引用了对象:

android线程泄露排查 线程泄露是什么_内存溢出_03


右键-》Merge…-》exclude all…

android线程泄露排查 线程泄露是什么_android线程泄露排查_04


tomcatThread 引用了 MemoryController -》userList 里面就是所有的teacher对象查看对象占的字节数:

android线程泄露排查 线程泄露是什么_android线程泄露排查_05


jstack实战死循环与死锁

可以打印jvm内部所有的线程,主要目的是定位线程出现长时间停顿的原因,如线程间死锁,死循环,请求外部资源导致的长时间等待等都是导致线程出现长时间停顿的原因。

  • 构造死循环:
/**
     * 死循环
     * */
    @RequestMapping("/loop")
    @ResponseBody
    public List<Long> loop(){
        String data = "{\"data\":[{\"partnerid\":]";
        return getPartneridsFromJson(data);
    }
        public static List<Long> getPartneridsFromJson(String data){
//        {\"data\":[{\"partnerid\":982,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":983,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":984,\"count\":\"10000\",\"cityid\":\"11\"}]}
        //上面是正常的数据
        List<Long> list = new ArrayList<Long>(2);
        if(data == null || data.length() <= 0){
            return list;
        }
        int datapos = data.indexOf("data");
        if(datapos < 0){
            return list;
        }
        int leftBracket = data.indexOf("[",datapos);
        int rightBracket= data.indexOf("]",datapos);
        if(leftBracket < 0 || rightBracket < 0){
            return list;
        }
        String partners = data.substring(leftBracket+1,rightBracket);
        if(partners == null || partners.length() <= 0){
            return list;
        }
        while(partners!=null && partners.length() > 0){
            int idpos = partners.indexOf("partnerid");
            if(idpos < 0){
                break;
            }
            int colonpos = partners.indexOf(":",idpos);
            int commapos = partners.indexOf(",",idpos);
            if(colonpos < 0 || commapos < 0){
                //partners = partners.substring(idpos+"partnerid".length());//1
                continue;
            }
            String pid = partners.substring(colonpos+1,commapos);
            if(pid == null || pid.length() <= 0){
                //partners = partners.substring(idpos+"partnerid".length());//2
                continue;
            }
            try{
                list.add(Long.parseLong(pid));
            }catch(Exception e){
                //do nothing
            }
            partners = partners.substring(commapos);
        }
        return list;
    }
  • 构造死锁:
private Object lock1 = new Object();
    private Object lock2 = new Object();
    /**
     * 死锁  线程1在等锁2(没释放锁1),线程2在等锁1(没释放锁2)
     * */
    @RequestMapping("/deadlock")
    @ResponseBody
    public String deadlock(){
        new Thread(()->{
            synchronized(lock1) {
                try {Thread.sleep(1000);}catch(Exception e) {}
                synchronized(lock2) {
                    System.out.println("Thread1 over");
                }
            }
        }) .start();
        new Thread(()->{
            synchronized(lock2) {
                try {Thread.sleep(1000);}catch(Exception e) {}
                synchronized(lock1) {
                    System.out.println("Thread2 over");
                }
            }
        }) .start();
        return "deadlock";
    }

打成jar,部署到服务器;

  • 定位死循环问题:
    在服务器上输入top命令,然后在浏览器上访问死循环的接口,慢慢开多个窗口访问;
  • android线程泄露排查 线程泄露是什么_List_06

  • 查看部署的jar,进程号18745;
  • android线程泄露排查 线程泄露是什么_内存溢出_07

  • 可以看到负载随着请求增多慢慢变大,如果负载过大,新的请求就接不进来了;很明显是18745进程造成的,cpu占用最多;
    输入 top -p 18745 -H 查看当前进程的所有线程;之前开的多个窗口测试,可以看到前面6个线程占用很多cpu;
  • android线程泄露排查 线程泄露是什么_java_08

  • jstack 18745 > 18745.txt(打印jvm内部所有线程)
[root@lzh ~]# printf "%x" 18776
4958     将第一个线程pid转成16进制

从服务器下载到win,打开。

搜索4958

android线程泄露排查 线程泄露是什么_android线程泄露排查_09


可以看到该线程是RUNNABLE状态,正在indexOf(),是getPartneridsFromJson,搜索一下getPartneridsFromJson,发现有6个线程都是在干这个事,所以是这个方法出问题。到此,死循环问题定位出来了。

  • 定位死锁问题
    访问死锁的接口;查看项目进程号;jstack 进程号 > 进程号.txt 打开txt,在最末尾发现死锁
  • android线程泄露排查 线程泄露是什么_List_10


可视化监控(jdk自带)

JDK提供了两个功能强大的可视化监控工具,JConsole和VisualVM。

JConsole

JConsole是一种基于JMX的可视化监视,管理工具。

jdk/bin 目录下找到jconsole.exe即可启动,启动后自动搜索出本机运行的所有虚拟机进程,相当于jps查询。

android线程泄露排查 线程泄露是什么_List_11


双击本地项目进程即可进入

android线程泄露排查 线程泄露是什么_List_12


“概述”页签显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用情况”、 “线程”、"类”、“CPU使用情况” 4种信息的曲线图,这些曲线图是后面“内存”、“线程”、 “类”页签的信息汇总。

内存监控
“内存”页签相当于可视化的jstat命令,用于监视受收集器管理的虚拟机内存(Java堆 和永久代)的变化趋势。通过代码来体验一下。

//-Xms100m -Xmx100m -XX:+UseSerialGC
  static  class  OOMObject{
      public byte[] placeholder =new byte[64*1024];
    }

    public static void fillHeap(int num) throws InterruptedException{
      List<OOMObject> list=new ArrayList<OOMObject>();
      for (int i=0;i<num;i++){
          //稍作延时,令监视曲线的变化更加明显
          Thread.sleep(50);
          list.add(new OOMObject());
      }
    }

    public static void main(String[] args) throws Exception{
        fillHeap(1000);
        System.gc();
    }

这段代码的作用是以 64KB/50毫秒的速度往Java堆中填充数据,一共填充1OOO次,使用JConsole的“内存”页 签进行监视,观察曲线和柱状指示图的变化。

android线程泄露排查 线程泄露是什么_内存溢出_13


程序运行后,在“内存”页签中可以看到内存池Eden区的运行趋势呈现折线状。而监视范围扩大至整个堆后,会发现曲线是一条向上增长的平滑曲线。并 且从柱状图可以看出,在1000次循环执行结束,运行System.gc()后,虽然整个新生代 Eden和Survivor区都基本被清空了,但是代表老年代的柱状图仍然保持峰值状态,说明被填 充进堆中的数据在System.gc()方法执行之后仍然存活。笔者的分析到此为止,现提两个小问 题供读者思考一下,答案稍后给出。

1)虚拟机启动参数只限制了 Java堆为100MB,没有指定-Xmn参数,能否从监控图中 估计出新生代有多大?

2)为何执行了 System.gc()之后,图4.6中代表老年代的柱状图仍然显示峰值状态,代 码需要如何调整才能让System.gcO回收掉填充到堆中的对象?

问题1答案:图"显示Eden空间为27 328KB,因为没有设置.XX:SurvivorRadio参数,所 以Eden与Suivivor空间比例为默认值8: 1,整个新生代空间大约为27 328KBX 125%=34 160KB。

问题2答案:执行完System.gc()之后,空间未能回收是因为List〈OOMObject> list对象 仍然存活,fillHeap()方法仍然没有退出,因此list对象在System.gc()执行时仍然处于作用域 之内气 如果把System.gc()移动到fillHeap()方法外调用就可以回收掉全部内存。

事实上,我这里的执行结果和书里的不一样

线程监控
如果上面的“内存”页签相当于可视化的jstat命令的话,“线程”页签的功能相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析。

死锁监控:

使用前面的死锁代码,启动项目,访问死锁接口:

android线程泄露排查 线程泄露是什么_List_14


android线程泄露排查 线程泄露是什么_java_15


线程13在等待一个被线程12持有的Object对象,而查看线程12,发现也在等待一个Object对象,被线程13持有,这样两个线程互相卡住了,不存在释放锁的希望。死循环监控:

访问死循环接口。浏览器开两窗口访问死循环接口;

android线程泄露排查 线程泄露是什么_内存溢出_16

这个监控死循环感觉不好使,得一个一个线程去看?

基于JVisualVM的可视化监控(jdk自带)

  • 监控本地java进程:
    双击打开,默认会把本机的java进程都给列出来;
    安装插件https://visualvm.github.io/pluginscenters.html,查看符合自己jdk版本的链接
    工具->插件

    换上链接,安装插件,BTrace和Visual GC
  • 监控远程tomcat:
    修改catalina.sh,重启tomcat:
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9011 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.net.preferlPv4Stack=true -Djava.rmi.server.hostname=192.168.169.145"
 port端口
 authenticate不启动认证
 ssl不启用ssl
 -Djava.net.preferlPv4Stack=true 优先使用ipv4
 hostname远程主机的ip地址

VisualVM操作:

右键远程,添加主机;

右键主机,添加JMX链接:

android线程泄露排查 线程泄露是什么_内存溢出_17


android线程泄露排查 线程泄露是什么_android线程泄露排查_18

  • 监控远程普通的java进程(springboot):
nohup java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9006 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.net.preferlPv4Stack=true -Djava.rmi.server.hostname=192.168.169.133 -jar jvm-test-0.0.1-SNAPSHOT.jar &

VisualVM操作:同上

  • 内存溢出
    使用之前的内存溢出代码,添加jvm参数-Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\FromC\新桌面\ 启动项目,访问内存溢出的接口;内存溢出后,会生成堆溢出文件。装入VisualVM。
  • android线程泄露排查 线程泄露是什么_List_19


  • android线程泄露排查 线程泄露是什么_java_20


  • android线程泄露排查 线程泄露是什么_java_21

  • 实例数达到了134576,这么多的实例,导致堆内存已经不够用,且没有被GC给回收
    网上的不错的内存溢出解决方案:

  • 死锁
    使用之前的死锁接口,启动项目,访问接口。
  • android线程泄露排查 线程泄露是什么_android线程泄露排查_22

  • 线程dump,最底下可以看到具体情况。
  • android线程泄露排查 线程泄露是什么_List_23

  • 死循环
    使用上面的死循环代码,启动项目,多开几个浏览器访问接口。
    visualVM查看cpu状况:可以看到getPartneridsFromJson方法占用cpu很高
  • android线程泄露排查 线程泄露是什么_List_24


  • android线程泄露排查 线程泄露是什么_内存溢出_25

  • 可以看到前三个线程占用cpu很高,dump线程,查看这三个线程
  • android线程泄露排查 线程泄露是什么_android线程泄露排查_26


  • android线程泄露排查 线程泄露是什么_内存溢出_27


  • android线程泄露排查 线程泄露是什么_java_28

  • 可以看到线程是RUNNABLE状态,是getPartneridsFromJson,搜索一下getPartneridsFromJson,发现有3个线程都是在干这个事,所以是这个方法出问题。到此,死循环问题定位出来了。
  • 基于Btrace的监控调试
    BTrace可以动态地向目标应用程序的字节码注入追踪代码。
    打开visualVM同时把Btrace的插件装上,配置环境变量:
  • android线程泄露排查 线程泄露是什么_内存溢出_29

  • 测试(拦截接口的参数):
    controller:
@RestController
@RequestMapping("ch4")
public class BTraceController {
    @RequestMapping("arg1")
    public String arg1(@RequestParam("name")String name) {
        return  "hello,"+name;
    }
}

找到启动的项目,右键,btrace application
然后把下面的代码都复制进去:改一改包路径

import com.sun.btrace.AnyType;
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.BTrace;
import com.sun.btrace.annotations.Kind;
import com.sun.btrace.annotations.Location;
import com.sun.btrace.annotations.OnMethod;
import com.sun.btrace.annotations.ProbeClassName;
import com.sun.btrace.annotations.ProbeMethodName;
//@BTrace表示这个是一个btrace脚本
@BTrace
public class PrintArgSimple {
	//表示在哪个类的哪个方法在什么时候进行拦截
	@OnMethod(
	        clazz="com.example.test.demo.controller.JstackController",
	        method="arg1",
	        //表示一进方法就拦截
	        location=@Location(Kind.ENTRY)
	)
	/**
	 * @ProbeClassName 拦截的类名
	 * @ProbeMethodName 拦截的方法名
	 * AnyType[] 拦截的参数名
	 */
	public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {
		BTraceUtils.printArray(args);
		BTraceUtils.println(pcn+","+pmn);
		BTraceUtils.println();
    }
}

点击start

android线程泄露排查 线程泄露是什么_android线程泄露排查_30


访问接口

android线程泄露排查 线程泄露是什么_java_31

  • Btrace使用详解