Java的内存相关

很烦 被问到这个Java内存机制很懵逼 网上找来这些


面试题:Java的内存模型
标准答案:Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。程序中的变量存储在主内存中,每个线程拥有自己的工作内存并存放变量的拷贝,线程读写自己的工作内存,通过主内存进行变量的交互。JMM就是规定了工作内存和主内存之间变量访问的细节,通过保障原子性、有序性、可见性来实现线程的有效协同和数据的安全。

面试题:JVM如何判断一个对象实例是否应该被回收?
标准答案: 垃圾回收器会建立有向图的方式进行内存管理,通过GC Roots来往下遍历,当发现有对象处于不可达状态的时候,就会对其标记为不可达,以便于后续的GC回收。

面试题:说说JVM的垃圾回收策略。
标准答案: JVM采用分代垃圾回收。在JVM的内存空间中把堆空间分为年老代和年轻代。将大量创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象。



作者:goeasyway
链接:https://www.jianshu.com/p/7e0833df599b
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

面试题:如何检测内存泄露,如何进行内存优化?

所谓“知己知彼,百战不殆”,我们要解决内存问题前,至少应该先了解一下内存是什么。说到内存有两个概念要先搞清楚:RAM和ROM。手机的RAM即我们常说的内存,是Random Access Memory的缩写,即随机存储器,在工作状态时可以随机读写数据,断电以后会丢失数据。手机的ROM和传统的ROM(Read Only Memory)又有些不一样,手机上的ROM在一定条件下是可写的,用户层级的写入被限制(所以有些用应需要ROOT权限)。它分为两部分,一部分是供系统使用(/System和/Cache),另外一部分是用作用户存储数据(/data),用户安装的应用就在这个data空间中。

Android 2.0后的系统可以把APK应用安装在SD卡上,但4.0后又取消了这个做法,不允许安装在SD卡上,而且随着手机内部存储容量的扩大,很多手机都不支持外插一个TF卡了。

我们为什么需要内存?

先来复习一个操作系统的知识,CPU执行运算时需要从内存(RAM)获取操作数据,对于多任务系统来说同时有多个进程在运行,不可能给每个进程都分配置大量的内存,因为RAM容量(即物理内存的容量)始终是有限的。“解决不了的问题,可以通过增加中间层来解决”,前人们通过给进程增加一个虚拟内存,让每个进程都有独立的虚拟内存空间,由系统来管理虚拟内存到物理内存的映射,保证各个进程的数据不会错乱。

在32位的系统中,内存的寻址空间是2的32次方,即4GB。

从上图也可以明白,你的应用中的对象所能所使用的内存在理论上也不能操过Heap区域的大小。

Android系统是基于Linux的,但Android中的进程分为两种,一种是Native进程,一种是Java进程。我们常接触的APK应用都是运行在Java进程中的。Android应用运行于Dalvik虚拟机之上的进程,由Dalvik虚拟机负责分配和管理应用的Java对象需要的内存。而对应到内存上,就存在Natvie内存和Java内存,由Dalvik虚拟机管理的就是Java内存(或者叫Java Heap内存)。

Native进程:采用C/C++实现,不包含Dalvik实例的进程,/system/bin/目录下面的程序文件运行后都是以native 进程形式存在的,/system/bin/surfaceflinger、/system/bin/rild、procrank等就是Native进程。

一般我们常遇到的内存问题,基本上都集中在Java内存。一些游戏或视频之类的应用,有可能会更多的面对Native的内存问题。

虽然有些手机的RAM很大(如小米5S有4G RAM),但Android系统为了能同时让比较多的进程常驻内存(这样程序启动时就不用每次都重新加载到内存,从而加快切换应用的速度),它们给每一个应用进程都分配了一个有限制的较小的heapsize,当Java申请的堆内存空间超过阈值时,就会抛出OOM异常。也就是说,程序发生OMM并不表示RAM不足,而是因为程序申请的Java Heap对象超过了Dalvik虚拟机设直heapgrowthlimit。

目前主流的一些机型这个值都有190M上下,想想几年前只有16到32M,是不是很幸福?

正如我们在“图片到底是什么”那个面试题中也有说过,Bitmap图片可以放在Java堆内存中,也有些情况可以放在Native内存中来获得更大的内存,避免OOM。因为APK应用的Java进程也是运行在Native进程之上的,而Native的内存空间是不受Dalvik虚拟机的heapsize限制,相对而言大很多(Java和Native可以通过JNI进行通信)。

了解了这些基础知识后,在面试进,你可能会遇到这样的问题:“如果一个项目的代码量很大,通过代码直接分析内存问题不太现实时,你有什么手段或者工具去发现这个项目中是否存在内存问题?并说一下解决的办法或者相关的案例。对于初级的开发,你对他们有什么建议能让他们避免内存问题吗?”



作者:goeasyway
链接:https://www.jianshu.com/p/26d37babcb50
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

对于我们来说,最容易发现的内存问题当然是OOM(OutOfMemoryError),应用直接Crash,日志也会很清晰的标明哪个对象OOM了。这个解决起来也不难,常见的Bitmap OOM相信大家也知道怎么处理。

