补充篇:什么是内存泄漏?

659 阅读6分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

什么是内存泄漏(Memory leak)?

定义:内存泄漏不是指物理上的内存消失,而是指一块应用程序分配到某段内存后,由于设计错误,该段内存没有被释放,所以导致这段内存永远也没法再被使用。

思考:操作系统内存泄漏关机能不能解决?

回答:可以。因为操作系统也是软件。关机就是结束了操作系统的进程。所以,操作系统的内存泄漏,泄漏的内存可以重新使用了。但是,制造内存泄漏的原因不会因此解决。必须通过工具监测并查看修改代码解决。

尤其是服务器,服务器通常是不会经常性的进行重启关闭的。所以,一旦服务器程序出现内存泄漏,时间一长,就会在某个时间导致软件出现OOM的情况。会严重影响软件的使用。

后果:在现代操作系统中,一个应用程序的常规内存会在程序终止时被释放。所以一个短暂运行的应用程序中的内存泄漏不会造成严重后果。

严重后果的情况:

  • 长时间运行的程序(服务器的后台任务)
  • 内存有限的嵌入式或者便携设备
  • 泄漏出现在操作系统或者关键驱动中
  • 运行在不会自动释放内存的操作系统上
  • 泄漏出现在共享内存。
  • 新的内存被频繁分配。【这一条可以指向下一个知识点,内存抖动

思考:一个Android APP如果内存泄漏了,当他结束进程之后,内存泄漏的该段内存会不会被系统回收,还是会永远的把这块内存泄漏掉 ?

回答:

内存泄漏只是对于当前正在运行的程序本身的。

如果APP的进程结束,那么该APP的内存泄漏并不会影响到系统,以及其他APP。现代操作系统本身就有内存管理回收的功能。

思考: 一个Android APP能够申请到多少内存?是否有最大限制?是不是我手机有2G内存,我就可以申请2G?

回答:

能够申请的内存有最大限制。

即使你的手机有2G内存,当你的App需要使用的内存超过这个最大限制之后,操作系统也不会在将更多的内存分配给你。而是会出现OOM的情况。APP会在此时Crash。

APP内存由 dalvik内存native内存 2部分组成。

这2部分加起来不能超过android对单个进程,虚拟机的内存限制。

内存的最大限制可以通过下面代码得到,单位是MB

ActivityManager activityManager =(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
activityManager.getMemoryClass();

注:查询结果就是 dalvik.vm.heapgrowthlimit 的值。

对于head堆的大小限制,可以查看/system/build.prop文件。【可以连接Android Studio,打开Device File Explorer,就可以看到】

dalvik.vm.heapstartsize=16m
dalvik.vm.heapgrowthlimit=128m
dalvik.vm.heapsize=192m

注:

dalvik.vm.heapstartsize=8m 表示堆分配的初始大小

dalvik.vm.heapsize 表示单个进程heap可用的最大内存。

但如果存在以下参数"dalvik.vm.headgrowthlimit = 128m"表示单个进程heap内存被限定在128m,即程序运行过程实际只能使用128m内存。

这三个值的形象比喻就是吃饭。

每个人(App进程)一开始都盛一碗饭(heapstartsize)。普通人吃三碗(headgrowthlimit )。猛人壮汉吃五碗(heapsize)。

内存泄漏典型例子

电梯案例 - 来自维基百科

下面我用 Java 代码进行说明

1- 设置目标楼层 , 新建对象存储目标楼层值起始楼层值

Scanner sc = new Scanner(System.in); 

int target = sc.nextInt();//目标楼层
int startFloor = sc.nextInt();//起始楼层

2 - 每上一层,判断一次是否到达目标楼层

int floorNum = X; //假设X是层数,常量
boolean isUp = target>startFloor;//当前方向是向上
boolean isDown = startFloor>target;
if(isUp){
   for(int i= startFloor,i<X,i++){
       //如果达到目标楼层
       if(i==target){
           System.out.println("到达目标楼层");   
           target = null;
           startFloor = null;
           sc = null;
        }
   } 
}

if(isDown){
   for(int i= startFloor,i>target,i--){
       //如果达到目标楼层
       if(i==target){
           System.out.println("到达目标楼层");   
           target = null;
           startFloor = null;
           sc = null;
       }
   }
}

上面的代码是电梯案例中可能出现问题的一种写法。就如案例所说,上面的代码忽略了target == startFloor 的可能性。

如果是target == startFloor 的这种情况,我们就忽视了这个输入。没有执行任何操作。

在这种情况下,target和startFloor 没有进行内存释放,就造成了内存泄漏。

如果上述代码是C语言来写的话,的确是这样的。

维基百科的电梯实例也是告诉了我们。如果我们使用了内存,但是没有释放。这块内存不能再被使用了,就是出现了内存泄漏。

思考:为什么上面这个例子,同样的程序逻辑,用C来写就是内存泄漏。但是用Java写就不会呢?

回答:

基于两点。

一、Java的垃圾回收机制。所有不可达的堆内存都会被GC给回收。

二、局部变量的内存回收,主要是栈内存回收,会通过线程独享的栈和栈帧进行管理。

上述的代码会在电梯按钮的点击事件之后调用。所以,上面的过程会在一个方法中被调用。这三个变量都是局部变量。

在Java中,局部变量是处于栈内存的栈帧中的。局部变量分为基本数据类型和引用数据类型两种。

引用数据类型的变量引用是存储在所在线程的栈内存的栈帧中的。每结束一个方法,就执行完一个栈帧。

此时,基本数据类型的target和startFloor 执行完栈帧之后,对应栈内存就释放了。

引用数据类型的sc 引用也在栈帧之中。执行完栈帧之后,对应栈内存就释放了。也就是sc这个引用被释放。

但是sc 对应的堆内存还在,但是堆内存没有引用指向,那块内存不可达。被Java的垃圾回收机制回收。

因此,我们在写程序的时候,不光要考虑程序设计的完成,还要思考对内存资源的使用。避免出现内存泄漏的情况。

对于不同的编程语言,即使是同样的程序思路,也不一定会出现内存泄漏的现象。所以,我们还应该适当了解不同语言的执行过程。