Java编程中经常容易被忽视,但本身又十分重要的一个问题就是内存使用的问题。Android应用主要使用Java语言编写,因此这个问题也同样会在Android开发中出现。Android主要应用在嵌入式设备当中,而嵌入式设备由于一些众所周知的条件限制,通常都不会有很高的配置,特别是内存是比较有限的。如果我们编写的代码当中有太多的对内存使用不当的地方,难免会使得我们的设备运行缓慢,甚至是死机。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。一方面,如果程序在运行过程中出现了内存泄漏的问题,仅仅会使得自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启)。另一方面Android为不同类型的进程分配了不同的内存使用上限,如果应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉。 然而内存的消耗甚至泄露是不可避免的,那么首先只有养成良好的编程习惯,尽量做到减少内存的使用,尽可能的使垃圾内存得到回收,这才是解决问题的根本途径,那么我们该怎样编写代码呢?以下是本人的一些拙见以及网上达人们得一点点小小的建议: (一) 查询数据库没有关闭游标程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。我觉得可以这样做: Cursor c = sqliteDb.query(TABLE_WORDS, word_full_projection,selection, null, null, null, null);
int numRows = c.getCount();
if(numRows > 0){
c.moveToFirst();
wordProfile account = new wordProfile();
account.createFromDb(c);
c.close();
return account;
}
c.close(); 可以采取将cursor里的值赋值给实现Parcelable的类的成员变量,在所需要的地方相应取值便是,这样编可以做到及时关闭游标,释放资源。 (二) 构造Adapter时,没有使用缓存的 convertView以构造ListView的BaseAdapter为例,在BaseAdapter中提高了方法: public View getView(int position, View convertView, ViewGroup parent) 来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。 由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收list item的view对象的过程可以查看: android.widget.AbsListView.java –> void addScrapView(View scrap) 方法。 (三) Bitmap对象不在使用时调用recycle()释放内存有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的,视情况而定。 (四) 将变量的作用域设置为最小最小化作用域意味着对垃圾收集器更快的可见。让我们举一个例子。我们有一个变量定义在方法级,当方法执行完毕后,也就是说,控制跳出方法后,则该变量将不会被引用。这也就意味着它已经可以被垃圾回收。但是如果该变量是类级的,这时垃圾回收器就需要等待直到对该类的所有的引用都被
移除后才能进行垃圾回收。优化属性的作用域是必须的。同样这也是封装原则。最小化作用域降低了通过包访问变量并减少了耦合。 (五) 仅当你确实需要时才初始化这个的意思是在你确实需要使用对象之前才分配内存。有些时候,需要在方法的开头定义一些属性。当定义这些属性时,我们很可能在定义它们的时候对其进行初始化。 这种情况下, 如果在首次使用之前出现任何问题 (如出现异常),那么已经初始化的变量所占用的内存就被浪费了。 (六) 不要在循环中定义变量这是大部分内存溢出的程序中的另一个常见的错误。在循环中定义和初始化变量,对于循环的每次执行,该变量实例将在内存中创建并分配内存。在循环启动后,第一个对象已经可以回收了。这种额外的内存分配也会增加垃圾回收器的负担。看一下下面的代码片段你将很容易的找出问题所在。有些时候,
确实有必要在循环内定义变量,不过一定要小心使用。 12345678910111213141516171819 | import java.util.Vector;public class DeclareInLoopExample { public void getDataIncorrect() throws Exception { Vector<string> data = new Vector<string>(11);for (String str : data) {String newStr = "";// ...Some string operationsnewStr = newStr + "X" + "X";}}public void getDataCorrect() throws Exception {Vector<string> data = new Vector<string>(11);String newStr = "";for (String str : data) {// ...Some string operationsnewStr = newStr + "X" + "X";}}} |
(七) 不要在循环中定义变量这是和 String 相关的一些东西,所有对 String 进行的操作都将产生另一个 String 对象。通常我们使用的 String 链接操作,这个需要注意,它会导致一个很长的动态的查询或产生一个 toString 方法中的 String。如果我们使用+操作符来连接字符串,这些额外的对象将根据+产生。当有过多操作的时候会变成很大的开销。使用不是不变对象,如 StringBuffer 来进行字符串链接操作。 (八) 不要在循环中定义变量这会使得垃圾回收器的工作变得比较容易。 将重对象的引用设置为 null。这将释放对中对象的引用,使得垃圾回收器能立即释放它们占用的内存。 (九) 简单的使用 finally 块finally 块即使 try 或 catch 执行不成功的时候也会执行,也就是说即使在 try 或 catch 中出现了任何异常,finally 块中的语句绝对也会执行。将清理内存的语句放在这里以保证内存被清理。 (十) 分散对象创建或删除的时间
集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增 加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机 会。 其他参考资料:http://www.chinaup.org/docs/toolbox/performance.html
按照以上的编程方法去写代码也许可以使你减少内存的消耗,防止内存泄露。然而当内存被慢慢的耗尽甚至出现内存泄露后,我们该怎么办呢?下面的论述也许对你定位内存泄露的原因以及找出正确的解决办法会有所帮助: 在介绍内存查看、分析工具之前,我想有必要先介绍一下相关的概念。 为何会内存溢出 我们知道JVM根据generation(代)来进行GC,根据下图所示,一共被分为young generation(年轻代)、tenured generation(老年代)、permanent generation(永久代, perm gen),perm gen(或称Non-Heap 非堆)是个异类,稍后会讲到。注意,heap空间不包括perm gen。 绝大多数的对象都在young generation被分配,也在young generation被收回,当young generation的空间被填满,GC会进行minor collection(次回收),这次回收不涉及到heap中的其他generation,minor collection根据weak generational hypothesis(弱年代假设)来假设young generation中大量的对象都是垃圾需要回收,minor collection的过程会非常快。young generation中未被回收的对象被转移到tenured generation,然而tenured generation也会被填满,最终触发major collection(主回收),这次回收针对整个heap,由于涉及到大量对象,所以比minor collection慢得多。
JVM有三种垃圾回收器,分别是throughput collector,用来做并行young generation回收,由参数-XX:+UseParallelGC启动;concurrent low pause collector,用来做tenured generation并发回收,由参数-XX:+UseConcMarkSweepGC启动;incremental low pause collector,可以认为是默认的垃圾回收器。不建议直接使用某种垃圾回收器,最好让JVM自己决断,除非自己有足够的把握。 Heap中各generation空间是如何划分的?通过JVM的-Xmx=n参数可指定最大heap空间,而-Xms=n则是指定最小heap空间。在JVM初始化的时候,如果最小heap空间小于最大heap空间的话,如上图所示JVM会把未用到的空间标注为Virtual。除了这两个参数还有-XX:MinHeapFreeRatio=n和 -XX:MaxHeapFreeRatio=n来分别控制最大、最小的剩余空间与活动对象之比例。在32位Solaris SPARC操作系统下,默认值如下,在32位windows xp下,默认值也差不多。 参数 | 默认值 | MinHeapFreeRatio | 40 | MaxHeapFreeRatio | 70 | -Xms | 3670k | -Xmx | 64m |
由于tenured generation的major collection较慢,所以tenured generation空间小于young generation的话,会造成频繁的major collection,影响效率。Server JVM默认的young generation和tenured generation空间比例为1:2,也就是说young generation的eden和survivor空间之和是整个heap(当然不包括perm gen)的三分之一,该比例可以通过-XX:NewRatio=n参数来控制,而Client JVM默认的-XX:NewRatio是8。至于调整young generation空间大小的NewSize=n和MaxNewSize=n参数就不讲了,请参考后面的资料。 young generation中幸存的对象被转移到tenured generation,但不幸的是concurrent collector线程在这里进行major collection,而在回收任务结束前空间被耗尽了,这时将会发生Full Collections(Full GC),整个应用程序都会停止下来直到回收完成。Full GC是高负载生产环境的噩梦…… 现在来说说异类perm gen,它是JVM用来存储无法在Java语言级描述的对象,这些对象分别是类和方法数据(与class loader有关)以及interned strings(字符串驻留)。一般32位OS下perm gen默认64m,可通过参数-XX:MaxPermSize=n指定,JVM Memory Structure 一文说,对于这块区域,没有更详细的文献了,神秘。 难么为神秘会内存泄露呢?要回答这个问题又要引出另外一个话题,既什么样的对象GC才会回收?当然是GC发现通过任何reference chain(引用链)无法访问某个对象的时候,该对象即被回收。名词GC Roots正是分析这一过程的起点,例如JVM自己确保了对象的可到达性(那么JVM就是GC Roots),所以GC Roots就是这样在内存中保持对象可到达性的,一旦不可到达,即被回收。通常GC Roots是一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。所以GC Roots是分析对象为何还存活于内存中的利器。知道了什么样的对象GC才会回收后,再来学习下对象引用都包含哪些吧。 从最强到最弱,不同的引用(可到达性)级别反映了对象的生命周期。 1. Strong Ref(强引用):通常我们编写的代码都是Strong Ref,于此对应的是强可达性,只有去掉强可达,对象才被回收。 2.Soft Ref(软引用):对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有Strong Ref时才回收对象。一般可用来实现缓存,通过java.lang.ref.SoftReference类实现。 3.Weak Ref(弱引用):比Soft Ref更弱,当发现不存在Strong Ref时,立刻回收对象而不必等到内存吃紧的时候。通过java.lang.ref.WeakReference和java.util.WeakHashMap类实现。 4.Phantom Ref(虚引用):根本不会在内存中保持任何对象,你只能使用Phantom Ref本身。一般用于在进入finalize()方法后进行特殊的清理过程,通过java.lang.ref.PhantomReference实现。 有了上面的种种我相信很容易就能把heap和perm gen撑破了吧,是的利用Strong Ref,存储大量数据,直到heap撑破;利用interned strings(或者class loader加载大量的类)把perm gen撑破。 关于shallow size、retained size Shallow size就是对象本身占用内存的大小,不包含对其他对象的引用,也就是对象头加成员变量(不是成员变量的值)的总和。在32位系统上,对象头占用8字节,int占用4字节,不管成员变量(对象或数组)是否引用了其他对象(实例)或者赋值为null它始终占用4字节。故此,对于String对象实例来说,它有三个int成员(3*4=12字节)、一个char[]成员(1*4=4字节)以及一个对象头(8字节),总共3*4 +1*4+8=24字节。根据这一原则,对String a=”rosen jiang”来说,实例a的shallow size也是24字节。 Retained size是该对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。换句话说,retained size是该对象被GC之后所能回收到内存的总和。为了更好的理解retained size,不妨看个例子。 把内存中的对象看成下图中的节点,并且对象和对象之间互相引用。这里有一个特殊的节点GC Roots,正解!这就是reference chain的起点。  
从obj1入手,上图中蓝色节点代表仅仅只有通过obj1才能直接或间接访问的对象。因为可以通过GC Roots访问,所以左图的obj3不是蓝色节点;而在右图却是蓝色,因为它已经被包含在retained集合内。 所以对于左图,obj1的retained size是obj1、obj2、obj4的shallow size总和;右图的retained size是obj1、obj2、obj3、obj4的shallow size总和。obj2的retained size可以通过相同的方式计算。 Heap Dump heap dump是特定时间点,java进程的内存快照。有不同的格式来存储这些数据,总的来说包含了快照被触发时java对象和类在heap中的情况。由于快照只是一瞬间的事情,所以heap dump中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。 在不同平台和不同java版本有不同的方式获取heap dump,而MAT需要的是HPROF格式的heap dump二进制文件。想无需人工干预的话,要这样配置JVM参数:-XX:-HeapDumpOnOutOfMemoryError,当错误发生时,会自动生成heap dump,在生产环境中,只有用这种方式。如果你想自己控制什么时候生成heap dump,在Windows+JDK6环境中可利用JConsole工具,而在Linux或者Mac OS X环境下均可使用JDK5、6自带的jmap工具。当然,还可以配置JVM参数:-XX:+HeapDumpOnCtrlBreak,也就是在控制台使用Ctrl+Break键来生成heap dump。由于我是windows+JDK5,所以选择了-XX:-HeapDumpOnOutOfMemoryError这种方式,更多配置请参考MAT Wiki 。
那么下面让我们一起来看看内存泄露调试: 一开始不得不说说ClassLoader,本质上,它的工作就是把磁盘上的类文件读入内存,然后调用java.lang.ClassLoader.defineClass方法告诉系统把内存镜像处理成合法的字节码。Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。system class loader在没有指定装载器的情况下默认装载用户类,在Sun Java 1.5中既sun.misc.Launcher$AppClassLoader。更详细的内容请参看下面的资料。 (一)内存监测工具 DDMS –> Heap无论怎么小心,想完全避免bad code是不可能的,此时就需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的地方。Android tools中的DDMS就带有一个很不错的内存监测工具Heap(这里我使用eclipse的ADT插件,并以真机为例,在模拟器中的情况类似)。用Heap监测应用进程使用内存情况的步骤如下: 1. 启动eclipse后,切换到DDMS透视图,并确认Devices视图、Heap视图都是打开的; 2. 将手机通过USB链接至电脑,链接时需要确认手机是处于“USB调试”模式,而不是作为“Mass Storage”; 3. 链接成功后,在DDMS的Devices视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息; 4. 点击选中想要监测的进程,比如system_process进程; 5. 点击选中Devices视图界面中最上方一排图标中的“Update Heap”图标; 6. 点击Heap视图中的“Cause GC”按钮; 7. 此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况[如图所示]。 
说明: a) 点击“Cause GC”按钮相当于向虚拟机请求了一次gc操作; b) 当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化; c) 内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。 如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值:Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断: a) 不断的操作当前应用,同时注意观察data object的Total Size值; b) 正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平; c) 反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大, 直到到达一个上限后导致进程被kill掉。 总之,使用DDMS的Heap视图工具可以很方便的确认我们的程序是否存在内存泄漏的可能性。 (二)内存分析工具 MAT(Memory Analyzer Tool)如果使用DDMS确实发现了我们的程序中存在内存泄漏,那又如何定位到具体出现问题的代码片段,最终找到问题所在呢?如果从头到尾的分析代码逻辑,那肯定会把人逼疯,特别是在维护别人写的代码的时候。这里介绍一个极好的内存分析工具 – Memory Analyzer Tool(MAT)。 MAT是一个Eclipse插件,同时也有单独的RCP客户端。官方下载地址、MAT介绍和详细的使用教程请参见:www.eclipse.org/mat,在此不进行说明了。另外在MAT安装后的帮助文档里也有完备的使用教程。在此仅举例说明其使用方法。我自己使用的是MAT的eclipse插件,使用插件要比RCP稍微方便一些。 使用MAT进行内存分析需要几个步骤,包括:生成.hprof文件、打开MAT并导入.hprof文件、使用MAT的视图工具分析内存。以下详细介绍。 我们先来看一个例子: <pre class="brush:[java]"> /**
* Pilot class
* @author rosen jiang
*/
package org.rosenjiang.bo;
public class ithouge{ String name;
int age; public ithouge(String a, int b){
name = a;
age = b;
}
}
</pre> <pre class="brush:[java]"> /**
* OOMHeapTest class
* @author rosen jiang
*/
package org.rosenjiang.test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.rosenjiang.bo.Pilot; public class OOMHeapTest {
public static void main(String[] args){
oom();
} private static void oom(){
Map<String, ithouge> map = new HashMap<String, ithouge>();
Object[] array = new Object[1000000];
for(int i=0; i<1000000; i++){
String d = new Date().toString();
ithouge p = new ithouge(d, i);
map.put(i+”rosen jiang”, p);
array=p;
}
}
}
</pre> 接下来是MAT使用方法介绍: (一) 生成.hprof文件生成.hprof文件的方法有很多,而且Android的不同版本中生成.hprof的方式也稍有差别,我使用的版本的是2.1,各个版本中生成.prof文件的方法请参考http://android.git.kernel.org/?p=platform/dalvik.git;a=blob_plain;f=docs/heap-profiling.html;hb=HEAD 1. 打开eclipse并切换到DDMS透视图,同时确认Devices、Heap和logcat视图已经打开了; 2. 将手机设备链接到电脑,并确保使用“USB 调试”模式链接,而不是“Mass Storage“模式; 3. 链接成功后在Devices视图中就会看到设备的序列号,和设备中正在运行的部分进程; 4. 点击选中想要分析的应用的进程,在Devices视图上方的一行图标按钮中,同时选中“Update Heap”和“Dump HPROF file”两个按钮; 5. 这是DDMS工具将会自动生成当前选中进程的.hprof文件,并将其进行转换后存放在sdcard当中,如果你已经安装了MAT插件,那么此时MAT将会自动被启用,并开始对.hprof文件进行分析; 注意:第4步和第5步能够正常使用前提是我们需要有sdcard,并且当前进程有向sdcard中写入的权限(WRITE_EXTERNAL_STORAGE),否则.hprof文件不会被生成,在logcat中会显示诸如 ERROR/dalvikvm(8574): hprof: can’t open /sdcard/com.xxx.hprof-hptemp: Permission denied. 的信息。 如果我们没有sdcard,或者当前进程没有向sdcard写入的权限(如system_process),那我们可以这样做: 6. 在当前程序中,例如framework中某些代码中,可以使用android.os.Debug中的: public static void dumpHprofData(String fileName) throws IOException 方法,手动的指定.hprof文件的生成位置。例如: xxxButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { android.os.Debug.dumpHprofData(“/data/temp/myapp.hprof”); … … } } 上述代码意图是希望在xxxButton被点击的时候开始抓取内存使用信息,并保存在我们指定的位置:/data/temp/myapp.hprof,这样就没有权限的限制了,而且也无须用sdcard。但要保证/data/temp目录是存在的。这个路径可以自己定义,当然也可以写成sdcard当中的某个路径。 (二) 使用MAT导入.hprof文件1. 如果是eclipse自动生成的.hprof文件,可以使用MAT插件直接打开(可能是比较新的ADT才支持); 2. 如果eclipse自动生成的.hprof文件不能被MAT直接打开,或者是使用android.os.Debug.dumpHprofData()方法手动生成的.hprof文件,则需要将.hprof文件进行转换,转换的方法: 例如我将.hprof文件拷贝到PC上的/ANDROID_SDK/tools目录下,并输入命令hprof-conv xxx.hprof yyy.hprof,其中xxx.hprof为原始文件,yyy.hprof为转换过后的文件。转换过后的文件自动放在/ANDROID_SDK/tools目录下。OK,到此为止,.hprof文件处理完毕,可以用来分析内存泄露情况了。 3. 在Eclipse中点击Windows->Open Perspective->Other->Memory Analyzer,或者打Memory Analyzer Tool的RCP。在MAT中点击File->Open File,浏览并导入刚刚转换而得到的.hprof文件。 (三) 使用MAT的视图工具分析内存
MAT工具分析了heap dump后在界面上非常直观的展示了一个饼图,该图深色区域被怀疑有内存泄漏,可以发现整个heap才64M内存,深色区域就占了99.5%。接下来是一个简短的描述,告诉我们main线程占用了大量内存,并且明确指出system class loader加载的”java.lang.Thread”实例有内存聚集,并建议用关键字”java.lang.Thread”进行检查。所以,MAT通过简单的两句话就说明了问题所在,就算使用者没什么处理内存问题的经验。在下面还有一个”Details”链接,在点开之前不妨考虑一个问题:为何对象实例会聚集在内存中,为何存活(而未被GC)?是的——Strong Ref,那么再走近一些吧。 
点击了”Details”链接之后,除了在上一页看到的描述外,还有Shortest Paths To the Accumulation Point和Accumulated Objects部分,这里说明了从GC root到聚集点的最短路径,以及完整的reference chain。观察Accumulated Objects部分,java.util.HashMap和java.lang.Object[1000000]实例的retained heap(size)最大,在上一篇文章中我们知道retained heap代表从该类实例沿着reference chain往下所能收集到的其他类实例的shallow heap(size)总和,所以明显类实例都聚集在HashMap和Object数组中了。这里我们发现一个有趣的现象,既Object数组的shallow heap和retained heap竟然一样,通过Shallow and retained sizes 一文可知,数组的shallow heap和一般对象(非数组)不同,依赖于数组的长度和里面的元素的类型,对数组求shallow heap,也就是求数组集合内所有对象的shallow heap之和。好,再来看org.rosenjiang.bo.ithouge对象实例的shallow heap为何是16,因为对象头是8字节,成员变量int是4字节、String引用是4字节,故总共16字节。 接着往下看,来到了Accumulated Objects by Class区域,顾名思义,这里能找到被聚集的对象实例的类名。org.rosenjiang.bo.ithouge类上头条了,被实例化了290,325次,再返回去看程序,我承认是故意这么干的。还有很多有用的报告可用来协助分析问题,只是本文中的例子太简单,也用不上。以后如有用到,一定撰文详细叙述。
又是perm gen 我们在上一篇文章中知道,perm gen是个异类,里面存储了类和方法数据(与class loader有关)以及interned strings(字符串驻留)。在heap dump中没有包含太多的perm gen信息。那么我们就用这些少量的信息来解决问题吧。 看下面的代码,利用interned strings把perm gen撑破了。 <pre class="brush:[java]"> /**
* OOMPermTest class
* @author rosen jiang
*/
package org.rosenjiang.test;
public class OOMPermTest {
public static void main(String[] args){
oom();
} private static void oom(){
Object[] array = new Object[10000000];
for(int i=0; i<10000000; i++){
String d = String.valueOf(i).intern();
array=d;
}
}
}
</pre> 控制台打印如下的信息,然后把java_pid1824.hprof文件导入到MAT。其实在MAT里,看到的状况应该和“OutOfMemoryError: Java heap space”差不多(用了数组),因为heap dump并没有包含interned strings方面的任何信息。只是在这里需要强调,使用intern()方法的时候应该多加注意。 java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid1824.hprof 
Heap dump file created [121273334 bytes in 2.845 secs]
Exception in thread ”main” java.lang.OutOfMemoryError: PermGen space     
  
 
倒是在思考如何把class loader撑破废了些心思。经过尝试,发现使用ASM来动态生成类才能达到目的。ASM(http://asm.objectweb.org)的主要作用是处理已编译类(compiled class),能对已编译类进行生成、转换、分析(功能之一是实现动态代理),而且它运行起来足够的快和小巧,文档也全面,实属居家必备之良品。ASM提供了core API和tree API,前者是基于事件的方式,后者是基于对象的方式,类似于XML的SAX、DOM解析,但是使用tree API性能会有损失。既然下面要用到ASM,这里不得不啰嗦下已编译类的结构,包括: 1、修饰符(例如public、private)、类名、父类名、接口和annotation部分。 2、类成员变量声明,包括每个成员的修饰符、名字、类型和annotation。 3、方法和构造函数描述,包括修饰符、名字、返回和传入参数类型,以及annotation。当然还包括这些方法或构造函数的具体Java字节码。 4、常量池(constant pool)部分,constant pool是一个包含类中出现的数字、字符串、类型常量的数组。 
已编译类和原来的类源码区别在于,已编译类只包含类本身,内部类不会在已编译类中出现,而是生成另外一个已编译类文件;其二,已编译类中没有注释;其三,已编译类没有package和import部分。 这里还得说说已编译类对Java类型的描述,对于原始类型由单个大写字母表示,Z代表boolean、C代表char、B代表byte、S代表short、I代表int、F代表float、J代表long、D代表double;而对类类型的描述使用内部名(internal name)外加前缀L和后面的分号共同表示来表示,所谓内部名就是带全包路径的表示法,例如String的内部名是java/lang/String;对于数组类型,使用单方括号加上数据元素类型的方式描述。最后对于方法的描述,用圆括号来表示,如果返回是void用V表示,具体参考下图。  
下面的代码中会使用ASM core API,注意接口ClassVisitor是核心,FieldVisitor、MethodVisitor都是辅助接口。ClassVisitor应该按照这样的方式来调用:visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*( visitInnerClass | visitField | visitMethod )* visitEnd。就是说visit方法必须首先调用,再调用最多一次的visitSource,再调用最多一次的visitOuterClass方法,接下来再多次调用visitAnnotation和visitAttribute方法,最后是多次调用visitInnerClass、visitField和visitMethod方法。调用完后再调用visitEnd方法作为结尾。 注意ClassWriter类,该类实现了ClassVisitor接口,通过toByteArray方法可以把已编译类直接构建成二进制形式。由于我们要动态生成子类,所以这里只对ClassWriter感兴趣。首先是抽象类原型: <pre class="brush:[java]"> /**
* @author rosen jiang
* MyAbsClass class
*/
package org.rosenjiang.test;
public abstract class MyAbsClass {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
abstract int absTo(Object o);
}
</pre> 其次是自定义类加载器,实在没法,ClassLoader的defineClass方法都是protected的,要加载字节数组形式(因为toByteArray了)的类只有继承一下自己再实现。 <pre class="brush:[java]"> /**
* @author rosen jiang
* MyClassLoader class
*/
package org.rosenjiang.test;
public class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
</pre> 最后是测试类。 <pre class="brush:[java]"> /**
* @author rosen jiang
* OOMPermTest class
*/
package org.rosenjiang.test;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes; public class OOMPermTest {
public static void main(String[] args) {
OOMPermTest o = new OOMPermTest();
o.oom();
} private void oom() {
try {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT,
“org/rosenjiang/test/MyAbsClass”, null, ”java/lang/Object”,
new String[] {});
cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, ”LESS”, ”I”,
null, new Integer(-1)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, ”EQUAL”, ”I”,
null, new Integer(0)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, ”GREATER”, ”I”,
null, new Integer(1)).visitEnd();
cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, ”absTo”,
“(Ljava/lang/Object;)I”, null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray(); List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
while (true) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.defineClass(“org.rosenjiang.test.MyAbsClass”, b);
classLoaders.add(classLoader);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
</pre> 不一会儿,控制台就报错了。 java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid3023.hprof 
Heap dump file created [92593641 bytes in 2.405 secs]
Exception in thread ”main” java.lang.OutOfMemoryError: PermGen space
 
 打开java_pid3023.hprof文件,注意看下图的Classes: 88.1k和Class Loader: 87.7k部分,从这点可看出class loader加载了大量的类。 
更进一步分析,点击上图中红框线圈起来的按钮,选择Java Basics——Class Loader Explorer功能。打开后能看到下图所示的界面,第一列是class loader名字;第二列是class loader已定义类(defined classes)的个数,这里要说一下已定义类和已加载类(loaded classes)了,当需要加载类的时候,相应的class loader会首先把请求委派给父class loader,只有当父class loader加载失败后,该class loader才会自己定义并加载类,这就是Java自己的“双亲委派加载链”结构;第三列是class loader所加载的类的实例数目。 
在Class Loader Explorer这里,能发现class loader是否加载了过多的类。另外,还有Duplicate Classes功能,也能协助分析重复加载的类,在此就不再截图了,可以肯定的是MyAbsClass被重复加载了N多次。 最后 其实MAT工具非常的强大,上面故弄玄虚的范例代码根本用不上MAT的其他分析功能,所以就不再描述了。其实对于OOM不只我列举的两种溢出错误,还有多种其他错误,但我想说的是,对于perm gen,如果实在找不出问题所在,建议使用JVM的-verbose参数,该参数会在后台打印出日志,可以用来查看哪个class loader加载了什么类,例:“[Loaded org.rosenjiang.test.MyAbsClass from org.rosenjiang.test.MyClassLoader]”。 |