java虚拟机14

内存分析工具

  • 上一节中是通过jmap -histo来查看堆内存空间及对象实例情况,但是前提是程序必须是存活的,如果程序关闭了就不能查看之前的内存情况了,就需要通过以下的工具来查看

内存分析工具

  • VisualVM
    • 也是可以查看dump文件的
    • 文件->装入->选择指定的dump文件,就可以打开dump文件了
    • 上一节的cpu%项目中,虚拟机开启了-XX:+HeapDumpOnOutOfMemoryError,就是内存溢出后会自动保存dump文件,文件名形如java_pid15032.hprof
    • 控制面板的显示内容
      • 概要
      • 实例数
      • OQL控制台
    • 排查问题时一般不会使用,因为显示的内容与命令行工具相差不大
  • MAT
    • 是eclipse公司做的,基于eclipse平台开发的java程序
    • 一般几个g的内存是适合查看的,内存太大了这个工具会很卡,同时要修改ini的配置文件,把其中的堆内存最大大小改大

MAT控制面板

  • overview-概要

    • actctions

      • histogram-柱状图,实体大小

        点击某个类,list objects,

MAT中的重要概念

  • incoming references-对象的引入

  • outgoing references-对象的引出

  • d1指向D
    e1指向E
    A--里面有一个c1
    C--除了myc_还有两个对象d1和e1
    B--里面有一个c2
    D
    E

概念解释实例

  • package ex14;
    
    /**
     * Incoming Vs Outgoing References
     */
    
    class A {
          
          
        private C c1 = C.getInstance();
    }
    
    class B {
          
          
        private C c2 = C.getInstance();
    }
    
    class C {
          
          
        private static C myC = new C();
    
        public static C getInstance() {
          
          
            return myC;
        }
    
        private D d1 = new D();
        private E e1 = new E();
    }
    
    class D {
          
          
    }
    
    class E {
          
          
    }
    
    public class MATRef {
          
          
        public static void main(String[] args) throws Exception {
          
          
            A a = new A();
            B b = new B();
            Thread.sleep(Integer.MAX_VALUE);//线程休眠
        }
    }
    
    

c这个类

  • 对象的引入
    • c1
    • c2
    • myc
  • 对象的引出
    • d1
    • e1
    • c类的class

为什么要有对象的引入和对象的引出这两个概念

  • 为了分析内存泄漏问题,内存泄漏是与垃圾回收相关的,而在垃圾回收中怎么判断垃圾?是通过可达性分析来判定的,通过gcroots来查它的引出,所以可以通过对象的引入和对象的引出来分析对象的引用关系

MAT中的浅堆与深堆

  • Shallow Heap(浅堆)

    • 对象自身占用的内存
    • 对象占用内存包括对象头、实例数据、对齐填充,这三部分合起来要是8字节的整数倍
    • 一般对象和数组对象是不同的,数组对象内存包括对象头+类型变量大小*数组的长度+对齐填充
  • Retained Heap(深堆)

    • 是一个统计结果,如果自身对象被gc了,能够释放的内存大小
    • 深堆释放的内存肯定是包含浅堆的内存的
  • A--SH=10 RH=70
    B--SH=10 RH=30
    C--SH=10 RH=30
    D--SH=10 RH=10
    E--SH=10 RH=10
    F--SH=10 RH=10
    G--SH=10 RH=10
    • SH表示浅堆,RH表示深堆,后面的数字表示大小,连线表示引用关系

    • 假设把A干掉,其余的全部都能释放,因为A干掉,根据可达性分析算法,上面的都被干掉了,后面的都能被回收

    • package ex14.heap;
      
      /**
       * Shallow Heap Vs Retained Heap
       */
      
      class A {
              
              
          private static byte[] b = new byte[10 * 1000];
          private B b1 = new B();
          private C c1 = new C();
      }
      
      class B {
              
              
          private D d1 = new D();
          private E e1 = new E();
      }
      
      class C {
              
              
          private F f1 = new F();
          private G g1 = new G();
      }
      
      class D {
              
              
      }
      
      class E {
              
              
      }
      
      class F {
              
              
      }
      
      class G {
              
              
      }
      
      public class MATHeap {
              
              
          public static void main(String[] args) throws Exception {
              
              
              A a = new A();
              Thread.sleep(Integer.MAX_VALUE);//线程休眠
          }
      }
      
      

