《移动App性能评测与优化》第一章1.3.3介绍了优化Dalvik内存碎片。文中列举了一段代码,可能会在GC后引起内存碎片问题,代码如下:
private Object result[] = new Object[100];
void fool(){
for (int i = 0;i < 100; i++){
byte[] tmp = new byte[2000];
result[i] = new byte[4];
}
}
文中大意是:result[]对象数组每一个对象在分配内存之前都会先分配一个临时的byte[[2000],因此实际给result[]对象数组每一个对象分配的byte[4]所在的内存地址是不连续的。当GC之后,临时对象tmp申请的内存被释放,留下了碎片化的内存。
提出问题:
1.GC之前result[]数组中每个成员的内存地址分布情况如何?
2.Dalvik GC会对内存碎片做整理么?
3.GC之后result[]数组中每个成员的内存地址分布情况如何?
准备工作:
Android Studio 3.2.1
Android Device Monitor,工具所在目录:Android\Sdk\tools\lib\monitor-x86_64\monitor.exe
[Memory Analyzer] (https://www.eclipse.org/mat/downloads.php)
程序编码:
package com.peterzhang.oom;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private Object result[] = new Object[10000];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void buttonOnclick(View view){
testMemoryFragmentation();
}
private void testMemoryFragmentation(){
for (int i = 0;i < 10000; i++){
byte[] tmp = new byte[2000];
result[i] = new byte[4];
}
}
}
点击运行,通过DeviceMonitor观察内存分配状态
Heap Allocated 大小为12.558M
通过DeviceMonitor左上角 "Dump HPROF File"导出此时的内存信息二进制文件a.hprof
此时生成的hprof文件还无法直接通过MAT工具打开需要利用AppData\Local\Android\Sdk\hprof-conv 工具转换格式,命令如下:
hprof-conv C:\Users\112106101\Desktop\mat\nexus4-android4.0.3\a.hprof C:\Users\112106101\Desktop\mat\nexus4-android4.0.3\a-standard.hprof
备注:此时导出来的二进制内存文件除了包含app自身占用的内存,还包括了App共享的系统资源占用的内存。如果期望只过滤出App自身的内存可以使用如下命令:
hprof-conv -z exclude non-app C:\Users\112106101\Desktop\mat\nexus4-android4.0.3\1.hprof C:\Users\112106101\Desktop\mat\nexus4-android4.0.3\1-android.hprof
然后打开MAT工具,选择open a-standard.hprof
可以看到,MainActivity一共占用了42312byte
点击“分配对象”按钮,执行testMemoryFragmentation方法,通过DeviceMonitor观察内存分配状态
Heap Allocated 大小为12.786M
同样导出HPROF内存文件,并通过MAT工具打开:
可以看到此时MainActivity一共占用了201920byte
执行testMemoryFragmentation方法,并GC之后,内存Heap Allocated增加了0.228M,大概是220k
MainActivity占用内存增加了159608byte,约等于160k,小于DeviceMonitor显示的内存增加数据220k。
监测工具 | 对象分配前后增加内存 |
---|---|
DeviceMonitor | 220k |
MAT | 160k |
增加的内存主要是因为result[]的内存申请
观察result[]数组内存分配,发现内存地址不是连续的,莫非是GC之后是存在了内存碎片?
验证:去掉代码
byte[] tmp = new byte[2000];
会不会就不存在内存碎片呢?
测试后数据如图所示:
从图上的数据观察到此时依然有内存碎片。
那么我们就无法下结论这里看到的内存碎片是由于代码引起的。
byte[] tmp = new byte[2000];
什么是内存碎片?
内存需要像硬盘一样定期清理碎片么?当然!内存碎片分为内部碎片和外部碎片。例如
HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全.。
这部分对齐填充的内存就属于内部碎片。外部碎片更容易理解,GC发生后会出现空闲的内存不连续,这部分就是外部碎片。
从MAT分析的result[]对象内存数据可以看到,每一个result[i]占用的内存是16byte,对应的代码是
result[i] = new byte[4];
这里16byte是如何得到的呢?
对象在内存中存储布局可以分为3块区域:对象头、实例数据和对齐填充。如果对象是一个数组,那么在对象头中还必须有一块用于记录数组长度的数据。《深入理解JAVA虚拟机》
我们来分析一下result[i]占用的内存空间:
类型 | 大小 |
---|---|
对象头 | 8byte |
数组大小 | 4byte |
实例数据 | 4byte |
对齐填充 | 4byte |
因此每一个result[]对象就占用了4byte的内部碎片。
除了内部碎片,从MAT分析的内存数据看到,还存在一些外部碎片。这也是上述测试数据中MAT分析的result[]申请对象160k,但是实际上HeapAllocated增加了220k的原因。