浅堆(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字节。

保留集(Retained Set)

对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。

深堆(Retained Heap)

深堆是指对象的保留集中所有的对象的浅堆大小之和。
注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。

对象的实际大小

这里,对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念和垃圾回收无关。
下图显示了一个简单的对象引用关系图,对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A与D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。
浅堆深堆与内存泄露 - 图1

支配树(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的直接支配者。
浅堆深堆与内存泄露 - 图2

内存泄漏(memory leak)

只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏

  1. 实际上有些情况会导致对象的生命周期变得很长甚至OOM,也可以叫做宽泛意义上的“内存泄漏”。
  2. 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutofMemory异常,导致程序崩溃。
  3. 注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小

浅堆深堆与内存泄露 - 图3
左边的图:Java使用可达性分析算法,最上面的数据不可达,就是需要被回收的对象。
右边的图:后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开(图示中的Forgotten Reference Memory Leak),从而导致没有办法被回收,这就是内存泄露。

内存溢出(out of memory)

内存不够用了,垃圾回收也没办法,就叫内存溢出。
大概有如下几种内存溢出的可能:

  • JVM的堆内存不够
  • 代码中创建了大量的对象并且长时间不会被回收

    • 老版本的JDK,对永久代垃圾回收不积极,容易报java.lang.OutOfMemoryError:PermGen space
    • JDK8及其之后叫元空间,在本地内存上,直接内存不足,也会报OOM,java.lang.OutofMemoryError:Metaspace

      Java中内存泄露的8种情况

      1. 静态集合类

      静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

      1. public class MemoryLeak {
      2. static List list = new ArrayList();
      3. public void oomTests(){
      4. Object objnew Object();//局部变量
      5. list.add(obj);
      6. }
      7. }

      2. 单例模式

      单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

      3. 内部类持有外部类

      内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

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

      在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

      1. public static void main(String[] args) {
      2. try{
      3. Connection conn =null;
      4. Class.forName("com.mysql.jdbc.Driver");
      5. conn =DriverManager.getConnection("url","","");
      6. Statement stmt =conn.createStatement();
      7. ResultSet rs =stmt.executeQuery("....");
      8. } catchException e){//异常日志
      9. } finally {
      10. // 1.关闭结果集 Statement
      11. // 2.关闭声明的对象 ResultSet
      12. // 3.关闭连接 Connection
      13. }
      14. }

      5. 变量不合理的作用域

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

      1. public class UsingRandom {
      2. private String msg;
      3. public void receiveMsg(){
      4. readFromNet();//从网络中接受数据保存到msg中
      5. saveDB();//把msg保存到数据库中
      6. }
      7. }

      如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。

      6. 改变哈希值

      改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。
      否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
      这也是 String 为什么被设置成了不可变类型,我们可以放心地把 String 存入 HashSet,或者把String 当做 HashMap 的 key 值;
      当我们想把自己定义的类保存到散列表的时候,需要保证对象的 hashCode 不可变。 ```java /**

      • 例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;

  1. public Person(int id, String name) {
  2. this.id = id;
  3. this.name = name;
  4. }
  5. @Override
  6. public boolean equals(Object o) {
  7. if (this == o) return true;
  8. if (!(o instanceof Person)) return false;
  9. Person person = (Person) o;
  10. if (id != person.id) return false;
  11. return name != null ? name.equals(person.name) : person.name == null;
  12. }
  13. @Override
  14. public int hashCode() {
  15. int result = id;
  16. result = 31 * result + (name != null ? name.hashCode() : 0);
  17. return result;
  18. }
  19. @Override
  20. public String toString() {
  21. return "Person{" +
  22. "id=" + id +
  23. ", name='" + name + '\'' +
  24. '}';
  25. }

}

  1. <a name="HPhWP"></a>
  2. ## 7. 缓存泄露
  3. 内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。<br />对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
  4. ```java
  5. public class MapTest {
  6. static Map wMap = new WeakHashMap();
  7. static Map map = new HashMap();
  8. public static void main(String[] args) {
  9. init();
  10. testWeakHashMap();
  11. testHashMap();
  12. }
  13. public static void init() {
  14. String ref1 = new String("obejct1");
  15. String ref2 = new String("obejct2");
  16. String ref3 = new String("obejct3");
  17. String ref4 = new String("obejct4");
  18. wMap.put(ref1, "cacheObject1");
  19. wMap.put(ref2, "cacheObject2");
  20. map.put(ref3, "cacheObject3");
  21. map.put(ref4, "cacheObject4");
  22. System.out.println("String引用ref1,ref2,ref3,ref4 消失");
  23. }
  24. public static void testWeakHashMap() {
  25. System.out.println("WeakHashMap GC之前");
  26. for (Object o : wMap.entrySet()) {
  27. System.out.println(o);
  28. }
  29. try {
  30. System.gc();
  31. TimeUnit.SECONDS.sleep(5);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. System.out.println("WeakHashMap GC之后");
  36. for (Object o : wMap.entrySet()) {
  37. System.out.println(o);
  38. }
  39. }
  40. public static void testHashMap() {
  41. System.out.println("HashMap GC之前");
  42. for (Object o : map.entrySet()) {
  43. System.out.println(o);
  44. }
  45. try {
  46. System.gc();
  47. TimeUnit.SECONDS.sleep(5);
  48. } catch (InterruptedException e) {
  49. e.printStackTrace();
  50. }
  51. System.out.println("HashMap GC之后");
  52. for (Object o : map.entrySet()) {
  53. System.out.println(o);
  54. }
  55. }
  56. }

上面代码和图示主演演示WeakHashMap如何自动释放缓存对象,当init函数执行完成后,局部变量字符串引用weakd1,weakd2,d1,d2都会消失,此时只有静态map中保存中对字符串对象的引用,可以看到,调用gc之后,HashMap的没有被回收,而WeakHashMap里面的缓存被回收了。

8. 监听器和其他回调

内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。
需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。