Java内存泄漏


本文将会介绍:

  • C++中的内存泄露
  • Java内存管理与垃圾回收
  • Java中的内存泄漏

一、C++中的内存泄露

在大一上C++面向对象课的时候老师告诉我们,new 和 delete 一定要配对使用!不然就会造成内存泄漏。在当时听的迷迷糊糊的,只是记住了。但到底什么是内存泄漏?为什么 new 出来的内存不 delete 就会造成内存泄漏呢?这就要从C++的运行时内存区域划分来讲起了

  1. C++运行时内存区域划分
    咱们先来看这样一段代码——
#include <iostream>
 using namespace std;

int var1 = 1;
int* arr = new int[2];

void memLeak();

int main()
{
   while(1)
       memLeak();
   return 0;
}

void memLeak(){
   int a = 0;
   int* b = new int[2];
   // do something with b
}

我们声明的这些变量分别存放在不同的内存区域,如下图——

注:stack和静态/全局区的格子中展示的是存放的值,而Heap中展示的是地址

我们先看看这几个分区的介绍:

栈区(stack):存放局部变量和参数,申请和释放都由编译器自动完成
堆区(heap):动态内存分配,申请和释放都是由程序员控制。
静态区/全局区(static):存放全局变量和静态变量。

再结合我们的代码来看,我们在memLeak函数中声明的局部变量 a 和 c 都存放在栈区,而全局变量var1则存放在静态全局区。存放在栈中的变量我们无需进行管理,随着函数的返回,分配的内存自动进行了回收;而存放在静态/全局区中的变量的生命周期是跟随整个程序的,我们也无需在代码中进行内存的管理。

那new出来的内存呢?从图中我们可以看出,我们在函数中执行 int* b = new int[2]; 并非是在栈中开辟了两个int大小的空间,而是在堆中分配了空间,并将这片空间的头地址赋值给b,与此同时,插一面小旗子,占山为王,说这一块地已经是我的了,只有我能在上面撒欢,别人不许过来!但是在函数结束之后,这一块内存并不会自动释放(拔掉小旗子,这一块地方可以被别人使用), 如果不手动调用 delete 去释放内存,其生命周期与会与程序一样长。

就我个人总结来说的话,内存泄漏就是在堆上已经无用的内存并未被回收而无法挪作他用的情况。

  1. C++中的内存泄漏

之前的代码就是一个内存泄漏的例子,在memLeak函数外,我们没有再用到 b 申请的内存,在memLeak函数中,我们并没有释放动态申请的内存。我们在外面死循环调用函数,会导致在堆中越来越多的人“占地为王”,最终导致堆被占满,其他地方无法再正常的申请到堆中内存。

#include <iostream>
using namespace std;

int main(){
    int b = new int[1024]; // #1 this memory was not recycled,4 KB memory was wasted
    //..
    b = new int[10000];
    //do some things
    delete b;
    //do other things
    return 0;
}

同时还在上述情况,一开始b 指向了一片分配的内存(注释1),而 b 又指向了其他内存,此时再也没有办法可以访问到注释1处分配的内存,导致内存泄漏。

二、Java内存管理和垃圾回收

  1. JVM 运行时内存模型
  • Java 虚拟机栈
  • Java 堆
  • 方法区

相信大家此时对栈和堆已经有了一定的认识,其实方法区也和 C++ 中的静态/全局区有一些类似,是用于存储虚拟机加载的类信息、常量、静态变量、及时编译后的代码等数据的地方。

同 C++ 一样,Java 中 new 关键字也是在堆上分配内存,将首地址赋给(*句柄,句柄的地址再赋给)局部变量。而与 C++ 不同的是,JVM 提供了 GC(Garbage Collection) 机制来管理 Java 堆中的内存。

(* 句柄...): 分别对应句柄使用访问和使用直接指针访问的两种对象访问模式,在 Sun 公司 HotSpot 虚拟机中是采用直接指针的访问模式(即直接定位对象)。

  1. GC