相对于OOM较麻烦一点的就是内存泄露(Memory Leak),每次就露那么一点,就像温水煮青蛙一样,很难发现有变化。内存泄漏也是造成应用程序OOM的主要原因之一!我们知道Android系统为每个应用程序分配的内存有限,而当一个应用中产生的内存泄漏比较多时,之后我们再申请新的内存时会及其容易产生OOM。

内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到GC roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。

面试题:如何检测内存泄露,如何进行内存优化?

Android系统为每一个应用程序都设置了一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起OOM。

较简单的查看一个应用的内存使用情况可以通过DDMS的Heap视图查看:


在Android Studio上也可以通过Memory Monitor查看内存中Dalvik Heap的实时变化:



注意:GC过于频繁容易出现内存抖动,这也是造成应用卡顿的常见原因。

也可以通过命行的方式查看:

adb shell dumpsys meminfo <package_name|pid> [-d]

具体的数值意义可以查看官网的说明:https://developer.android.com/studio/profile/investigate-ram.html

MAT内存分析工具

详细的内存使用情况,可以通过Android Studio的Android Monitor界面,在Memory那栏有上几个小图标,点击有一个向下箭头的图标会自动生成并打开的HPROF视图。

不过用他来分析内存泄露还不是很智能,我们可以借助第三方工具,常见的工具就是MAT了(Memory Analyzer Tool),下载地址 http://eclipse.org/mat/downloads.php,这里我们需要下载独立版的MAT(之前在使用Ecelipse开发Android应用时,我们常常会使用它的插件版本)。

注意:Android Monitor生成的HPROF文件为Dalvik虚拟机格式的,需要转成J2SE虚拟机格式的,否则MAT工具中无法打开。转换的方式也很简单,Android Studio自带了,直接在“Captures”->"Heap Snapshot"选中刚刚生成的".hprof"文件,然后鼠标右键选择“Export to standard .hprof”可以在MAT上使用了。

MAT的具体使用方式,网上很多,大家可以自己搜一下。这里就提一下用它怎么能快速查找到内存泄露的点,比如通过“Dominator Tree”的"Path To GC Roots"的排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个我们已经不需要使用的对象否还存在强引用链。比如,我们已退出一个Activity(onDestroy方法也被执行了),但在Path To GC Roots中却发现这个Activity对象还被有一个引用链,那么就可以确认这个Activity对像就产生了内存泄漏。一般来说,从它的引用链上也可以直观地看出是谁在引用它。

除了上面介绍了MAT检测内存泄露, 有一个叫LeakCanary工具大家也可以尝试一下。项目地址:https://github.com/square/leakcanaryLeakCanary会检测应用的内存回收情况,如果发现有垃圾对象没有被回收,就会去分析当前的内存快照,也就是上边MAT用到的.hprof文件,找到对象的引用链,并显示在页面上。这款插件的好处就是,可以在手机端直接查看内存泄露的地方,可以辅助我们检测内存泄露。

开发中如何避免内存泄漏

这点我比较喜欢问面试者,希望面试者能罗列出一些他自己遇到过的情况。通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多(它里面有N多资源的引用),影响比较明显。下面就示例两种错误的引用方式。

错误的单例模式

public class Singleton {
    private static Singleton instance;
    private Context mContext;

    private Singleton(Context context) {
        this.mContext = context;
    }

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

这是一个非线程安全的单例模式,instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,假如Activity A去getInstance获得instance对象,传入this,常驻内存的Singleton保存了你传入的Activity A对象,并一直持有,即使Activity被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC掉,这样就导致了内存泄漏。

View持有Activity引用

public class MainActivity extends Activity {
    private static Drawable mDrawable;

    @Override
    protected void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = new ImageView(this);
        mDrawable = getResources().getDrawable(R.drawable.ic_launcher);
        iv.setImageDrawable(mDrawable);
    }
}

有一个静态的Drawable对象当ImageView设置这个Drawable时,ImageView保存了mDrawable的引用,而ImageView传入的this是MainActivity的mContext,因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,MainActivity被销毁时,也不能被GC掉,所以造成内存泄漏。

其实避免Activity的泄漏的方式可以总结为:不要让生命周期长于Activity的对象持有到Activity的引用。

在开发中,我们也可以给一些初级的工程师相关的建议,如:

  1. 注意单例模式和静态变量是否会持有对Context的引用;
  1. 注意监听器的注销;(在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。)
  2. 不要在Thread或AsyncTask中的引用Activity;

小结

内存泄漏检测并不属于一个经常会做的事情,所以上面写的一些东西难免会有一些错误。不过我认为在面试中,更关注的是面试者做何去发现和解决这个问题,然后是否会对遇到过的问题有一个总结,至于细节上的东西在真正做的时候会直接得到工具或LOG的反馈,不一定非常记得很清楚的人才说明他会这个东西。



作者:goeasyway
链接:https://www.jianshu.com/p/e86c58ba9343
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

发布了36 篇原创文章 · 获赞 69 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/xchaha/article/details/80426860