Java堆分析

本博客为炼数成金JVM教程第八课

目录

  • 内存溢出(OOM)的原因
  • MAT使用基础
    • 浅堆(shallow Heap) 和 深堆(Retained Heap)
    • 显示入引用(incoming) 和出引用(outgoing)
    • 支配树
  • 使用Visual VM 分析堆
  • Tomcat OOM分析案例

    JVM中,有哪些内存区间?
    JVM中的内存区间

    永久区不属于堆,但属于JVM进程的空间。
    线程栈:是操作系统分配给JVM的一块内存区域。虽然不在堆里面,但是这几块空间加起来不能超过操作系统可以分配给JVM的内存空间。
    这四块内存空间的任意一块不满足内存要求,都会导致内存溢出。

堆溢出

public static void main(String args[]){
    ArrayList<byte[]> list=new ArrayList<byte[]>();
    for(int i=0;i<1024;i++){
        list.add(new byte[1024*1024]);
    }
}

占用大量堆空间,直接溢出

Exception in thread "main" java.lang.OutOfMemoryError: **Java heap space**
at geym.jvm.ch8.oom.SimpleHeapOOM.main(SimpleHeapOOM.java:14)

解决方案:
1: 增加堆空间
2:及时释放内存

永久区溢出

由于类的原信息保存在永久区,我们生成大量的类,导致永久区溢出
public static void main(String[] args) {
    for(int i=0;i<100000;i++){
        CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
    }
}
Caused by: java.lang.OutOfMemoryError: **PermGen space**
[Full GC[Tenured: 2523K->2523K(10944K), 0.0125610 secs] 2523K->2523K(15936K), 
[Perm : 4095K->4095K(4096K)], 0.0125868 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 4992K, used 89K [0x28280000, 0x287e0000, 0x2d7d0000)
  eden space 4480K,   2% used [0x28280000, 0x282966d0, 0x286e0000)
  from space 512K,   0% used [0x286e0000, 0x286e0000, 0x28760000)
  to   space 512K,   0% used [0x28760000, 0x28760000, 0x287e0000)
 tenured generation   total 10944K, used 2523K [0x2d7d0000, 0x2e280000, 0x38280000)
   the space 10944K,  23% used [0x2d7d0000, 0x2da46cf0, 0x2da46e00, 0x2e280000)
 **compacting perm gen  total 4096K, used 4095K [0x38280000, 0x38680000, 0x38680000)
   the space 4096K,  99% used [0x38280000, 0x3867fff0, 0x38680000, 0x38680000)**
    ro space 10240K,  44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)
    rw space 12288K,  52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000)

解决方案:
1:增大Perm区
2:允许Class回收

Java栈溢出
这里的栈溢出指:在创建线程的时候,需要为线程分配栈空间,这个栈空间是向操作系统请求的,如果操作系统无法给出足够的空间,就会抛出OOM
Java堆分析_第1张图片
如图所示:堆空间和线程栈空间的总和是不可以操作操作系统可分配的总空间的。

