关于java:JVM下篇性能监控与调优篇补充浅堆深堆与内存泄露

7次阅读

共计 7996 个字符,预计需要花费 20 分钟才能阅读完成。

https://gitee.com/vectorx/NOT…

https://codechina.csdn.net/qq…

https://github.com/uxiahnan/N…

[toc]

补充:浅堆深堆与内存泄露

1. 浅堆(Shallow Heap)

浅堆是指一个对象所耗费的内存。在 32 位零碎中,一个对象援用会占据 4 个字节,一个 int 类型会占据 4 个字节,long 型变量会占据 8 个字节,每个对象头须要占用 8 个字节。依据堆快照格局不同,对象的大小可能会同 8 字节进行对齐。

以 String 为例:2 个 int 值共占 8 字节,对象援用占用 4 字节,对象头 8 字节,共计 20 字节,向 8 字节对齐,故占 24 字节。(jdk7 中)

int hash32 0
int hash 0
ref value C:\Users\Administrat

这 24 字节为 String 对象的浅堆大小。它与 String 的 value 理论取值无关,无论字符串长度如何,浅堆大小始终是 24 字节。

2. 保留集(Retained Set)

对象 A 的保留集指当对象 A 被垃圾回收后,能够被开释的所有的对象汇合(包含对象 A 自身),即对象 A 的保留集能够被认为是只能通过对象 A 被间接或间接拜访到的所有对象的汇合。艰深地说,就是指仅被对象 A 所持有的对象的汇合。

3. 深堆(Retained Heap)

深堆是指对象的保留集中所有的对象的浅堆大小之和。

留神:浅堆指对象自身占用的内存,不包含其外部援用对象的大小。一个对象的深堆指只能通过该对象拜访到的(间接或间接)所有对象的浅堆之和,即对象被回收后,能够开释的实在空间。

4. 对象的理论大小

这里,对象的理论大小定义为一个对象所能涉及的所有对象的浅堆大小之和,也就是通常意义上咱们说的对象大小。与深堆相比,仿佛这个在日常开发中更为直观和被人承受,但实际上,这个概念和垃圾回收无关。

下图显示了一个简略的对象援用关系图,对象 A 援用了 C 和 D,对象 B 援用了 C 和 E。那么对象 A 的浅堆大小只是 A 自身,不含 C 和 D,而 A 的理论大小为 A、C、D 三者之和。而 A 的深堆大小为 A 与 D 之和,因为对象 C 还能够通过对象 B 拜访到,因而不在对象 A 的深堆范畴内。

5. 摆布树(Dominator Tree)

摆布树的概念源自图论。MAT 提供了一个称为摆布树(Dominator Tree)的对象图。摆布树体现了对象实例间的摆布关系。在对象援用图中,所有指向对象 B 的门路都通过对象 A,则认为对象 A 摆布对象 B。如果对象 A 是离对象 B 最近的一个摆布对象,则认为对象 A 为对象 B 的间接支配者。摆布树是基于对象间的援用图所建设的,它有以下根本性质:

  • 对象 A 的子树(所有被对象 A 摆布的对象汇合)示意对象 A 的保留集(retained set),即深堆。
  • 如果对象 A 摆布对象 B,那么对象 A 的间接支配者也摆布对象 B。
  • 摆布树的边与对象援用图的边不间接对应。

如下图所示:左图示意对象援用图,右图示意左图所对应的摆布树。对象 A 和 B 由根对象间接摆布,因为在到对象 C 的门路中,能够通过 A,也能够通过 B,因而对象 C 的间接支配者也是根对象。对象 F 与对象 D 互相援用,因为到对象 F 的所有门路必然通过对象 D,因而,对象 D 是对象 F 的间接支配者。而到对象 D 的所有门路中,必然通过对象 C,即便是从对象 F 到对象 D 的援用,从根节点登程,也是通过对象 C 的,所以,对象 D 的间接支配者为对象 C。同理,对象 E 摆布对象 G。达到对象 H 的能够通过对象 D,也能够通过对象 E,因而对象 D 和 E 都不能摆布对象 H,而通过对象 C 既能够达到 D 也能够达到 E,因而对象 C 为对象 H 的间接支配者。

6. 内存透露(memory leak)

可达性剖析算法来判断对象是否是不再应用的对象,实质都是判断一个对象是否还被援用。那么对于这种状况下,因为代码的实现不同就会呈现很多种内存透露问题(让 JVM 误以为此对象还在援用中,无奈回收,造成内存透露)。

> 是否还被应用?是

> 是否还被须要?否

严格来说,只有对象不会再被程序用到了,然而 GC 又不能回收他们的状况,才叫内存透露。但理论状况很多时候一些不太好的实际(或忽略)会导致对象的生命周期变得很长甚至导致 00M,也能够叫做宽泛意义上的“内存透露”。

