1、内存管理机制
JVM有自动内存管理机制,不需要人为地给每一个new操作写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题。然而一旦出现内存泄漏和溢出方面的问题,如果不清楚JVM内存的内存管理机制,将很难定位与解决问题。
- 线程私有数据区包含:程序计数器、虚拟机栈、本地方法栈
- 线程共享数据区包含:Java堆、方法区(内部包含常量池、直接内存
程序计数器
当前线程所执行的字节码的行好指示器,一个线程一个,线程私有。
- 如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计数器值为空。
Java虚拟机栈
线程私有,生命周期和线程一样。虚拟机栈描述的是java方法执行的内存模式:每个方法被执行都会创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。
- 每一个方法被调用直到被执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
- 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StatckOverflowError异常。如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常
本地方法栈
与虚拟机栈的作用相似,虚拟机栈为虚拟机执行java方法服务,本地方法栈则是为虚拟机使用Native方法服务。
- 也会抛出StatckOverflowError和OutOfMemoryError异常
java堆
所有线程共享的一块内存区域(最大),虚拟机启动的创建。存放对象实例,为对象实例分配内存。
- 是垃圾收集器管理的主要区域,也被称做“GC堆”。
- 可处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
- 如果在堆没有内存完成实例分配,并且堆也无法再扩展,将会抛出OutOfMemoryError异常
方法区
所有线程共享的一块内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据
- 和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可选择不实现GC。
- 当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常
运行时常量池
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 相对于Class文件常量池的一个重要特征是具备动态性,体现在并非只有预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
- 是方法区的一部分,会受到方法区内存的限制。
- 在Java虚拟机规范中,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
直接内存
直接内存也称堆外内存,它不是虚拟机运行时数据区的一部分。JDK1.4后引入NIO类,是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接在堆外分配内存,然后通过存储在Java堆中的DirectByteBuffer对象作为引用对这块内存进行操作。这样能够显著提高性能,避免Java堆和Native堆中来回复制数据。
对象访问
主流的两种访问方式
通过句柄访问对象
在Java堆中划分出一块内存来作为句柄池,reference存储的是对象的句柄地址,在句柄中包含了对象实例数据与类型数据各自的具体地址信息。好处:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
通过直接指针访问对象
在Java堆对象的布局中考虑如何放置访问类型数据的相关信息,reference存储的直接就是对象地址。好处:速度更快,节省了一次指针定位的时间开销。
2、Java对象的生命周期
创建阶段(Created)
- 为对象分配存储空间
- 构造对象
- 初始化(static成员到非static成员,超类到子类)
应用阶段(In Use)
- 对象至少被一个强引用持有着。
不可见阶段(Invisible)
- 程序本身不再持有该对象的不论什么强引用
- 这些引用仍然存在
- 程序的运行已经超出了该对象的作用域。
不可达阶段(Unreachable)
- 程序本身不再持有该对象的不论什么强引用
- 该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着(GC root)
收集阶段(Collected)
- 垃圾回收器已经对该对象的内存空间又一次分配做好准备时
3、判断Java中对象存活
根搜索方法
根搜索方法是通过一些“GCRoots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(ReferenceChain),当一个对象没有被GCRoots的引用链连接的时候,说明这个对象是不可用的。
GCRoots对象包括:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
- 方法区域中的类静态属性引用的对象。
- 方法区域中常量引用的对象。
- 本地方法栈中JNI(Native方法)的引用的对象。
4、JVM垃圾回收算法
复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当一块内存用完了,将存活的对象复制到另一块上面,然后把已使用的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法将内存缩小为原来的一半,代价较高。
标记-清除算法
算法分为"标记"和"清除"两个阶段:首先标记出需要回收的对象,在标记完成后统一回收被标记的对象。它主要不足有两个:一是效率问题,标记和清除两个过程效率都不高。二是空间问题,标记清除后会产生大量不连续内存碎片,碎片太多可能导致要分配较大对象时,无法找到足够的内存空间不得不提前触发一次垃圾收集动作。
标记-整理算法
标记过程与"标记-清除"算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法
商业虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期将内存划分为几块。Java堆分为新生代和老年代,这样可以根据年代特点采用适当的收集算法。新生代中每次垃圾收集都有大批对象死去,那就选用复制算法。老年代对象存活率高,没有额外空间进行分配担保,适合使用"标记-清理"或"标记-整理"算法来回收。
5、垃圾回收器
通常虚拟机中往往不止有一种GC收集器,下图展示的是HotSpot虚拟机中存在的七种作用于不同分代(新生代、老年代)的收集器,其中被连线的两个收集器表示可以搭配使用。
各垃圾回收器区别对比:
具体可参考:https://www.jianshu.com/p/114bf4d9e59e
6、内存分配与回收策略
可以看到整个内存分为三个区域:新生代,老年代,永久代。
不同的对象的生命周期是不一样的,因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
年轻代(Young Generation)
- 所有新生成的对象首先都是放在年轻代的。
- 新生代中每次垃圾收集都有大批对象死去,那就选用复制算法。
- 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。
- 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)
年老代(Old Generation)
- 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。
- 内存比新生代也大很多(大概比例是1:2)
- 当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低
- 老年代对象存活时间比较长,存活率标记高,因此适合使用"标记-清理"或"标记-整理"算法来回收。
持久代(Permanent Generation)
- 用于存放静态文件,如Java类、方法等。
- 持久代对垃圾回收没有显著影响
内存对象的处理过程
- 大部分对象在Eden区中生成。
- 回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区
- 当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
- 当步骤3达到一定次数,当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
- 当对象在老年代区域停留的世界达到一定程度,该对象会移动到持久代。持久代也会存放一些静态文件。
补充
当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
空间分配担保:
在Minor GC前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果条件成立,那么Minor GC是成立的。如果不成立,虚拟机查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次移动到老年代对象的平均大小,如果大于,将尝试一次Minor GC。如果小于,或者HandlePromotionFailure设置值不允许冒险,那将进行一次Full GC。
对象从年轻代进入老年代时机
1、对象优先在Eden分区:
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机发起一次Minor GC。GC后对象尝试放入Survivor空间,如果Survivor空间无法放入对象时,只能通过空间分配担保机制提前转移到老年代。
2、大对象直接进入老年代:
大对象指需要大量连续内存空间的Java对象。虚拟机提供-XX:PretenureSizeThreshold参数,如果大于这个设置值对象则直接分配在老年代。这样可以避免新生代中的Eden区及两个Survivor区发生大量内存复制。
3、长期存活的对象进入老年代:
虚拟机会给每个对象定义一个对象年龄计数器。如果对象在Eden出生并且经过一次Minor GC后任然存活,且能够被Survivor容纳,将被移动到Survivor空间中,并且对象年龄设为1.每次Minor GC后对象任然存活在Survivor区中,年龄就加一,当年龄到达-XX:MaxTenuringThreshold参数设定的值时,将会移动到老年代。
4、动态年龄判断:
虚拟机不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold设定的值才会将对象移动到老年代去。如果Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
7、GC的执行机制
GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。操作频繁,速度快
Full GC
Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。
可能导致Full GC的原因
年老代空间不足
1)分配足够大空间给old gen。
2)避免直接创建过大对象或者数组,否则会绕过年轻代直接进入年老代。
3)应该使对象尽量在年轻代就被回收,或待得时间尽量久,避免过早的把对象移进年老代。方法区的永久代空间不足
1)分配足够大空间给。
2)避免创建过多的静态对象。被显示调用System.gc()
通常情况下不要显示地触发GC,让JVM根据自己的机制实现。上一次GC之后Heap的各域分配策略动态变化
参考:
周志明老师的《深入理解Java虚拟机》
《android应用性能优化最佳实践》
https://www.jianshu.com/p/a9ff882337d4
https://www.cnblogs.com/andy-zcx/p/5522836.html
https://www.cnblogs.com/fefjay/p/6297340.html
https://www.jianshu.com/p/a62697f00b85
https://www.jianshu.com/p/b4a03b5de0d9
https://www.jianshu.com/p/114bf4d9e59e
https://www.cnblogs.com/mengfanrong/p/4007456.html
8、三大内存泄漏检测工具:
8.1、LeakCanary
https://github.com/square/leakcanary
8.2、StrictMode
private boolean DEV_MODE = true;
public void onCreate() {
if (DEV_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyDialog() //弹出违规提示对话框
.penaltyLog() //在Logcat 中打印违规异常信息
.penaltyFlashScreen() //API等级11
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
https://www.cnblogs.com/yaowen/p/6024690.html
8.3、Android Profiler
参考:https://blog.csdn.net/niubitianping/article/details/72617864
https://blog.csdn.net/niubitianping/article/details/72617864
9、常见内存泄漏
9.1、资源型文件未关闭
资源型文件(比如Cursor、File文件等)往往都用了一些缓存,在不使用时,应该及时关闭他们,以便它们的缓存数据能够及时被回收。避免内存泄漏。
9.2、注册对象未注销
如果在退出Activity,已注册的对象没有及时注销,会导致Activity无法被GC回收。不断进出这个Activity会导致大量Activity无法被回收而引起频繁的GC情况,甚至导致Out of Memory
9.3、类的静态变量持有大数据对象,如Bitmap等
9.4、非静态内部类的静态实例持有activity引用
内部类会持有外部类引用,如果实例是静态的会间接长期维持着外部类的引用,导致无法被GC回收
9.5、handler
问题根源: 非静态匿名内部类持有外部类引用
解决办法:
1、使用一个静态Handler内部类,然后对Handler持有的对象使用弱引用
public class NoLeakActivity extends AppCompatActivity {
private NoLeakHandler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new NoLeakHandler(this);
Message message = Message.obtain();
mHandler.sendMessageDelayed(message,10*60*1000);
}
private static class NoLeakHandler extends Handler{
private WeakReference mActivity;
public NoLeakHandler(NoLeakActivity activity){
mActivity = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
}
2、在Activity的onDestrory或者onStop时,移除消息队列中的消息,避免Looper的消息队列中有待处理的消息需要处理
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
9.6、容器中的对象没清理
9.7、webview
缺点:内存占用巨大,存在内存泄露,崩溃率高
在 Android 5.1 系统上,在项目中遇到一个WebView引起的问题,每打开一个带webview的界面,退出后,这个activity都不会被释放,activity的实例会被持有,由于我们项目中经常会用到浏览web页面的地方,可能引起内存积压,导致内存溢出的现象
解决办法
public void destroy() {
if (mWebView != null) {
// 如果先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,需要先onDetachedFromWindow(),再
// destory()
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
// 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
try {
mWebView.destroy();
} catch (Throwable ex) {
}
}
}
参考:https://yq.aliyun.com/articles/61612
然而OPPO机型仍然存在java.util.concurrent.TimeoutException。可尝试为webview开启多线程
9.7.1、解决办法
为webview开启独立进程,在适当时机杀掉webview独立进程
9.7.2、开启多线程好处:
- 每个独立的进度都能分配独立的内存,为webview开启独立进程,相当于APP可以获得双倍内存,增大webview获得的内存,变相的减少内存泄漏产生OOM的概率
- webview发生崩溃不会导致APP闪退
9.7.3、为webview开启多线程
修改AndroidManifest.xml
9.7.4、数据同步问题
在acitvity onDestroy回调时判断到不是相同的进程,就将进程杀掉,释放内存
参考:https://www.jianshu.com/p/8ed995016fde
9.8、优化内存空间
强引用(StrongReference)
Object object =new Object();
是指创建一个对象并把这个对象赋给一个引用变量。
- 强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
软引用(SoftReference)
- 只具有软引用的对象,会在内存空间不足的时候被GC,如果回收之后内存仍不足,才会抛出OOM异常;
- 软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
弱引用(WeakReference)
- 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
虚引用(PhantomReference)
- 仅持有虚引用的对象,在任何时候都可能被GC;
- 常用于跟踪对象被GC回收的活动;
- 必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
参考:https://www.cnblogs.com/huajiezh/p/5835618.html
9.9、珍惜Services资源
避免做耗时操作,减少使用常驻服务,尽量使用IntentService控制Service生命周期
9.10、OnTrimMemory优化
9.10.1、OnTrimMemory作用
主要作用就是指导应用程序在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验.
Android系统会根据不同等级的内存使用情况,调用这个函数,并传入对应的等级:
表示应用程序的所有UI界面被隐藏:
TRIM_MEMORY_UI_HIDDEN 即用户点击了Home键或者Back键
可以在此时释放UI资源。
当我们的应用程序真正运行时的回调:
TRIM_MEMORY_RUNNING_MODERATE 低内存状态+
TRIM_MEMORY_RUNNING_LOW 低内存状态++
TRIM_MEMORY_RUNNING_CRITICAL 低内存状态+++,已经根据LRU缓存规则杀掉大部分缓存进程
9.10.2、当应用程序是缓存时的回调
TRIM_MEMORY_BACKGROUND 低内存状态,在LRU列表最近位置,会被杀掉概率+
TRIM_MEMORY_MODERATE 低内存状态,在LRU列表中间位置,会被杀掉概率++
TRIM_MEMORY_COMPLETE 低内存状态,在LRU列表最边缘位置,会被杀掉概率+++
9.10、3、可实现OnTrimMemory的组件
Application、Activity、Fragement、Service、ContentProvider
参考:https://www.jianshu.com/p/5b30bae0eb49
9.10.4、OnTrimMemory回调可以释放哪些资源?
文件缓存、图片缓存(第三方图片库的缓存),动态生成动态添加的View(如列表)
9.10.5、OnTrimMemory和OnStop区别
- onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回调只有当我们程序中的所有UI组件全部不可见的时候才会触发
- onStop()方法只是当一个Activity完全不可见的时候就会调用
注意:onTrimMemory的TRIM_MEMORY_UI_HIDDEN 等级是在onStop方法之前调用的
9.10.6、使用OnTrimMemory的好处
增加不被杀掉的几率,热启动
9.10.7、使用场景
- 常驻内存的应用(比如电话)
- 有后台Service运行的应用(比如音乐、下载等)
9.11、检查应该使用多少内存
9.11.1、getMemoryClass()来获取app可以用heap大小
ActivityManager mActivityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = mActivityManager. getMemoryClass();
9.11.2、声明更大的heap空间和获取方式:
声明:
在manifest的Application标签下添加largeheap=true
获取大heap size:
int largeMemoryClass = mActivityManager. getLargeMemoryClass();
注意:不一定能获取更大的heap,因为在某些严格的机器上,large heap 和通常的heap size 是一样的。
缺点:
- 使用额外内存会影响系统的用户体验
- GC运行时间更长
9.12、Bitmap
待复习
9.13、使用最优的数据类型
9.13.1、使用@IntDef @StringDef代替枚举
枚举特点:
- 代码易读,安全
- 每个枚举值都是一个单例对象,内存占用大
可以使用@IntDef @StringDef代替,同样能达到类型安全,代码易读的效果
dependencies {
compileOnly 'com.android.support:support-annotations:25.1.0'
}
ublic class SexTest {
public final int MAN = 2;
public final int WOMEN = 3;
/**
* 只能使用 {@link #MAN} {@link #WOMEN}
*/
@Documented // 表示开启Doc文档
@IntDef({
MAN,
WOMEN,
}) //限定为MAN,WOMEN
@Target({
ElementType.PARAMETER,
ElementType.FIELD,
ElementType.METHOD,
}) //表示注解作用范围,参数注解,成员注解,方法注解
@Retention(RetentionPolicy.SOURCE) //表示注解所存活的时间,在运行时,而不会存在 .class 文件中
public @interface Sex { //接口,定义新的注解类型
}
public void setSex(@Sex int sex){
this.sex = sex;
}
public static void main(String[] args){
setSex(MAN);
}
}
引申:注解的原理及使用
9.13.2、SparseXXX和ArrayMap的使用
HashMap是哈希表结构,它存储的内容是键值对(key-value)映射。一个数组是记录key的Hash值,每个hash值对应一个记录Entry对象的链表。
共同点:对key使用二分法进行从小到大排序,在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,然后通过index来进行添加、查找、删除等操作。所以在数据量大的情况下性能并不明显,将降低至少50%。
数据量都在千级以内用SparseXXX和ArrayMap替代HashMap能提升性能。
1、SparseXXX
- SparseBooleanArray
key是用int数组记录,value是用boolean数组记录
- SparseArray
key是用int数组记录,value是用Object数组记录
- LongSparseArray
key是用Long数组记录,value是用Object数组记录
优势:
- 避免了基本数据类型的装箱操作
- 不需要额外的结构体,单个元素的存储成本更低
- 数据量小的情况下,随机访问的效率更高
缺点:
- 插入操作需要复制数组,增删效率降低
- 数据量巨大时,复制数组成本巨大,gc()成本也巨大
- 数据量巨大时,查询效率也会明显下降
2、ArrayMap
key是Object,一个数组记录key的hash值,另外一个数组记录Value值
用ArrayMap替代HashMap的好处是减少内存消耗,提升性能,查询效率高。
9.14、注意代码抽象
抽象会导致一个显著的开销:通过需要同等量的代码用于可执行。那些代码会被map到内存中
如果抽象没有显著地提高效率,应该尽量避免,避免过度代码抽象。
9.15、避免使用依赖注入框架
- 会通过扫描你的代码执行更多的初始化操作
- 其运行方式把一些我们不需要的大量代码映射到内存中
- 被映射后的数据会被分配到干净的内群中,长时间都不会使用,造成内存浪费
9.16、谨慎使用外部库
不要为了1-2个小功能而导入整个library,如果没有找到吻合外部库,就尝试自己实现
9.17、剔除不必要的代码
压缩、优化、混淆
代码混淆的作用:去除无用代码,通过语义模糊 重命名类 字段 方法从而缩小、优化代码,从而使得代码更简洁、更少量的RAM映射页
9.18、内存抖动
问题:内存抖动会引起频繁的GC,从而使UI线程被频繁阻塞,导致画面卡顿。
优化方案:尽量避免频繁创建大量、临时的小对象