Android-内存泄漏问题多多,怎么优化?

在JVM规范中,如果堆可以动态扩展,但是扩展后仍然无法申请到足够的内存,就会抛出OutOfMemoryError异常。当然,我们可以通过-Xmx-Xms来控制堆内存的大小,其中,-Xmx用于设置Java堆起始的大小,-Xms用于设置Java堆可扩展到最大值。

  • 方法区

像Java堆一样,方法区(Method Area)是各个线程共享的内存区域,它的生命周期与虚拟机相同,即随着虚拟机的启动和结束而创建、销毁。方法主要用于存放已被虚拟机加载的类信息、常量、静态变量以及即时编译器(JIT)编译后的代码等数据,它的大小决定了系统能够加载多少个类,如果定义的类太多,导致方法区抛出OutOfMemoryError异常。需要注意的是,对于JDK1.7来说,在HotSpot虚拟机中方法区可被理解为"永久区",但是JDK1.8以后,方法区已被取消,替代的是元数据区。元数据区是一块堆外的直接内存,与永久区不同,如果不指定大小,默认情况下在类加载时虚拟机会尽可能加载更多的类,直至系统内存被消耗殆尽。当然,我们可以使用参数-XX:MaxMetaspaceSzie来指定元数据区的大小。

运行时常量池用于存放编译期生成的各种字面量和符号引用。

  • 直接内存

直接内存(Direct Memory)不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它是在JDK1.4中新加入的NIO(New Input/Output)类,通过引入了一种基于通道与缓冲区的I/O方式,使用Native函数库直接分配得到的堆外内存。对于这部分内存区域,主要通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,直接内存的存在避免了在Java堆和Native堆中来回复制数据,从而在某些场景能够显著地提高性能。

本机直接内存的分配不会受到Java堆大小的限制,但是仍然会受到本机总内存(包括RAM以及SWAP或者分页文件)大小以及处理器寻址空间的限制。如果申请分配的内存总和(包括直接内存)超过了物理内存的限制,就会导致动态扩展时出现OutOfMemoryError异常。

1.2 垃圾回收器与内存分配策略

在上一节中我们详细分析了JVM的运行时内存区域,了解到程序计数器、虚拟机栈、本地方法栈是线程的私有区域,当线程结束时这部分所占内存资源将会自动释放,而线程的共享区域Java堆是存放所有对象实体的地方,因此是垃圾回收器回收(GC,Garbage Collection)的主要区域。(方法区也会有GC,但是一般我们讨论的是Java堆)

1.2.1 如何判断对象"已死"?

垃圾收集器在回收一个对象,第一件事就是确定这些对象之中有哪些是“存活”的,哪些已经“死亡”,而垃圾回收器回收的就是那些已经“死亡”的对象。如何确定对象是否已经死亡呢?通常,我们可能会说当一个对象没被任何地方引用时,就认为该对象已死。但是,这种表述貌似不够准确,因此,JVM规范中给出了两种判断对象是否死亡的方法,即引用计数法可达性分析

  • 引用计数法

引用计数法实现比较简单,它的实现原理:给对象一个引用计数器,每当一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就会被认为已经死亡。客观地说,引用计数器法效率确实比较高,也容易实现,但是它也有不足之处,就是无用对象之间相互引用的问题,这种情况的出现会导致相互引用的对象均无法被垃圾回收器回收。

  • 可达性分析

为了解决引用计数法的无用对象循环引用导致无法被回收情况,JVM中又引入了可达性分析算法来判断对象是否存活,这种算法也是普遍被应用的方式。可达性分析基本思想:通过一系列被称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连接时,则证明此对象不可用,即被判断可回收对象。可达性分析算法示意图如下图所示:

那么,哪些对象可以作为"GC Roots"呢?

  • 虚拟机栈中(局部变量表)引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中Native方法引用的对象;
1.2.2 垃圾收集算法

前面我们通过引用计数法可达性分析找到了哪些对象是可以被回收的,本节将重点阐述JVM中的垃圾回收器是如何将这些不可用对象进行回收,即垃圾收集算法,主要包括标记-清除算法复制算法标记-整理以及分代收集等。相关介绍如下:

  • 标记-清除算法

