前言
在平时工作过程中,有时会遇到
OutOfMemoryError
,我们知道遇到
Error
一般表明程序存在着严重问题,可能是灾难性的。所以找出是什么原因造成
OutOfMemoryError
非常重要。现在向大家引荐
Eclipse Memory Analyzer tool(MAT)
,来化解我们遇到的难题。如未说明,本文均使用
Java 5.0 on Windows XP SP3
环境。
为什么用
MAT
之前的观点,我认为使用实时
profiling/monitoring
之类的工具,用一种非常实时的方式来分析哪里存在内存泄漏是很正确的。年初使用了某
profiler
工具测试消息中间件中存在的内存泄漏,发现在吞吐量很高的时候
profiler
工具自己也无法响应,这让人很头痛。后来了解到这样的工具本身就要消耗性能,且在某些条件下还发现不了泄漏。所以,分析离线数据就非常重要了,
MAT
正是这样一款工具。
为何会内存溢出
我们知道
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(
弱年代假设
)
来假设
younggeneration
中大量的对象都是垃圾需要回收,
minor collection
的过程会非常快。
young generation
中
未被回收的对象被转移到
tenured generation
,然而
tenured generation
也会被填满,最终触发
major collection(
主回收
)
,这次回收针对整个
heap
,由于涉及到大量对象,所以比
minor collection
慢得多。
JVM
有三种垃圾回收器,
1.分别是
throughputcollector
,用来做并行
young generation
回收,由参数
-XX:+UseParallelGC
启动;
2.
concurrent low pause collector
,用来做
tenuredgeneration
并发回收,由参数
-XX:+UseConcMarkSweepGC
启动;
3.
incremental lowpause collector
,可以认为是默认的垃圾回收器。不建议直接使用某种垃圾回收器,最好让
JVM
自己决断,除非自己有足够的把握。
Heap
中各
generation
空间是如何划分的?
通过
JVM
的
-Xmx=n
参数可指定max最大
heap
空间,
-Xms=n则是指定min
最小
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
中幸存的对象被转移到
tenuredgeneration
,但不幸的是
concurrent collector
线程在这里进行
major collection
,而在回收任务结束前空间被耗尽了,这时将会发生
Full Collections(Full GC)
,整个应用程序都会停止下来直到回收完成。
FullGC
是高负载生产环境的噩梦
……
现在来说说异类
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
才会回收后,再来学习下对象引用都包含哪些吧。
从最强到最弱,不同的引用(可到达性)级别反映了对象的生命周期。
lStrong Ref(
强引用
)
:通常我们编写的代码都是
Strong Ref
,于此对应的是强可达性,只有去掉强可达,对象才被回收。
lSoft Ref(
软引用
)
:对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有
Strong Ref
时才回收对象。一般可用来实现缓存,通过
java.lang.ref.SoftReference
类实现。
lWeak Ref(
弱引用
)
:比
Soft Ref
更弱,当发现不存在
Strong Ref
时,立刻回收对象而不必等到内存吃紧的时候。通过
java.lang.ref.WeakReference
和
java.util.WeakHashMap
类实现。
lPhantom 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
总和。
Heap Dump
heap dump
是特定时间点,
java
进程的内存快照。有不同的格式来存储这些数据,总的来说包含了快照被触发时
java
对象和类在
heap
中的情况。由于
快照只是一瞬间的事情,所以
heap dump
中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。
在不同平台和不同
java
版本有不同的方式获取
heap dump
,而
MAT
需要的是
HPROF
格式的
heap dump
二进制文件。想无需人工干预的话,要这样配置
JVM
参数:
-XX:-HeapDumpOnOutOfMemoryError
,当错误发生时,会自动生成
heapdump
,在生产环境中,只有用这种方式。如果你想自己控制什么时候生成
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
。
参考资料
MAT Wiki
Interned Strings
Strong,Soft,Weak,Phantom Reference
Tuning GarbageCollection with the 5.0 Java[tm] Virtual Machine
Permanent Generation
UnderstandingWeak References
译文
JavaHotSpot VM Options
Shallow and retained sizes
JVM Memory Structure
GC roots
请注意!引用、转贴本文应注明原作者:Rosen Jiang 以及出处:
http://www.blogjava.net/rosen
----
使用Memory Analyzer tool(MAT)分析内存泄漏(二)
前言
在
使用Memory Analyzer tool(MAT)分析内存泄漏(一)
中,我介绍了内存泄漏的前因后果。在本文中,将介绍MAT如何根据heap dump分析泄漏根源。由于测试范例可能过于简单,很容易找出问题,但我期待借此举一反三。
一开始不得不说说ClassLoader,本质上,它的工作就是把磁盘上的类文件读入内存,然后调用java.lang.ClassLoader.defineClass方法
告诉系统把内存镜像处理成
合法的字节码。Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。system class loader在没有指定装载器的情况下默认装载用户类,在Sun Java 1.5中既sun.misc.Launcher$AppClassLoader。更详细的内容请参看下面的资料。
准备heap dump
请看下面的Pilot类,没啥特殊的。
/**
* Pilot class
*
@author
rosen jiang
*/
package
org.rosenjiang.bo;
public
class
Pilot{
String name;
int
age;
public
Pilot(String a,
int
b){
name = a;
age = b;
}
}
然后再看OOMHeapTest类,它是如何撑破heap dump的。
/**
* 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 map =
new
HashMap();
Object[] array =
new
Object[1000000];
for
(
int
i=0; i<1000000; i++){
String d =
new
Date().toString();
Pilot p =
new
Pilot(d, i);
map.put(i+"rosen jiang", p);
array[i]=p;
}
}
}
是的,上面构造了很多的Pilot类实例,向数组和map中放。由于是Strong Ref,GC自然不会回收这些对象,一直放在heap中直到溢出。当然在运行前,先要在Eclipse中配置VM参数-XX:+HeapDumpOnOutOfMemoryError。好了,一会儿功夫内存溢出,控制台打出如下信息。
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3600.hprof
Heap dump file created
[
78233961 bytes in 1.995 secs
]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
java_pid3600.hprof既是heap dump,可以在OOMHeapTest类所在的工程根目录下找到。
MAT安装
话分两头说,有了heap dump还得安装MAT。可以在http://www.eclipse.org/mat/downloads.php选择合适的方式安装。安装完成后切换到Memory Analyzer视图。在Eclipse的左上角有Open Heap Dump按钮,按照刚才说的路径找到java_pid3600.hprof文件并打开。解析hprof文件会花些时间,然后会弹出向导,直接Finish即可。稍后会看到下图所示的界面。
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.
Pilot对象实例的shallow heap为何是16,因为对象头是8字节,成员变量int是4字节、String引用是4字节,故总共16字节。
public
class
Pilot{
String name;
int
age;
public
Pilot(String a,
int
b){
name = a;
age = b;
}
}
接着往下看,来到了Accumulated Objects by Class区域,顾名思义,这里能找到被聚集的对象实例的类名。org.rosenjiang.bo.
Pilot类上头条了,被实例化了290,325次
,再返回去看程序,我承认是故意这么干的。还有很多有用的报告可用来协助分析问题,只是本文中的例子太简单,也用不上。以后如有用到,一定撰文详细叙述。
又是perm gen
我们在上一篇文章中知道,perm gen是个异类,里面存储了类和方法数据(与class loader有关)以及interned strings(字符串驻留)。在heap dump中没有包含太多的perm gen信息。那么我们就用这些少量的信息来解决问题吧。
看下面的代码,利用interned strings把perm gen撑破了。
/**
* 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[i]=d;
}
}
}
控制台打印如下的信息,然后把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感兴趣。首先是抽象类原型:
/**
*
@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);
}
其次是自定义类加载器,实在没法,ClassLoader的defineClass方法都是protected的,要加载字节数组形式(因为toByteArray了)的类只有继承一下自己再实现。
/**
*
@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);
}
}
最后是测试类。
/**
*
@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 classLoaders =
new
ArrayList();
while
(
true
) {
MyClassLoader classLoader =
new
MyClassLoader();
classLoader.defineClass("org.rosenjiang.test.MyAbsClass", b);
classLoaders.add(classLoader);
}
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
不一会儿,控制台就报错了。
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]”。
全文完。