https://gitee.com/vectorx/NOT…
[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程序统一,则容器中的对象在程序完结之前将不能被开释,从而造成内存透露。简略而言,长生命周期的对象持有短生命周期对象的援用,只管短生命周期的对象不再应用,然而因为长生命周期对象持有它的援用而导致不能被回收。
<code class="java">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不显性地敞开,将会造成大量的对象无奈被回收,从而引起内存透露。
<code class="java">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,很有可能导致内存透露的产生。
<code class="java">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 不可变。
<code class="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; 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 + '\'' + '}'; } }
<code class="java">/** * 例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会主动抛弃此值。
<code class="java">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); <b style="color:transparent">来源gao@dai!ma.com搞$代^码网</b> } 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. 内存泄露案例剖析
<code class="java">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()办法变成如下办法:
<code class="java">public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; return result; }
一旦援用过期,清空这些援用,将援用置空。