Java的一个核心优点是它是一种垃圾收集语言。这意味着我们可以创建对象,垃圾回收器将负责为我们分配和释放内存。
虽然我们有垃圾收集器,但不幸的是,我们可以通过犯一些代码错误来阻止他完成任务。这些错误导致内存泄漏。泄漏会通过浪费未释放的内存来影响我们的android应用程序,最终导致内存不足异常和延迟。
什么是内存泄漏?
无法从内存中释放未使用的对象
这意味着应用程序中存在垃圾回收器无法从内存中释放的未使用对象。因此,存储单元一直被占用直到应用程序/方法结束。
在跳转到上下文之前,让我们先从基础知识开始。
什么是RAM/内存?
随机存取存储器(RAM)是android设备或计算机中用来存储当前运行的应用程序及其数据的存储器。
我将解释RAM中的两个主要字符,第一个是堆,第二个是堆栈。
什么是堆和堆栈?
我不打算让它变长,所以简短地描述一下,栈用于静态内存分配,而堆用于动态内存分配。请记住,堆和栈都存储在RAM中。
那么它在现实世界中是如何工作的呢?
让我们用一个简单的程序来解释堆栈和堆的使用情况。
下面的图像显示了堆栈和堆的外观以及启动程序时它们所指的位置。
让我们一起检查程序的执行,停止每一行,并告诉do是如何工作的。并解释堆栈和堆何时需要进行分配以及何时释放内存。
- 第1行-java运行时创建一个堆栈内存,它将由main方法的线程使用。
- 第2行-这里我们创建一个原始局部变量。它是在main方法的堆栈内存中创建并存储的。
- 第3行-我们正在创建一个新对象。对象在堆栈中创建并存储在堆中。堆栈正在存储对象对堆的引用。我们称之为指针。
- 第4行-与第3行相同。
- 第5行-在堆栈中创建的新块。这将由foo方法线程使用。
- 第6行-这里我们为foo方法的堆栈内存中的“param”对象创建一个新对象。指向堆中原始对象的地址。
- 第7行-我们正在创建一个新对象。在堆栈中创建并指向堆中的字符串池的对象。
- 第8行-在foo方法的最后一行,方法终止。对象将从foo方法的堆栈块中释放。
- 第9行-与第8行相同,在main方法的最后一行,方法终止。主方法的堆栈块变为空闲。
你在问自己,把内存从堆里释放出来怎么样?
当方法终止时会发生什么?
每个方法都有自己的作用域。当函数终止时,对象将自动释放并从堆栈中回收。
在图1中,当foo方法终止时。foo方法的堆栈内存或堆栈块将被自动释放和回收。
在图2中当主方法终止时。主方法的堆栈内存或堆栈块将被自动释放和回收。
结论
现在,我们知道堆栈中的对象是临时的。当方法终止时,它们将被释放并回收。
那堆呢?
堆与堆栈不同。为了释放和回收堆内存中的对象,我们需要帮助。
为此,Java已经成为帮助我们的超级英雄。我们叫它垃圾回收器。他将为我们做艰苦的工作。关心检测未使用的对象,释放它们,并回收内存中的更多空间。
垃圾收集器是如何工作的?
简单。垃圾回收器正在查找未使用或无法访问的对象。那是什么意思?这意味着如果堆中有一个对象没有指向它的任何引用。垃圾回收器将负责从内存中释放它并回收更多空间。
当垃圾收集器工作时会发生什么?
我以图2为例。当垃圾回收器启动并执行艰苦的工作时,结果应该如图3所示。他会从堆里放出来。
现在你是说。好吧,太棒了!但是,如果垃圾回收器正在努力寻找未使用的对象并释放它们,那么内存泄漏是如何发生的呢?让我们来看看。
内存泄漏是如何发生的?
它发生在堆内存中有未使用的对象时。堆栈中的某些对象仍然指向/引用它。
为了更好地理解概念,下图中有一个简单的视觉表示。
在可视化表示中,我们看到的是当我们有从堆栈中引用但不再使用的对象时。垃圾回收器永远不会释放这些对象或释放内存,因为这些对象看起来像是在使用而不是在使用。
我们怎么能造成泄漏?
在Android中,有多种方法可以导致内存泄漏。而且可以使用异步任务、处理程序、单例类、线程等轻松实现。我将解释如何使用线程或singleton导致泄漏,以及如何修复和避免它们。
在Github中查看我的存储库。在这里我有一些代码示例,说明了如何导致内存泄漏,以及如何使用AsyncTask、处理程序等来避免和修复内存泄漏。
内存泄露代码示例:
1. 我们怎么能用线程造成泄漏?
我们将创建一个线程,它在后台运行一个需要20秒的任务。
public class ThreadActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
new DownloadTask().start();
}
private class DownloadTask extends Thread {
@Override
public void run() {
SystemClock.sleep(2000 * 10);
}
}
}
正如我们所知,内部类拥有对其封闭类的隐式引用,它将自动生成一个构造函数,并将活动作为对它的引用传递。
让我们戴上我们的秘密眼镜,以不同的方式看待这个类,看看它到底是什么样子。
public class ThreadActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
new DownloadTask(this).start();
}
private class DownloadTask extends Thread {
Activity activity;
public DownloadTask(Activity activity) {
this.activity = activity;
}
@Override
public void run() {
SystemClock.sleep(2000 * 10);
}
}
}
戴上眼镜后,你可以注意到我们在class里有活动参考。
那么当线程启动时会发生什么呢?
在正常情况下,用户打开“活动等待20秒”,直到下载任务完成。
任务完成后,堆栈将释放所有对象。
然后,下次垃圾回收器工作时,他将从堆中释放对象。
当用户关闭activity时,main
方法将从堆栈中释放,ThreadActivity
也将从堆中回收,一切都按需要工作,不会出现泄漏。
如果用户在10秒后关闭/旋转活动。
任务仍在工作,这意味着活动仍然活跃,我们内存泄漏了!!
注意:当download run()
任务完成后,堆栈将释放对象。因此,当垃圾收集器下次工作时,对象将从堆中回收,因为它没有引用任何对象。
dump文件
在打开和关闭活动5次之后。
更多dump文件的操作参考这篇文章:https://javakk.com/1050.html
2. 我们如何使用singleton导致泄漏
让我们举一个例子,一个单例需要上下文来做一些事情,比如获取资源或使用共享偏好…等等
public class SingletonManager {
private static SingletonManager singleton;
private Context context;
private SingletonManager(Context context) {
this.context = context;
}
public synchronized static SingletonManager getInstance(Context context) {
if (singleton == null) {
singleton = new SingletonManager(context);
}
return singleton;
}
什么时候会发生内存泄漏?
从活动初始化singleton时,将向singleton传递长寿命上下文活动引用。
public class LoginActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//...
SingletonManager.getInstance(this);
}
}
然后,singleton将保持活动直到应用程序结束。
什么是PermGen?
PermGen是永久代的缩写,它是一个特殊的堆空间,它与主Java堆分开。静态方法和静态变量存储在堆的PermGen部分。只有原语和引用存储在PermGen中,值存储在堆的另一部分(年轻/旧一代)。
注意:JDK8之后使用元空间metaspace代替了永久代
如何避免单粒子泄漏?
通过传递应用程序上下文而不是活动上下文来避免此泄漏。
SingletonManager.getInstance(getApplicationContext());
不要保留对上下文活动的长期引用
或者,您可以更新singleton类,以便在创建对象时使用应用程序上下文,而不是从外部传递它。
public class SingletonManager {
// ....
public synchronized static SingletonManager getInstance(Context context) {
if (singleton == null) {
singleton = new SingletonManager(context.getApplicationContext());
}
return singleton;
}
}
现在,您可以确保您将始终使用应用程序上下文,而不管它们将发送哪个上下文。
3. 如何使用侦听器导致泄漏
侦听器或观察者模式是在Android开发中创建回调的最常见策略,在许多情况下,您在活动或片段中为单例对象或x-manager注册侦听器,但忘记注销它。当您将活动作为侦听器传递时,这很容易导致内存泄漏,请确保您了解其他对象对活动实例的操作。
public class LoginActivity extends Activity implements LocationListener {
@Override
public void onLocationUpdated(Location location){
// do something
}
@Override
protected void onStart(){
LocationManager.getInstance().register(this);
}
@Override
protected void onStop(){
LocationManager.getInstance().unregister(this);
}
}
在上面的示例中,我们注册活动以侦听位置更新,如果我们忘记从LocationManager
注销该活动,并且您的活动即将停止,Android框架将对其调用onDestroy()
,但垃圾回收器将无法从内存中删除实例,因为LocationManager
仍然强烈地提到它。
如何避免这种泄漏?
请确保注销onDestroy
/onStop
方法中的所有侦听器。
protected void onDestroy() {
unregisterReceivers();
LocationManager.getInstance().unregister(this);
AppState.getInstance().setStateChangeListener(null);
LocalBroadcastManager.getInstance(getApplicationContext())
.unregisterReceiver(messageReciver);
super.onDestroy();
}
内存泄露检测工具
定位和检查内存泄露的工具很多,可以参考这几篇文章:
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1090.html
暂无评论