如下图,当 Y 生命周期完结的时候,X 仍然援用着 Y,这时候,垃圾回收期是不会回收对象 Y 的;如果对象 X 还援用着生命周期比拟短的 A、B、C,对象 A 又援用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存透露,直到内存溢出。

申请了内存用完了不开释,比方一共有 1024M 的内存,调配了 512M 的内存始终不回收,那么能够用的内存只有 512M 了,好像泄露掉了一部分;艰深一点讲的话,内存透露就是【占着茅坑不拉 shi】

7. 内存溢出(out of memory)

申请内存时,没有足够的内存能够应用;艰深一点儿讲,一个厕所就三个坑,有两个站着茅坑不走的(内存透露),剩下最初一个坑,厕所示意接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存透露变成内存溢出了。可见,内存透露和内存溢出的关系:内存透露的增多,最终会导致内存溢出。

<mark> 透露的分类 </mark>

  • 常常产生:产生内存泄露的代码会被屡次执行,每次执行,泄露一块内存;
  • 偶尔产生:在某些特定状况下才会产生
  • 一次性:产生内存泄露的办法只会执行一次;
  • 隐式透露:始终占着内存不开释,直到执行完结;严格的说这个不算内存透露,因为最终开释掉了,然而如果执行工夫特地长,也可能会导致内存耗尽。

8. Java 中内存泄露的 8 种状况

8.1. 动态汇合类

动态汇合类,如 HashMap、LinkedList 等等。如果这些容器为动态的,那么它们的生命周期与 JVM 程序统一,则容器中的对象在程序完结之前将不能被开释,从而造成内存透露。简略而言,长生命周期的对象持有短生命周期对象的援用,只管短生命周期的对象不再应用,然而因为长生命周期对象持有它的援用而导致不能被回收。

public class MemoryLeak {static List list = new ArrayList();
    public void oomTests(){Object obj=new Object();// 局部变量
        list.add(obj);
    }
}

8.2. 单例模式

单例模式,和动态汇合导致内存泄露的起因相似,因为单例的动态个性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有内部对象的援用,那么这个内部对象也不会被回收,那么就会造成内存透露。

8.3. 外部类持有外部类

外部类持有外部类,如果一个外部类的实例对象的办法返回了一个外部类的实例对象。这个外部类对象被长期援用了,即便那个外部类实例对象不再被应用,但因为外部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存透露。

8.4. 各种连贯,如数据库连贯、网络连接和 IO 连贯等

在对数据库进行操作的过程中,首先须要建设与数据库的连贯,当不再应用时,须要调用 close 办法来开释与数据库的连贯。只有连贯被敞开后,垃圾回收器才会回收对应的对象。否则,如果在拜访数据库的过程中,对 Connection、Statement 或 ResultSet 不显性地敞开,将会造成大量的对象无奈被回收,从而引起内存透露。

public static void main(String[] args) {
    try{
        Connection conn =null;
        Class.forName("com.mysql.jdbc.Driver");
        conn =DriverManager.getConnection("url","","");
        Statement stmt =conn.createStatement();
        ResultSet rs =stmt.executeQuery("....");
    } catch(Exception e){// 异样日志} finally {
        // 1.敞开后果集 Statement
        // 2.敞开申明的对象 ResultSet
        // 3.敞开连贯 Connection
    }
}

8.5. 变量不合理的作用域

变量不合理的作用域。一般而言,一个变量的定义的作用范畴大于其应用范畴,很有可能会造成内存透露。另一方面,如果没有及时地把对象设置为 null,很有可能导致内存透露的产生。

public class UsingRandom {
    private String msg;
    public void receiveMsg(){readFromNet();// 从网络中承受数据保留到 msg 中
        saveDB();// 把 msg 保留到数据库中}
}

如下面这个伪代码,通过 readFromNet 办法把承受的音讯保留在变量 msg 中,而后调用 saveDB 办法把 msg 的内容保留到数据库中,此时 msg 曾经就没用了,因为 msg 的生命周期与对象的生命周期雷同,此时 msg 还不能回收,因而造成了内存透露。实际上这个 msg 变量能够放在 receiveMsg 办法外部,当办法应用完,那么 msg 的生命周期也就完结,此时就能够回收了。还有一种办法,在应用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。

8.6. 扭转哈希值

扭转哈希值,当一个对象被存储进 HashSet 汇合中当前,就不能批改这个对象中的那些参加计算哈希值的字段了。

否则,对象批改后的哈希值与最后存储进 HashSet 汇合中时的哈希值就不同了,在这种状况下,即便在 contains 办法应用该对象的以后援用作为的参数去 HashSet 汇合中检索对象,也将返回找不到对象的后果,这也会导致无奈从 HashSet 汇合中独自删除以后对象,造成内存透露。

这也是 String 为什么被设置成了不可变类型,咱们能够释怀地把 String 存入 HashSet,或者把 String 当做 HashMap 的 key 值;

当咱们想把本人定义的类保留到散列表的时候,须要保障对象的 hashCode 不可变。

/**
 * 例 1
 */
public class ChangeHashCode {public static void main(String[] args) {HashSet set = new HashSet();
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");