标记-清理算法是最基础的垃圾收集算法,它的实现分为两个阶段,即“标记”“清除”,其中,标记的作用为通过引用计数法或可达性分析算法标记出所有需要回收的对象;清除的作用为在标记完成后统一回收所有被标记的对象。这种算法比较简单,但是缺陷也比较明显,主要表现为两个方面:一是标记和清理的效率比较低;二是标记清理之后会产生大量不连续的内存碎片,空间碎片太大可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次GC。标记-清除算法执行过程如下图所示:

  • 复制算法

为了解决标记-清理算法效率不高问题,人们提出了一种复制算法,它的基本原理:将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上,然后再把已使用的内存空间一次性清理掉。这种方式实现简单,运行高效,且缓解了内存碎片的问题,但是由于其只对整个半区进行内存分配、回收,从而导致可使用的内存缩小为整个内存的一半。复制算法执行过程如下图所示:

在HotSpot虚拟机中,整个内存空间被分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间。当回收时,将EdenSurvivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认EdenSurivor的大小比例为8:1:1,也就是每次新生代中可用内存空间为整个内存空间的90%,这就意味着有剩余的10%不可用。当Survivor空间不够用时,就需要依赖其他内存(老年代)进行分担担保。

:新生代是指刚创建不久的对象;老年代指被多次GC仍然存活的对象。

  • 标记-整理算法

虽说复制算法有效地提高了标记-清除算法效率不高问题,但是在对象存活率较高的情况下,就需要进行较多的复制操作(复制对象),尤其是所有对象都100%存活的极端情况,这种复r制算法效率将会大大降低,因此,老年代区域通常不会直接选用这种算法。根据老年代的特点,有人提出了标记-整理算法,该算法基于标记-清除算法发展而来,其中,标记同标记-清除算法一致,整理为将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记-整理算法执行过程如下图所示:

  • 分代收集算法

分代收集算法是目前大部分虚拟机的垃圾收集器采用的算法,这种算法的思想是根据对象的存活周期的不同将Java堆内存划分为几块,即新生代区域和老年代区域,然后对不同的区域采用合适的算法。由于新生代每次GC时都会有大批对象死去,只有少量的对象存活,因此通常选用复制算法;而老年代中因为存活率高、没有额外空间对它进行分配担保,就必须使用"标记-清理“或”标记-整理"算法进行回收。分代收集算法模型如下图所示:

哪些对象能够进入老年代?

  • 大对象;
  • 每次Eden进行GC后对象年龄加1进入Survivor,对象年龄达到15时进入老年代;
  • 如果Survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于等于该年龄的对象就直接进入老年代。
  • 如果survivor空间不能容纳Eden中存活的对象,由于担保机制会进入老年代。如果survivor中的对象存活很多,担保失败,那么会进行一次Full GC。

什么是Minor GC、Major GC和Full GC?

  • Minor GC从新生代空间(Eden和Survivor区域)回收内存;
  • Major GC是清理永久代;
  • Full GC是清理整个堆内存空间,包括新生代和永久代。
1.2.3 内存分配与回收策略

Java的自动内存管理归结于两方面,即为对象分配内存回收分配给对象的内存,其中,在上一小节中我们详细阐述了回收内存的具体细节,这里不再讨论。对于对象的内存分配,实际上就是在Java堆中为对象分配内存,准确来说是在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,并且,少数情况下也可能会直接分配在老年代中。总之,JVM中对对象内存的分配不是固定的模式,其细节取决于使用哪种垃圾收集器,和虚拟机中与内存相关的参数设置。常见的内存分配策略:

  • 对象优先在Eden分配

大多数情况下,对象在Java堆的新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

  • 大对象直接进入老年代

所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。对于内存分配来说,大对象也是一个很棘手的东西,尤其是“短命大对象”,经常出现在内存空间还较多的情况下,大对象直接导致提前出发垃圾收集器以获取足够的连续空间来“安置”它们。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,使得大于这个设置值得对象直接在老年代内存区域分配,这样做的目的在于避免在Eden区及两个Survivor区之间发生大量的内存复制。

  • 长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。虚拟机提供了一个-XX:MaxTenuringThreshold参数设置老年代年龄阈值。

虚拟机并不是永远地要求对象的年龄必须达到了-XX:MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

  • 空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的;如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那么这时就需要进行一次Full GC。

1.3 JVM的类加载机制

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading)7个阶段,其中,验证、准备、解析3个部分统称为连接(Linking)。类的生命周期如下图所示:

在虚拟机中,我们常说的类的加载过程是指加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)这五个阶段,它们的具体作用为:

  • 加载

