[TOC]
Java虚拟机是一台“抽象的计算机”,它拥有自己的处理器,堆栈,寄存器以及相应的指令系统;Java虚拟机屏蔽了与具体操作系统相关的平台信息,使得Java程序只需要生成在该虚拟机上运行的目标代码,就可以在多平台上运行。虽然叫Java虚拟机,但在它之上运行的语言不仅有Java、Kotlin、Groovy、Scala等都可以运行。
Java虚拟机包括:类加载系统、运行时区域、执行引擎、本地方法库等。
- 方法区(公有):被
JVM加载的类的结构信息,包括运行时常量池、字段、方法信息、静态变量等数据;Java堆(公有):JVM启动时创建,存储几乎所有对象的实例,可以细分为老年代、新生代(Eden、From Survivor、To Survivor),垃圾回收器主要就是管理堆内存,如果满了,就会出现OutOfMemoryError;Java虚拟机栈(私有):存储Java方法调用的状态,一个线程会执行一个或多个方法,一个方法对应一个栈帧,栈帧内容包含:局部变量表、操作数栈、动态链接、方法返回地址、附加信息等;栈内默认最大是1M,超出则抛出StackOverflowError;- 本地方法栈(私有):执行
native方法;- 程序计数器(私有):多线程中记录程序执行下一条指令的计数器,记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行
Native方法,则计数器值为空。
- 线程独自:每个线程都会有它独立的空间,随线程生命周期而创建和销毁;
- 线程共享:所有线程能访问这块内存数据,随虚拟机或者
GC而创建和销毁。
class文件包含JAVA程序执行的字节码:数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符;文件开头有一个0xcafebabe(16进制)特殊的一个标志,如下图所示:
- 强引用:当新建的对象为强引用时,垃圾回收器绝对不会回收它,宁愿抛出
OutOfMemoryError异常,让程序异常终止也不会回收; - 软引用:当新建的对象为软引用时,在内存不足时,回收器就会回收这些对象,如果回收后还是没有足够的内存,抛出
OutOfMemoryError异常; - 弱引用:当新建的对象为弱引用时,垃圾回收器不管当前内存是否足够,都会回收它的内存;
- 虚引用:虚引用跟其他引用都不同,如果一个对象仅持有虚引用,在任何时候都可能被
GC回收,只是当它被回收时会收到一个系统通知。
- 引用计数算法:每个对象都有一个引用计数器,当对象每被引用一次时就加1,引用失效时就减1;当计数为0时则将该对象设置为可回收的“垃圾对象”; 缺点:循环引用不能回收;
class Bean{
public Object b = null;
private byte[] data = new byte[1024 * 1024];
}
public class GCTest{
public static void main(String[] args) {
Bean b1 = new Bean();
Bean b2 = new Bean();
b1.b = b2;
b2.b = b1;
b1 = null;
b2 = null;
System.gc();
}
}- 可达性分析算法:将对象及其引用关系看做一个图,选定活动对象作为
GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用,那么认为是可回收对象。
可以作为GC Roots的对象:
- 虚拟机栈中正在引用的对象;
- 本地方法栈中正在引用的对象;
- 静态属性引用的对象;
- 方法区常量引用的对象;
- 标记-清除算法:用根搜索算法标记可被回收的对象,之后将被标记为“垃圾”的对象进行回收; --> 内存碎片
- 复制算法(年轻代):先把内存一分为二,每次只使用其中一个区域,垃圾收集时,将存活的对象拷贝到另一个区域,然后对之前的对象全部回收;--> 减小了内存使用空间
- 标记-压缩算法(老年代):在标记可回收的对象后,将所有的存活对象压缩到内存的一段,让它们排在一个,然后对边界以外的内存进行回收;
- 分代收集算法:
Java堆中存在的对象生命周期有较大差别,大部分生命周期很短,有的很长,设置与应用程序或者Java虚拟机生命周期一样。因为分代算法就是根据对象的生命周期长短,将对象放到不同的区域;
堆区:堆区分为年轻代和老年代,其空间大小理论比值为2:1;其中年轻代又会分为Eden区和Survivor区,其空间大小理论比值为8:2;Surfivor区又分为from区和to区,其空间大小理论比值为1:1。
gc流程:创建对象时首先会被放入Eden区,该区存满时会触发gc,gc时清除可回收对象,然后把Eden区剩余存活对象移动到From区;新创建的对象会继续被放入Eden区,第二次gc时清除Eden区和和From区可回收对象,然后把Eden区和From区剩余存活对象移动到To区;第三次gc时会把Eden区和To区中剩余存活对象移动到From区……依次反复进行。
从年轻代进入老年代的条件:
大对象,大对象会直接进入老年代;
每次gc时会对已存活对象进行标记(每次+1),标记达到一定次数(Java为15次,Android的CMS垃圾回收器为6次)时该对象会从年轻代进入老年代;
Survivor区中From或To区中的相同标记(相同年龄)对象大小总和大于等于From或To区的一半时,这些对象可以进入老年代。
在Java环境的bin目录下有一个jvisualvm工具,该工具可以观察到程序运行过程中内存的动态情况,从而证实上述描述。
内存抖动通常指在短时间内发生了多次内存的分配和释放,主要原因是短时间内频繁地创建对象。为了应对这种情况,虚拟机会频繁地触发GC操作,当GC进行时,其它线程会被挂起等待GC完成,频繁GC会使UI在绘制时超过16ms一帧,从而导致画面卡顿。
public class ChurnActivity extends AppCompatActivity {
private Handler mHandler;
private Button btnChurn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_churn);
initView();
setViewsListener();
}
private void initView() {
btnChurn = findViewById(R.id.btn_churn);
mHandler = new Handler();
}
private void setViewsListener() {
btnChurn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
churn(0);
}
});
}
Runnable r = new Runnable() {
@Override
public void run() {
allocate();
}
};
private void allocate() {
for (int i = 0; i < 1000; i++) {
String ob[] = new String[10000];
}
churn(50);
}
private void churn(int delay) {
mHandler.postDelayed(r, delay);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacks(r);
}
}使用Profiler查看内存抖动:
- 内存泄漏:一个不再被程序使用的对象或变量依旧存活在内存中无法被回收;
- 内存溢出:当程序申请内存时,没有足够的内存供程序使用;
比较小的内存泄漏并不会有太大的影响,但内存泄漏多了,占用的内存空间就更大,程序正常需要申请的内存则会相应减少。
内存泄漏分析工具使用MAT,它除了可以分析内存泄漏之外,还可以分析大对象。
官方下载地址:https://www.eclipse.org/mat/downloads.php 。
下载的是MAC版,安装时遇到一个问题:
The platform metadata area could not be written: /private/var/folders/9j/zj116b2n765fkk7qm1s7ctq8000解决方法:在应用程序中右键mat.app-->显示包内容-->Contents/Eclipse/MemoryAnalyzer.ini,修改内容如下:
借助Android Studio 的Profile工具,在操作页面之前dump(截取该时间点内存中存在的对象)一份文件,操作页面(比如进入SecondActivity 然后再返回主页面)之后再dump一份文件。然后把这两份文件(memory1.hprof,memory2.hprof)保存到本地。
使用Android SDK环境sdk/platform-tools/目录下的hprof-conv工具将3.3.3获取的hprof文件转换为MAT可以识别的文件:
hprof-conv -z memory1.hprof memory1_after.hprof
hprof-conv -z memory2.hprof memory2_after.hprof首先用Mat打开(Open Heap Dump..)两个转换后的hprof文件:
选择直方图:
排除其他引用:
定位结果:
因为我们进入SecondActivity之后又退出页面了,按道理其不应该存在,但此时排除其他引用之后发现它仍然存活,由此可以判断内存泄漏。从上图可以看出这是匿名内部类持有外部类引用引起的内存泄漏,需要在页面销毁时结束动画。
模块lqr_wechat是一个仿微信的项目,登录账号:dawa,密码:123456。在多次登录并退出之后会发现MainActivity存留多个,表明存在内存泄漏情况。通过Mat排查可知因使用网易云信的SDK并且没有及时注销引起内存泄漏,解决方式如下:
@Override
protected void onDestroy() {
unRegisterBroadcastReceiver();
super.onDestroy();
// 在这里将网易云信的注册 注销掉
NimAccountSDK.onlineStatusListen(mOnlineStatusObserver, false);
NimUserInfoSDK.observeUserInfoUpdate(userInfoobserver, false);
NimFriendSDK.observeFriendChangedNotify(changedNotifyObserver, false);
NimSystemSDK.observeReceiveSystemMsg(systemMessageObserver, false);
NimTeamSDK.observeTeamRemove(teamobserver, false);
}另在低版本上因输入法引起的内存泄漏解决方法:
@Override
protected void onDestroy() {
unRegisterBroadcastReceiver();
super.onDestroy();
// InputMethodManagerdManager
// mServedView
// mNextServedView
method("mServedView");
method("mNextServedView");
}
// 暴力置null(反射)
public void method(String attr){
InputMethodManager im = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
try {
Field field = InputMethodManager.class.getDeclaredField(attr);
field.setAccessible(true);
Object curView = field.get(im);
if(null != curView){
Context context = ((View)curView).getContext();
if(context == this){
field.set(im,null);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}- 使用
Memory profiler检测内存抖动; - 使用
MAT检测内存泄漏; - 使用
LeakCannary线下监控; - 采用
Glide等三方库加载图片。
- 避免在
for循环里分配对象占用内存; - 自定义
View的onDraw方法避免执行复杂的方法与创建对象; - 采用对象池模型解决频繁创建与销毁;
- 对
bitmap做缩放,重用bitmap; - 配置
LargeHeap属性; - 在
onTrimMemory进行处理; - 使用松散数组:
SparseArray,ArrayMap。