        set.add(p1);
        set.add(p2);

        p1.name = "CC";// 导致了内存的透露
        set.remove(p1); // 删除失败

        System.out.println(set);

        set.add(new Person(1001, "CC"));
        System.out.println(set);

        set.add(new Person(1001, "AA"));
        System.out.println(set);

    }
}

class Person {
    int id;
    String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {if (this == o) return true;
        if (!(o instanceof Person)) return false;

        Person person = (Person) o;

        if (id != person.id) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
/**
 * 例 2
 */
public class ChangeHashCode1 {public static void main(String[] args) {HashSet<Point> hs = new HashSet<Point>();
        Point cc = new Point();
        cc.setX(10);//hashCode = 41
        hs.add(cc);

        cc.setX(20);//hashCode = 51  此行为导致了内存的透露

        System.out.println("hs.remove =" + hs.remove(cc));//false
        hs.add(cc);
        System.out.println("hs.size =" + hs.size());//size = 2

        System.out.println(hs);
    }

}

class Point {
    int x;

    public int getX() {return x;}

    public void setX(int x) {this.x = x;}

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        return result;
    }

    @Override
    public boolean equals(Object obj) {if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Point other = (Point) obj;
        if (x != other.x) return false;
        return true;
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                '}';
    }
}

8.7. 缓存泄露

内存透露的另一个常见起源是缓存,一旦你把对象援用放入到缓存中,他就很容易忘记。比方:之前我的项目在一次上线的时候,利用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,然而生产环境有几百万的数据。

对于这个问题,能够应用 WeakHashMap 代表缓存,此种 Map 的特点是,当除了本身有对 key 的援用外,此 key 没有其余援用那么此 map 会主动抛弃此值。

public class MapTest {static Map wMap = new WeakHashMap();
    static Map map = new HashMap();

    public static void main(String[] args) {init();
        testWeakHashMap();
        testHashMap();}

    public static void init() {String ref1 = new String("obejct1");
        String ref2 = new String("obejct2");
        String ref3 = new String("obejct3");
        String ref4 = new String("obejct4");
        wMap.put(ref1, "cacheObject1");
        wMap.put(ref2, "cacheObject2");
        map.put(ref3, "cacheObject3");
        map.put(ref4, "cacheObject4");
        System.out.println("String 援用 ref1,ref2,ref3,ref4 隐没");

    }

    public static void testWeakHashMap() {System.out.println("WeakHashMap GC 之前");
        for (Object o : wMap.entrySet()) {System.out.println(o);
        }
        try {System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.out.println("WeakHashMap GC 之后");
        for (Object o : wMap.entrySet()) {System.out.println(o);
        }
    }

    public static void testHashMap() {System.out.println("HashMap GC 之前");
        for (Object o : map.entrySet()) {System.out.println(o);
        }
        try {System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.out.println("HashMap GC 之后");
        for (Object o : map.entrySet()) {System.out.println(o);
        }
    }

}

下面代码和图示主演演示 WeakHashMap 如何主动开释缓存对象,当 init 函数执行实现后,局部变量字符串援用 weakd1,weakd2,d1,d2 都会隐没,此时只有动态 map 中保留中对字符串对象的援用,能够看到,调用 gc 之后,HashMap 的没有被回收,而 WeakHashMap 外面的缓存被回收了。

8.8. 监听器和其余回调

内存透露第三个常见起源是监听器和其余回调,如果客户端在你实现的 API 中注册回调,却没有显示的勾销,那么就会积累。

须要确保回调立刻被当作垃圾回收的最佳办法是只保留它的弱援用,例如将他们保留成为 WeakHashMap 中的键。

9. 内存泄露案例剖析

public class Stack {private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) { // 入栈
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() { // 出栈
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    private void ensureCapacity() {if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

上述程序并没有显著的谬误,然而这段程序有一个内存透露,随着 GC 流动的减少,或者内存占用的一直减少,程序性能的升高就会体现进去,重大时可导致内存透露,然而这种失败状况绝对较少。

代码的次要问题在 pop 函数,上面通过这张图示展示。假如这个栈始终增长,增长后如下图所示

当进行大量的 pop 操作时,因为援用未进行置空,gc 是不会开释的,如下图所示

从上图中看以看出,如果栈先增长,再膨胀,那么从栈中弹出的对象将不会被当作垃圾回收,即便程序不再应用栈中的这些队象,他们也不会回收,因为栈中依然保留这对象的援用,俗称过期援用,这个内存泄露很荫蔽。

将代码中的 pop() 办法变成如下办法:

public Object pop() {if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

一旦援用过期,清空这些援用,将援用置空。

正文完
 0