新增引用的影响

  • A--SH=10 RH=40
    B--SH=10 RH=30
    C--SH=10 RH=30
    D--SH=10 RH=10
    E--SH=10 RH=10
    F--SH=10 RH=10
    G--SH=10 RH=10
    H--SH=10 RH=10
  • 新增一个H引用,H引用的浅堆和深堆相等,就是它自己的对象内存

  • 除此之外,A的深堆大小发生了变化,由70变为40

使用MAT分析内存泄漏

案例代码

  • package ex14;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.IntStream;
    
    public class ObjectsMAT {
          
          
    
    
        static class A {
          
          
            B b = new B();
        }
    
        static class B {
          
          
            C c = new C();
        }
    
        static class C {
          
          
            List<String> list = new ArrayList<>();
        }
    
        static class Demo1 {
          
          
            Demo2 Demo2;
    
            public void setValue(Demo2 value) {
          
          
                this.Demo2 = value;
            }
        }
    
        static class Demo2 {
          
          
            Demo1 Demo1;
    
            public void setValue(Demo1 value) {
          
          
                this.Demo1 = value;
            }
        }
    
        static class Holder {
          
          
            Demo1 demo1 = new Demo1();
            Demo2 demo2 = new Demo2();
    
            Holder() {
          
          
                demo1.setValue(demo2);
                demo2.setValue(demo1);
            }
    
    
            private boolean aBoolean = false;
            private char aChar = '\0';
            private short aShort = 1;
            private int anInt = 1;
            private long aLong = 1L;
            private float aFloat = 1.0F;
            private double aDouble = 1.0D;
            private Double aDouble_2 = 1.0D;
            private int[] ints = new int[2];
            private String string = "1234";
        }
    
        Runnable runnable = () -> {
          
          
            Map<String, A> map = new HashMap<>();
    
            IntStream.range(0, 100).forEach(i -> {
          
          
                byte[] bytes = new byte[1024 * 1024];
                String str = new String(bytes).replace('\0', (char) i);
                A a = new A();
                a.b.c.list.add(str);
    
                map.put(i + "", a);
            });
    
            Holder holder = new Holder();
    
            try {
          
          
                //sleep forever , retain the memory
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        };
    
        void startKingThread() throws Exception {
          
          
            new Thread(runnable, "king-thread").start();
        }
    
        public static void main(String[] args) throws Exception {
          
          
            ObjectsMAT objectsMAT = new ObjectsMAT();
            objectsMAT.startKingThread();
        }
    }
    
    
  • 引用关系

    • holder指向demo1
      holder指向demo2
      map指向一个hashmap_存在100个
      King_thread--包含map和holder
      demo1
      demo2
      其中A->B->C_包含一个list->1MB的byte数组
    • map是一个hashmap,map里面有100个A,但是A又引用了B,B又引用了C,C中存在一个list,这个list又引用了1MB的数组

    • holder类中有两个对象demo1和demo2,它们相互引用

内存泄漏检查

  • 启动上述程序后,用mat分析内存问题
    • problem suspect 1
      • king-thread持有了99.68%的资源,同时解释到是一个HashMap&Node的实例
      • 阿里有一个代码规范,线程必须取名字,也就是此处的king-thread

使用MAT分析内存泄漏

支配树视图

  • open dominator tree for entire heap

  • 能够展示堆当中最大的对象,是以深堆大小倒序排列的

    • king-thread Thread
      • HashMap
        • HashMap$Node—一共有100个
          • A
  • 怎么算到底了呢?深堆和浅堆相等时

总结

怎么寻找内存泄漏的位置

  • 不论是通过mat中自带的猜想,还是通过支配树层层分析,内存泄漏的点就是那些深堆比较大,而浅堆比较小的对象

MAT中内存对比

  • jmap -dump:live,format=b,file=heap100.bin 进程号

  • jmap -dump:live,format=b,file=heap10.bin 进程号

  • 上述案例中考虑hashmap有10个节点和有100个节点的内存对比

    • 1.查看对象数量,打开柱状图,此时控制面板最后一个图标open another heap dump
      • 通过package分组,可以看到相同类的相对数量变化,在10中打开,显示A、B、C都是负数,说明相比100个节点时要少多少个此类的实例对象

线程视图

  • 每个dump视图的控制面板,if available,show each thread’s name, stack, frame locals, retained heap, etc
    • 通过name找到king-thread,它的浅堆只有120,但是深堆有20974824
      • lambda$new
        • HashMap
          • HashMap$Node
            • value
    • 一直向下找到c类中的list

柱状图视图

  • overview中的histogram,可以展示所有的类,同时再次点击具体的类,可以找出它的对象的引入和它的对象的引出

Path To GC Roots

  • 在柱状图中,点击某个对象,list object,然后选择with incoming references,这里就是具体的对象实例了,是有实际物理地址的,然后点击单个的实例,选择path to gc roots,然后可以选择引用的强弱类型
    • 例如选择上面例子中的c类,然后path to gc roots,最后还是回到了king-thread

高级功能—OQL

  • object query language,对象查询语言,官网(http://tech.novosoft-us.com/products/oql_book.htm)
    • select * from ex14.ObjectsMAT$A,然后点!,就可以找到所有的实例对象

使用MAT分析内存泄漏

实战演练

  • package cn.enjoyedu.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 类说明:
     */
    @RestController
    @RequestMapping("/jvm")
    public class MatController {
          
          
        @RequestMapping("/mat")
        public String mat() {
          
          
            ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();
            localVariable.set(new Byte[4096 * 1024]);// 为线程添加变量
            return "success";
        }
    
    
        @RequestMapping("/mat1")
        public String mat1() {
          
          
            ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();
            localVariable.set(new Byte[4096 * 1024]);// 为线程添加变量
            localVariable.remove();
            return "success";
        }
    }
    
    
  • java -jar -XX:+HeapDumpOnOutOfMemoryError jvm-1.0-SNAPSHOT.jar

  • ab -c 10 -n 1000 http://127.0.0.1:8080/jvm/mat

结果

  • probleam suspect 1

    • the thread org.apache.tomcat.util.thread.TaskThread @ 0xe3b69de8 http-nio-8080-exec-6 keeps local variables with total size 67,115,432(15.06%)bytes.

      The memory is accumulated in one instance of “java.lang.ThreadLocal T h r e a d L o c a l M a p ThreadLocalMap ThreadLocalMapEntry[]” loaded by “”.

    • 是tomcat里面一个taskThread,持有的局部变量,占有了15.06%的内存

  • 下面还有问题猜想,但是都是tomcat的taskthread

具体分析

  • 打开柱状图

  • 找出深堆大小排名前几的类,然后去找响应的对应的引入,再找path to gc roots,找到是http-nio-8080-exec-3的线程中的threadlocals

  • 这个时候就要去代码中找哪些地方用了threadlocal

    • 为什么要用threadlocal?

      在并发编程中,为了线程安全,在中间一段程序中get出set进去的东西

ThreadLocal内存泄漏

  • 基于ThreadLocalMap实现

    • entry,这个类是继承了WeakReference的,是弱引用,熬不过gc,只要发生垃圾回收,就会被回收

      • key,其中的key是一个弱引用
      • value
  • ThreadLocalRef
    CurrentThreadRef
    ThreadLocal指向value_并且是虚线
    Stack--ThreadLocalRef和CurrentThreadRef
    ThreadLocal
    CurrentThread
    entry--key和value
    Map
    • 由于ThreadLocal的key是弱引用,所以ThreadLocal指向entry中的key是一条虚线
  • 因此如果key被回收了,就找不到value了,就发生了内存泄漏

怎么解决泄漏问题呢?

  • 每次ThreadLocal.get()方法之后,都要继续执行ThreadLocal.remove()方法

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/115223154