内存泄漏与内存溢出总结 【转载自公众号】

内存泄漏与内存溢出总结
程序员小乐 前天
专注于编程、互联网动态。最终将总结的技术、心得、经验(数据结构与算法、源码分析等)分享给大家,这里不只限于技术!还有职场心得、生活感悟、以及面经。点击上方 “程序员小乐” ,选择“置顶公众号”,第一时间送达!

每日英文
If you wait to do everything until you’re sure it’s right, you’ll probably never do much of anything.
如果你等到每件事都确定是对的才去做,那你也许永远都成不了什么事。

乐乐有话说
成长本来就是一个逐渐孤立无援的过程,你要努力强大起来,然后独当一面。

来自:默尛铭

链接:blog.csdn.net/u012792686/article/details/69666498

图片来自网络

1 导读
本篇文章是最近几天关于内存优化的个人学习总结,从基础到日常常见的内存泄漏的顺序慢慢介绍…本编全文本,可能有些单调,不过认真看下来,肯定收益良多!

如果急着解决,直接看 ”常见的内存溢出处理”,”常见的内存泄漏”

java 内存分配策略

Java程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

2 栈与堆的区别
在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。

当在一段方法块中定义一个变量时,Java就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。

在堆中分配的内存,将由Java垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

例子:

public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();

public void method() {
    int s2 = 1;
    Sample mSample2 = new Sample();
}

}
.

Sample mSample3 = new Sample();
Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。

java中的四种引用类型:

强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
如:User u=new User();
User u 存在于栈里面,new User() 存在于堆里面的,栈通过 = 号,将堆对象引用起来,叫强引用(当前这种形式称为显式的强引用(强可及对象))

软引用(SoftReference):只有在内存空间不足时,才会被回的对象;

弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;

虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

软引用和弱引用,这两个引用是可以随时被虚拟机回收的对象,我们将一些比较占内存但是又可能后面用的对象,比如Bitmap对象,可以声明为软引用或弱引用。但是注意一点,每次使用这个对象时候,需要显示判断一下是否为null,以免出错。

3 什么是垃圾回收机制?
JVM的垃圾回收机制中,判断一个对象是否死亡,并不是根据是否还有对象对其有引用,而是通过可达性分析。对象之间的引用可以抽象成树形结构,通过树根(GC Roots)作为起点,从这些树根往下搜索,搜索走过的链称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明这个对象是不可用的,该对象会被判定为可回收的对象。

那么哪些对象可作为GC Roots(GC 会自动回收的对象)呢?主要有以下几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般说的Native方法)引用的对象。
5.Thread —活着的线程

一般出问题时也是不巧当的调用以上对象造成的

4 内存泄漏与内存溢出的区别
内存泄漏 Memory Leak

指程序申请了内存后(new),用完的内存没有释放(delete),一直被某个或某些- 实例所持有却不再被使用导致 GC 不能回收

生活例子 : 电热水器洗完澡不关水,其他人用就没热水的情况

内存泄漏是导致内存溢出的原因之一;内存泄漏累积起来就会造成内存溢出

内存泄漏可以通过完善代码来避免

内存溢出 Out Of Memory

指程序申请内存时,没有足够的内存空间使用

生活例子 : 水杯满了还往里面加水

内存溢出可以通过调整配置来减少发生频率,无法彻底避免

常见内存溢出处理:

绝大部分的内存溢出原因是由于图片太大引起的
一般我们使用软引用/弱引用解决由于图片资源过大的内存溢出
但是API9以后,GC机制改变了,建议使用LruCache缓冲图片资源
注意临时Bitmap对象的及时回收,先recycle(),后致空
尽量避免Try catch某些大杯存分配的操作
加载Bitmap时:缩放比例、解码格式、局部加载(图片大于手机屏幕)

5 内存泄漏的原因
以发生的方式来分类,内存泄漏可以分为4类:

常发性内存泄漏 : 发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

偶发性内存泄漏 : 发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

一次性内存泄漏 : 发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

隐式内存泄漏 : 程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。(重视)

小结:

一次性内存泄漏没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较于常发性和偶发性内存泄漏它更难被检测到

6 个人总结的内存泄漏
单例模式造成的内存泄漏

由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。

案例:

public class AppManager {

private static AppManager instance;
private Context context;

private AppManager(Context context) {
this.context = context;
}

public static AppManager getInstance(Context context) {

if (instance != null) {

instance = new AppManager(context);

}

return instance;

}
}
这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

1、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长 ;

2、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。(static > Activty 生命周期)

所以正确的单例应该修改为下面这种方式:

public class AppManager {

private static AppManager instance;    
 private Context context;      

private AppManager(Context context) {         

this.context = context.getApplicationContext();     
}      

public static AppManager getInstance(Context context) {        
if (instance != null) {              

instance = new AppManager(context);       
}      
return instance;    

} 

}
这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。

第二种方式:这样写,连Context都不用传进来了:

在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,

context = getApplicationContext();
/**
* 获取全局的context
* @return 返回全局context对象
*/
public static Context getContext(){
return context;
}
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}
public static AppManager getInstance() {
if (instance == null) {
instance = new AppManager();
}
return instance;
}
非静态内部类创建静态实例造成的内存泄漏

public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;

@Override      
protected void onCreate(Bundle savedInstanceState) {       
     super.onCreate(savedInstanceState);        
setContentView(R.layout.activity_main);       

if(mManager == null){         
mManager = new TestResource();

 }       

 //...   
 }     

 class TestResource {      
 //...    
 }

}
这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

正确的做法为:

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext 。

Handler造成的内存泄漏

Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写不规范即有可能造成内存泄漏

如下示例:

public class MainActivity extends AppCompatActivity {

private Handler mHandler = new Handler() {

@Override
public void handleMessage(Message msg) {
//…
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}

private void loadData(){
//…request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,

正确做法为:

public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private TextView mTextView

//①将Handler改成静态内部类
private static class MyHandler extends Handler {

//②将需要引用Activity的地方,改成弱引用
private WeakReference reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}

@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
activity.mTextView.setText(“”);
}
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private void loadData() {
//…request
Message message = Message.obtain();
mHandler.sendMessage(message);
}

//③在onDestory()方法中移除handler发送的消息和任务
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}

}
使用:
mHandler.removeCallbacksAndMessages(null);
是移除消息队列中所有消息和所有的Runnable。

当然也可以使用:
mHandler.removeCallbacks();或mHandler.removeMessages();
来移除指定的Runnable和Message。

下面几个方法都可以移除 Message:

public final void removeCallbacks(Runnable r);

public final void removeCallbacks(Runnable r, Object token);

public final void removeCallbacksAndMessages(Object token);

public final void removeMessages(int what);

public final void removeMessages(int what, Object object);
线程造成的内存泄漏

对于线程造成的内存泄漏,也是平时比较常见的,如下这两个示例可能每个人都这样写过:

//——————test1
new AsyncTask

猜你喜欢

转载自blog.csdn.net/tianhongyan1122/article/details/81612827