加载过程是将二进制字节流(Class字节码文件)通过类加载器加载到内存并实例化Class对象的过程(加载到方法区内)。这个过程独立于虚拟机之外,并且二进制流可以从不同的环境内获取或者由其他文件生成。

  • 验证

验证Class文件的字节流是否符合虚拟机的要求,以免造成虚拟机出现异常。包括:文件格式验证元数据验证字节码验证符号引用验证

  • 准备

为静态变量(被final关键字修饰)分配内存空间、赋值和设置类变量初始化(自动初始化)。

  • 解析

将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄以及调用点限定符7类符号引用进行。

  • 初始化

执行类构造器<clinit>方法的过程,变量的声明初始化就在这个阶段进行。

虚拟机类加载的时机?1)遇到new、getstatic、putstatic或者invokestatic 这四条字节码指令的时候,且该类没有进行初始化则进行该类的初始化;
2)使用反射机制的时候;
3)初始化类的父类;
4)初始化虚拟机要执行主类;
5)使用动态语言特性的时候;总之,当对一个类进行主动引用的时候就会进行初始化操作,而进行被动引用的时候便不会触发类初始化操作,比如通过子类引用父类静态字段时子类不会被初始化外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

资料获取→专栏

常见内存泄漏与优化
2.1 内存泄漏

当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被垃圾收集器回收,结果它们就一直存在于内存中(通常指Java堆内存),占用有效空间,永远无法被删除。随着内存不断泄漏,堆中的可用空间就不断变小,这意味着为了执行常用的程序,垃圾清理需要启动的次数越来越多,非常严重的话会直接造成应用程序报OOM异常。优化/避免内存泄漏原则:

  • 涉及到使用Context时,尽量使用Application的Context;
  • 对于非静态内部类、匿名内部类,需将其独立出来或者改为静态类;
  • 在静态内部类中持有外部类(非静态)的对象引用,使用弱引用来处理;
  • 不再使用的资源(对象),需显示释放资源(对象置为null),如果是集合需要清空;
  • 保持对对象生命周期的敏感,尤其注意单例、静态对象、全局性集合等的生命周期;
2.2 常见内存泄漏与优化

(1) 单例造成的内存泄漏

  • 案例

/** 工具类,单例模式

  • @Auther: Jiangdg
  • @Date: 2019/10/8 17:23
  • @Description:
    */
    public class CommonUtils {
    private static CommonUtils instance;
    private Context mCtx;

private CommonUtils(Context context){
this.mCtx = context;
}

public static CommonUtils getInstance(Context context) {
if(instance == null) {
instance = new CommonUtils(context);
}
return instance;
}
}

/**使用单例模式时造成内存泄漏
*

  • @Auther: Jiangdg
  • @Date: 2019/10/8 17:24
  • @Description:
    */
    public class SingleActivity extends AppCompatActivity {
    private CommonUtils mUtils;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mUtils = CommonUtils.getInstance(this);
}
}

  • 分析与优化

在上述示例中,当SingleActivity实例化Commontils对象完毕后,Commontils将持有SingleActivity对象的引用,而由于单例模式的静态特性,Commontils对象的生命周期将于应用进程的
一致,这就会导致在应用未退出的情况下,如果SingleActivity对象已经不再需要了,而Commontils对象该持有该对象的引用就会使得GC无法对其进行正常回收,从而导致了内存泄漏。优化:对于需要传入Context参数的情况,尽量使用Application的Context,因为它会伴随着应用进程的存在而存在。

public class SingleActivity extends AppCompatActivity {
private CommonUtils mUtils;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 造成内存泄漏
//mUtils = CommonUtils.getInstance(this);

mUtils = CommonUtils.getInstance(this.getApplicationContext());
}
}

(2) Handler造成的内存泄漏

  • 案例

/** 使用Handler造成内存泄漏

  • @Auther: Jiangdg
  • @Date: 2019/10/8 17:55
  • @Description:
    */
    public class HandlerActivity extends AppCompatActivity {

// 匿名内部类
private Handler mUIHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

new Thread(new Runnable() {
@Override
public void run() {
// 处理耗时任务
// …

mUIHandler.sendEmptyMessage(0x00);
}
});
}
}

  • 分析与优化

