java 内存泄露的原因及解决方法?(对象的根)

内存泄漏介绍

       

       Java语言的一个关键的优势就是它的内存管理机制。你只管创建对象,Java的垃圾回收器帮你分配以及回收内存。然而,实际的情况并没有那么简单,因为内存泄漏在Java应用程序中还是时有发生的。

      下面就解释下什么是内存泄漏,它为什么会发生,以及我们如何阻止它的发生。

1. 什么是内存泄漏?

      内存泄漏的定义:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。

      要想理解这个定义,我们需要先了解一下对象在内存中的状态。下面的这张图就解释了什么是无用对象以及什么是未被引用对象

      上面图中可以看出,里面有被引用对象和未被引用对象。未被引用对象会被垃圾回收器回收,而被引用的对象却不会。未被引用的对象当然是不再被使用的对象,因为没有对象再引用它。然而无用对象却不全是未被引用对象。其中还有被引用的。就是这种情况导致了内存泄漏。

2. 为什么会发生内存泄漏?

      来先看看下面的例子,为什么会发生内存泄漏。下面这个例子中,A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从而导致内存问题,因为如果A引用更多这样的对象,那将有更多的未被引用对象存在,并消耗内存空间。

      B对象也可能会持有许多其他的对象,那这些对象同样也不会被垃圾回收器回收。所有这些没在使用的对象将持续的消耗之前分配的内存空间。

3. 如何防止内存泄漏的发生?

      下面是几条容易上手的建议,来帮助你防止内存泄漏的发生。

  • 特别注意一些像HashMap、ArrayList的集合对象,它们经常会引发内存泄漏。当它们被声明为static时,它们的生命周期就会和应用程序一样长。
  • 特别注意事件监听和回调函数。当一个监听器在使用的时候被注册,但不再使用之后却未被反注册。
  • “如果一个类自己管理内存,那开发人员就得小心内存泄漏问题了。” 通常一些成员变量引用其他对象,初始化的时候需要置空。


内存泄漏详解

        

          内存泄露是指程序中间动态分配了内存,但是在程序结束时没有释放这部分内存,从而造成那一部分内存不可用的情况。分配了内存而没有释放,逐渐耗尽内存资源,导致系统崩溃。

         内存回收包括中如果是栈的话,在函数调用结束之后就回收了.但是如果堆内存的话,即使是对象已经不可达,但是如果内存充足,它也可能不回收,等到内存吃紧的时候再回收.

          造成内存泄露的根本原因是:多重引用。程序员认为,对象在其生命周期结束的时候就是没有用的,即为null时候。但是,gc认为只有对象的计数为0的时候才可以进行垃圾回收。

如下表格所示:

程序员的观点

gc的观点

Object e = new Object();                

class A  a = e;                       

class B  b = e;                      

class C  c = e;                      

class D  d = e; 

e.count++ 

e.count++

e.count++

e.count++

e.count++

e = null;  

e无用了,释放。 

e.count--

e.count为4,不能释放,造成内存泄露。

      多重引用如图所示:


      由此得出结论:要释放对象,必须使对象的引用计数为0。


      总之,在java语言中,判断一块空间是否符合垃圾回收器回收标准的标准只有以下两个:

(1)给对象赋予了空值null,以后再没有调用过。

(2)给对象了新值,即重新分配了内存空间。

      概括地讲:内存泄漏地主要原因是,保留下来却永远不再使用地对象的引用。注意,当局部变量不再使用时,没有必要将其显式地设置为null,对这些变量地引用将随着方法的退出而自动清除。


什么是活着的对象?

从根引用开始找对象,对象内部的属性也可能是引用,只要能级联到的都被认为是活着的对象(包括C Heap区域的对象空间)。

什么是根?

“本地变量引用”、“操作数栈引用”、“PC寄存器”、“本地方法栈引用”、“静态引用”等这些就是根。(所谓的根其实不止一个,把它叫做入口更合适吧),PC寄存器中可能包含了从栈顶抛出的引用,这些引用正在被使用,只是它们并不是本地变量(就像字符串拼接最终输出一样,中间结果没有本地变量,可能在栈顶抛出,但是在生命周期内不会被GC)。本地方法在一些内部调用中,在生命周期内依然是生效的(例如new Thread().start()这个操作,虽然Thread本身没有Java引用指向它,但是只要线程没有结束,这个对象就会一直存在。)这几部分是线程私有的,所以都把它们归结到运行时栈中。静态引用本身是具有全局生命周期的,它如果不是final,则其引用的对象可以发生改变,但是静态属性本身是全局的生命周期,它当前所引用的对象及间接引用的对象也都被认为是活着的对象,因此它也是入口之一,如图所示为活着的对象示意图。



关于shallow size、retained size

 

Shallow size就是对象本身占用内存的大小,不包含对其他对象的引用,也就是对象头加成员变量(不是成员变量的值)的总和。在32位系统上,对象头占用8字节,int占用4字节,不管成员变量(对象或数组)是否引用了其他对象(实例)或者赋值为null它始终占用4字节。故此,对于String对象实例来说,它有三个int成员(3*4=12字节)、一个char[]成员(1*4=4字节)以及一个对象头(8字节),总共3*4 +1*4+8=24字节。根据这一原则,对String a=”rosen jiang”来说,实例a的shallow size也是24字节(很多人对此有争议,请看官甄别并留言给我)。

 

Retained size是该对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。换句话说,retained size是该对象被GC之后所能回收到内存的总和。为了更好的理解retained size,不妨看个例子。

 

把内存中的对象看成下图中的节点,并且对象和对象之间互相引用。这里有一个特殊的节点GC Roots,正解!这就是reference chain的起点。

从obj1入手,上图中蓝色节点代表仅仅只有通过obj1才能直接或间接访问的对象。因为可以通过GC Roots访问,所以左图的obj3不是蓝色节点;而在右图却是蓝色,因为它已经被包含在retained集合内。

所以对于左图,obj1的retained size是obj1、obj2、obj4的shallow size总和;右图的retained size是obj1、obj2、obj3、obj4的shallow size总和。obj2的retained size可以通过相同的方式计算。


参考:http://blog.sina.com.cn/s/blog_701c951f0100n1sp.html


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值