public static class SleepThread implements Runnable{
    public void run(){
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public static void main(String args[]){
    for(int i=0;i<1000;i++){
        new Thread(new SleepThread(),"Thread"+i).start();
        System.out.println("Thread"+i+" created");
    }
}

运行参数:
-Xmx1g -Xss1m

Exception in thread "main" java.lang.OutOfMemoryError:  **unable to create new native thread**

解决方法:
1:减少堆内存
2:减少线程栈大小

堆+线程栈的大小不大于总内存,所以减小堆内存,给线程栈足够的空间,让其生成更多的线程
或者减小线程栈的大小。原先一个线程需要1M的内存,如果缩小至512K,那么就可以生成更多的线程

直接内存溢出
ByteBuffer.allocateDirect() 无法从操作系统获取足够的空间

Java堆分析_第2张图片

操作系统可分配给JVM的内存空间是一定的,堆空间,线程栈空间和直接内存加起来不能大于操作系统可分配的大小。

for(int i=0;i<1024;i++){
    ByteBuffer.allocateDirect(1024*1024);
    System.out.println(i);
      System.gc();
}

运行参数:-Xmx1g -XX:+PrintGCDetails
运行结果:
这里写图片描述
Java堆分析_第3张图片

从堆栈中可以看到,eden, feom,to,permgen 的空间都非常的富足。
则可以判断是直接内存使用过度导致的。

解决方法:
1: 减少堆内存
2: 有意触发GC
减少堆空间之后,操作系统可以将更多的内存分配给直接内存
直接内存需要GC回收,但是直接内存无法引起GC。也就是直接内存使用满时,无法触发GC。
如果堆空间很富余,那么无法触发GC,直接内存可能就会溢出。
如果堆空间触发GC,直接内存可以回收

MAT使用基础

Memory Analyzer(MAT)
基于Eclipse的软件
http://www.eclipse.org/mat/
使用它来分析dump的堆

柱状图显示,显示每个类的使用情况,比如类的数量,所占空间等。
Java堆分析_第4张图片

支配树
Java堆分析_第5张图片

什么是支配树?
在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B
如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配者被回收,被支配对象也被回收

Java堆分析_第6张图片

从这里可以理解:通过支配树可以看到,一个对象被回收后,有多少内存空间可以被回收

显示线程信息
Java堆分析_第7张图片

显示堆总体信息,比如消耗最大的一些对象等
Java堆分析_第8张图片

显示引用对象
Java堆分析_第9张图片
with outgoing references 显示一个对象引用的对象
with incoming references 显示引用这个对象的对象

深堆浅堆
Java堆分析_第10张图片

浅堆(Shallow Heap):
一个对象结构所占用的内存大小
String
3个int类型以及一个引用类型合计占用内存 3*4 + 4 = 16个字节,再加上对象头的8个对象占用的空间,即浅堆的大小是16 + 8 = 24 个字节
对象大小按照8 字节对齐(一定是8的倍数)
浅堆大小和对象的内容无关,只和对象的结构有关

深堆(Retained Heap)
一个对象被GC回收后,可以真实释放的内存大小,即只能通过对象访问到的(直接或者间接)所有对象的浅堆之和(支配树)

例子:

public class Point {
    private int x;
    private int y;
    public Point(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }
}
public class Line {

    private Point startPoint;
    private Point endPoint;
    public Line(Point startPoint, Point endPoint) {
        super();
        this.startPoint = startPoint;
        this.endPoint = endPoint;
    }

    public static void main(String[] args) throws InterruptedException {
        Point a = new Point(0,0);
        Point b = new Point(1,1);
        Point c = new Point(5,3);
        Point d = new Point(9,8);
        Point e = new Point(6,7);
        Point f = new Point(3,9);
        Point g = new Point(4,8);
        Line line1 = new Line(a, b);
        Line line2 = new Line(a, c);
        Line line3 = new Line(d, e);
        Line line4 = new Line(f, g);
        a = null;
        b = null;
        c = null;
        d = null;
        e = null;
        Thread.sleep(1000000);
    }
}

内存引用图如下:
Java堆分析_第11张图片

我们使用jps命令和jamp命令打出dump信息

jmap -dump:format=b,file=test1.hprof 9736

对象的关系如下所示:
Java堆分析_第12张图片
可以看到,所有的Point实例浅堆和深堆的大小都是16字节。而dLine对象,浅堆为16字节,深堆也是16字节,这是因为dLine对象内的两个点f和g没有被设置为null,因此,即使dLine被回收,f和g也不会被释放。对象cLine内的引用对象d和e由于仅在cLine内还存在引用,因此只要cLine被释放,d和e必然也作为垃圾被回收,即d和e在cLine的保留集内,因此cLine的深堆为16*2+16=48字节。
对于aLine和bLine对象,由于两者均持有对方的一个点,因此,当aLine被回收时,公共点a在bLine中依然有引用存在,故不会被回收,点a不在aLine对象的保留集中,因此aLine的深堆大小为16+16=32字节。对象bLine与aLine完全一致。

使用Visual VM 分析堆
Java堆分析_第13张图片

Java堆分析_第14张图片

Java堆分析_第15张图片
Java堆分析_第16张图片
Java堆分析_第17张图片

Tomcat OOM 分析案例

  • Tomcat OOM
    • tomcat 在接收大量请求时发生OOM,获取堆Dump文件,进行分析
  • 使用MAT打开堆
  • 分析目的:
    • 找出OOM的原因
    • 推测系统OOM的状态
    • 给出解决这个OOM的方法

Java堆分析_第18张图片

可见整个堆的大小是29.7M,可以推测系统运行时堆的大小是32M左右,导致了OOM。
retained size为16.4M,指的是释放 StandardManager 这个对象之后可以释放的空间。从这里可以看到,我们基本可以推断,OOM跟session相关,它支配着16.4M的堆的内容。

我们需要关心StandardManager 引用了哪几个对象。
Java堆分析_第19张图片

Java堆分析_第20张图片
可见这个sessions 占用了大量的内存。

再进一步查看:
Java堆分析_第21张图片
这里有大量的HashMap

Java堆分析_第22张图片
在这里我们可以认为,系统保存了过多的session没有被释放,导致了OOM

所以我们要确认是否有大量的session存在?
Java堆分析_第23张图片
可见,有9K多个session,每个session的retained size为1.5M, 这占用了大量的空间

我们在看具体的session的信息
Java堆分析_第24张图片

接着我们根据session的创建时间,计算出每秒创建的session是多少个(获取出最大的session创建时间和最小的session创建时间)
Java堆分析_第25张图片

9941/((1403324677648-1403324645728)/1000) = 320次/秒
所以结论就是:
tomcat在32M内存下,承受每秒320次请求,持续31秒,合计9941次请求,导致OOM

解决方案如下:
1: OOM 由于保存session过多引起,可以考虑增加堆大小
2: 如果应用允许,缩短session的过期时间,使得session可以及时过期,并回收

你可能感兴趣的:(深入JVM内核—原理,诊断与优化)