在Android应用启动时,应用程序的主线程会为其自动创建一个Looper对象和与之关联的MessageQueue,当主线程实例化一个Handler对象后,它就自动与主线程的MessageQueue关联起来,所有发送到MessageQueueMessage(消息)都会持有Handler的引用。由于主线程的Looper对象会随着应用进程一直存在的且Java类中的非静态内部类和匿名内部类默认持有外部类的引用,假如HandlerActivity提前出栈不使用了,但MessageQueue中仍然还有未处理的MessageLooper就会不断地从MessageQueue取出消息交给Handler来处理,就会导致Handler对象一直持有HandlerActivity对象的引用,从而出现HandlerActivity对象无法被GC正常回收,进而造成内存泄漏。优化:将Handler类独立出来,或者使用静态内部类,因为静态内部类不持有外部类的引用。

public class HandlerActivity extends AppCompatActivity {

// 匿名内部类默认持有HandlerActivity的引用
// 造成内存泄漏
// private Handler mUIHandler = new Handler() {
// @Override
// public void handleMessage(Message msg) {
// super.handleMessage(msg);
// }
// };

// 优化,使用静态内部类
// 假如要持有HandlerActivity,以便在UIHandler中访问其成员变量或成员方法
// 需要使用弱引用处理
private UIHandler mUIHandler;
static class UIHandler extends Handler {
private WeakReference mWfActivity;

public UIHandler(HandlerActivity activity) {
mWfActivity = new WeakReference<>(activity);
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mUIHandler = new UIHandler(this);
}
}

Java中四种引用关系:

  • 强引用用来描述永远不会被垃圾收集器回收掉的对象,类似"Object obj = new Object"
  • 软引用用来描述一些还有用但并非必须的对象,由SoftReference类实现。被软引用关联着的对象会在系统将要发生OOM之前,垃圾收集器才会回收掉这些对象。
  • 弱引用用来描述非必须的对象,比软引用更弱一些,由WeakReference类实现。被弱引用的对象只能生产到下一次垃圾收集发生之前,无论当前内存是否足够。
  • 虚引用最弱的引种引用关系,由PhantomReference类实现。一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用来获取该对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被垃圾收集器回收时收到一个系统通知

(3) 线程(非静态内部类或匿名内部类)造成的内存泄漏

  • 案例

/** 使用线程造成的内存泄漏

  • @Auther: Jiangdg
  • @Date: 2019/10/9 10:04
  • @Description:
    */
    public class ThreadActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 开启一个子线程
new Thread(new MyRunnable()).start();
// 开启一个异步任务
new MyAsyncTask(this).execute();
}

class MyRunnable implements Runnable {

@Override
public void run() {

}
}

class MyAsyncTask extends AsyncTask {
private Context mCtx;

public MyAsyncTask(Context context) {
this.mCtx = context;
}

@Override
protected Object doInBackground(Object[] objects) {
return null;
}
}
}

  • 分析与优化

在之前的分析中可知,Java类中的非静态内部类和匿名内部类默认持有外部类的引用。对于上述示例中的MyRunnableMyAsyncTask来说,它们是一个非静态内部类,将默认持有ThreadActivity对象的引用。假如子线程的任务在ThreadActivity销毁之前还未完成,就会导致ThreadActivity无法被GC正常回收,造成内存泄漏。优化:将MyRunnable和MyAsyncTask独立出来,或使用静态内部类,因为静态内部类不持有外部类的引用。

public class ThreadActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 开启一个子线程
new Thread(new MyRunnable()).start();
// 开启一个异步任务
// 优化:使用Application的Context
new MyAsyncTask(this.getApplicationContext()).execute();
}

// 优化:使用静态内部类
static class MyRunnable implements Runnable {

@Override
public void run() {

}
}

// 优化:使用静态内部类
// 如果需要传入Context,使用Application的Context
static class MyAsyncTask extends AsyncTask {
private Context mCtx;

public MyAsyncTask(Context context) {
this.mCtx = context;
}

@Override
protected Object doInBackground(Object[] objects) {
return null;
}
);
}

// 优化:使用静态内部类
static class MyRunnable implements Runnable {

@Override
public void run() {

}
}

// 优化:使用静态内部类
// 如果需要传入Context,使用Application的Context
static class MyAsyncTask extends AsyncTask {
private Context mCtx;

public MyAsyncTask(Context context) {
this.mCtx = context;
}

@Override
protected Object doInBackground(Object[] objects) {
return null;
}

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值