这是我参与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的垃圾回收机制回收。
因此,我们在写程序的时候,不光要考虑程序设计的完成,还要思考对内存资源的使用。避免出现内存泄漏的情况。
对于不同的编程语言,即使是同样的程序思路,也不一定会出现内存泄漏的现象。所以,我们还应该适当了解不同语言的执行过程。