遇到性能问题大家都很头疼,那么遇到性能问题,如何进行分析,有没有一个通用的分析思路呢,接下来,根据个人工作中的经验,做了三种常见OOM示例,以期提供一套通用的分析思路作为参考,希望对大家有帮助。
当然,同行大牛发现文中见解有问题的话,也希望能够予以指正。
一.堆溢出
java堆是用来存放对象实例的,如果我们不断的创建对象,且保证GC Roots到对象之间可达,那么就可以避免垃圾回收机制清理这些创建的对象,因此,当对象数量到达堆的最大容量限制后就会产生内存溢出异常。下面我们来模拟这种异常。(1)配置jvm启动参数,run as –> run configration 里可以配置。
下面几个参数的意思是:-Xms5m -Xmx5m :堆的最大最小值都是5m,避免自动扩展。-XX:+HeapDumpOnOutOfMemoryError :让虚拟机在出现堆溢出时自动dump当前内存堆转储快照,以便进行分析。-XX:+PrintGCDetails -XX:+PrintGCTimeStamps 打印gc信息便于我们分析。
(2)准备示例代码:
public class MemoryleaksDemo {
public static void main(String[] args) throws InterruptedException {
MemoryleaksDemo mem =new MemoryleaksDemo();mem.BadExample();
}
public void BadExample() throws InterruptedException {
//内存泄露:o对象一直被v引用,gc无法清理掉,造成泄露
Vector v=new Vector(10);
while(true)
{
Object o=new Object();
v.add(o);
o=null;
}
}
}
(3)运行结果:java.lang.OutOfMemoryError: Java heap space,且自动生成了dump文件java_pid5043.hprof,在工程根目录下。
分析:
分析dump文件
(1)打开dump文件,看到Overview中total使用了3.4M,其中一块内存占用了3.1M,点击Leak Suspects
(2)可以看到第一个问题,存在一个超大的java.lang.Object[]对象,点击Details
(3)可以看到有163840个Object对象,正是我们示例代码中新建的大量的object对象。
(4)为了验证我们的猜想,切换到Histogram 视图,按照Retained Heap由大到小排序,可以看到java.lang.Object 对象的数目很多,而且占用的空间也较大。
5)右键选择 Merge Shortest Paths to gc Roots->exclude all phantom/weak/soft etc.references ,查看该类的GC root(去除虚引用&软引用)
(6)可以看到有14条引用路径,其中第一个占用的内存最大,我们展开折叠项,发现了大量java.lang.Object 对象的引用,验证了上面的推测。
二.线程栈溢出
设置线程栈的jvm参数是-Xss,该参数规定了每个线程堆栈的大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一 个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
栈(JVM Stack)存放的主要是栈帧( 局部变量表, 操作数栈 , 动态链接 , 方法出口信息 ),栈包含栈帧。如果抛出java.lang.StackOverflowError错误,一般出现此情况是因为方法运行时,请求新建栈帧,但是栈所剩空间小于战帧所需空间。例如,通过递归调用方法,不停的产生栈帧,一直把栈空间堆满,直到抛出异常。下面我们使用递归调用模拟栈溢出。
(1)配置jvm启动参数,run as –> run configration 里可以配置。
设置-Xss1m,堆5m,且在OOM时生产dump文件。
(2)栈溢出代码示例:
public class StackOverflow {
public void stackOverFlowMethod(){
stackOverFlowMethod();
}
/**
* 通过递归调用方法,不停的产生栈帧,一直把栈空间堆满,直到抛出异常
*/
public static void main(String[] args) {
StackOverflow sof = new StackOverflow();
sof.stackOverFlowMethod();
}
}
(3)运行结果:
Exception in thread "main" java.lang.StackOverflowError。注意栈溢出并没有生成dump文件,虽然我们设置了
-XX:+HeapDumpOnOutOfMemoryError 参数。
三.方法区溢出
方法区是所有线程共享的,主要用于存储类信息、常量池、方法数据、代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
从JDK1.8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是Metaspace。Metaspace使用的是本地内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。对于该区域的测试,基本思路是运行时产生大量的类去填满方法区,直到溢出。
cglib原理:动态生成一个要代理类的子类,子类重写目标代理类的所有非final方法,在子类中拦截所有父类方法的调用,顺势织入横切逻辑。
(1)配置jvm参数
-Xms512m -Xmx512m -XX:MaxMetaspaceSize=1000m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-XX:MaxMetaspaceSize=1000m这个参数用于限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。当然metaspace还有其他相关参数,但是本例子不再过多阐述。
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
public class JavaMethodAreaOOM {
public static void main(String[] args) {
// TODO Auto-generated method stub
while(true) {
//创建加强器
Enhancer enhancer = new Enhancer();
//为加强器指定要代理的业务类
enhancer.setSuperclass(OOMObject.class);
//设置回调
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
//被代理的目标类
static class OOMObject{
}
}
(3)运行结果:
java.lang.OutOfMemoryError: Metaspace
分析:
(1)通过jvisualvm工具监控,可以很清晰的看到metaspace空间一直在增长,最后达到1000m,即我们设置的最大值,装载类的数量也持续增加,从1万多增加到12万多。
(2)查看报错信息是在at org.springframework.cglib.core.AbstractClassGenerator.create处发生异常。
(3)打开dump文件的Histogram视图,
搜索org.springframework.cglib.core.AbstractClassGenerator,
发现相关的类org.springframework.cglib.core.AbstractClassGenerator$Source有3个对象。
(4)选中该class,右键List objects->with incoming references
(5)发现这个对象持有大量classloader 对象的引用。
例如<classloader>bestpay.com.cn.Testutil.JavaMethodAreaOOM$OOMObject$$EnhancerByCGLIB$$16725992_210,后面$$16725992_210都不一样,由此猜测这些class非同一代理类生成,这样就能理解为什么metaspace会增加,因为他们的class不同。
四.常量池溢出
在jdk1.8后,将常量池放到了堆中,所以对于常量池的溢出不会再报java.lang.OutOfMemoryError: Metaspace。
class文件信息和动态常量池存储在方法区,class信息包括类信息和静态常量池。动态常量池里的内容是可以动态添加的,例如调用String的intern方法就能将string的值添加到String常量池中,8种基本数据类型(除了double和float)都使用了常量池。
下面我们以String的intern方法来模拟常量池溢出。
(1)jvm参数设置为:
-Xms500m -Xmx500m -XX:MaxMetaspaceSize=20m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-XX:MaxMetaspaceSize=20m Metaspace最大设置20m。
(2)准备示例代码
import java.util.ArrayList;
import java.util.List;
public class StringOomTest {
public void test() {
List<String> list=new ArrayList<String>();int i=0;
String str1=null;
while(true) {
i=i+5;
list.add(String.valueOf(i).intern());
}
}
public static void main(String [] args){
StringOomTest str=new StringOomTest();
str.test();
}
}
(3)运行结果
java.lang.OutOfMemoryError: GC overhead limit exceeded。
发生该错误,是程序基本上耗尽了所有的可用内存,GC也清理不了,于是jvm发出了这样的信号: 执行垃圾收集的时间比例太大,有效的运算量太小。默认情况下,如果GC花费的时间超过98%, 并且GC回收的内存少于2%, JVM就会抛出这个错误。
分析
通过jvisualvm工具实时监控
(1)可以看到已使用的metaspace大小从2.7m增加到8.7m左右后就不再增加了,并没有一直增加到设置的上限20m,也证实了生成的string常量不在metaspace空间中存在,而是在堆中存放。
(2)堆的大小从327m增加到479m后基本趋于稳定,不再增加,基本接近最大值500m,GC活动频繁。
(3)打开dump文件,点击Leak Suspects
(4)可以看到这个java.lang.Object[]数组占的空间特别大。
拉到最后,发现有7,877,381个java.lang.String对象。
(5)打开dominator视图,发现main thread占了99.74%的内存
展开java.lang.Thread,看到java.util.ArrayList对象,继续展开,看到object数组,下面有7,877,381个string对象,与overview中一致,正是示例代码新建的string对象。
(6)展开string对象,可以看到里面是一个char数组,存放的是int类型的数字。
本次分享就到这里,下次见。
作者简介:
隶属于甜橙金融质量平台性能团队,资深性能工程师,专注于性能领域,为性能问题的发现、分析、解决提供解决方案,以提高系统性能为己任。