在讨论 GC 之前,我们先来想一个问题,为什么堆区需要专门的垃圾回收机制?

一个接口中的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才内知道创建那些对象,这部分的内存分配是动态的。
——《深入理解Java虚拟机》

我个人理解,首先这一片内存是动态分配的,我们无法从编译期得知需要分配多少内存,其次因为程序不知道这一块内存什么时候会没用,所以需要一个算法,来告诉虚拟机,这一片内存已经不会再被使用了,即对象已死亡。

  • 可达性分析算法

在Java GC的主流实现中,都是通过可达性分析 (Reachability analysis) 来实现判定对象是否存活的。这个算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链 (Reference Chain),当一个对象到 GC Roots 没有任何引用链的时,则证明此对象是不可用的。
在Java语言中,可作为 GC Roots 的对象包括以下几种
》 虚拟机栈中引用的对象
》 方法区中类静态属性引用的对象
》 方法区中常量引用的对象
》 本地方法栈中 JNI 引用的对象
——《深入理解Java虚拟机》 周志明

举个例子~

class Test{

    int id;
    Test(int id){
        this.id = id;
    }

    public static void main(String []args){
        Test test = new Test(0);
        System.gc();
        System.out.println("--------------");
        test = null;
        System.gc();
    }

    @Override
    public void finalize(){
        System.out.println("Test" + id);
    }

}

运行结果:

--------------
Test0

一开始,在栈中存在一个GC Root test, 它指向了在堆中的Test对象,这个对象在存在一条到GC Roots的引用链,此时调用 System.gc() 不会回收该对象,而后我们执行了 test = null,断开了对象与 GC Root 的引用链,表明此对象已经死亡,再次调用 System.gc() 对象被回收(执行了finalize方法):

三、Java中的内存泄露

在第二部分我们介绍了Java的 GC 机制,由虚拟机通过一定的算法在帮我们在 gc 的时候去释放一些堆中无用的内存。但这并不能完全避免 Java 的内存泄漏,当引用链的生命周期长于对象的生命周期时,还是会出现内存泄漏的情况。
在网上搜集了一些内存泄漏的例子——

  1. 静态集合类引起内存泄漏
static Vector v = new Vector(10);
void fun(){
  for (int i = 1; i<100; i++)
  {
      Object o = new Object();
      v.add(o);
      o = null;
  }
}

在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。

  1. 当集合里面的对象属性被修改后,再调用remove()方法时不起作用
public static void main(String[] args)
{
    Set<Person> set = new HashSet<Person>();
    Person p1 = new Person("唐僧","pwd1",25);
    Person p2 = new Person("孙悟空","pwd2",26);
    Person p3 = new Person("猪八戒","pwd3",27);
    set.add(p1);
    set.add(p2);
    set.add(p3);
    System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
    p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变

    set.remove(p3); //此时remove不掉,造成内存泄漏

    set.add(p3); //重新添加,居然添加成功
    System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
    for (Person person : set)
    {
         System.out.println(person);
    }
}
  1. 单例模式
class A{
    public A(){
            B.getInstance().setA(this);
    }
//...
}
//B类采用单例模式
class B{
    private A a;
    private static B instance=new B();
    public B(){}
    public static B getInstance(){
        return instance;
    }
    public void setA(A a){
        this.a=a;
    }
    //getter...  
} 

显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发生什么情况

  1. 非静态内部类
public class MainActivity extends Activity {
    ...
    Handler handler = new MyHandler();
    Runnable ref1 = new MyRunable();
    Runnable ref2 = new Runnable() {
        @Override
        public void run() {
              //
        }
    };
       ...
    public void dosomething(){
        handler.post(ref1); 
    }
}

ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:
image

可以看到ref2这个匿名类的实现对象里面多了一个引用:
this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。同理,Handler也是这样。


引用:
  1. C/C++程序编译时和运行时内存区域分配
  2. 《深入理解Java虚拟机》 —— 周志明
  3. Android内存泄漏总结
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容