前言

本篇博文主要介绍使用程序触发对应的内存溢出,并附带上JVM常用的命令,供以后查看使用。

堆溢出

堆主要是用来存储对象,我们只要不断的创建对象,并防止虚拟机对对象进行回收则可以触发堆溢出。

  1. -Xms设置堆最小值、-Xmx设置堆最大值。如果两者相同,则可以避免堆自动扩展;
  2. -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析
/**
 * Title:HeapOom 堆溢出
 * Description: VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 *
 * @author lin.xu
 * @date 2017/12/4.
 */
public class HeapOom {

  static class OomObject {

  }

  public static void main(String[] args) {
    List<OomObject> list = new ArrayList<OomObject>();

    while (true) {
      list.add(new OomObject());
    }
  }
}

执行过后,会提示java.lang.OutOfMemoryError: Java heap space

解决方法:

通过内存映像分析工具(如Eclipse Memory Analyzer、JProfiler)对Dump出来的堆转储快照进行分析,重点先确定是内存泄漏还是内存溢出。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,掌握泄漏对象的类型信息及GC Roots引用链信息就可以定位泄漏代码位置;如果不是内存泄漏,则可以检查-Xms-Xmx参数设置,另外查看下代码判断对象生命周期是否过长。

虚拟机栈和本地方法栈溢出

栈内存容量的计算由操作系统为虚拟机分配的内存,扣除最大堆内存和方法区占用的最大内存之后所剩余的,由于程序计数器占用内存小可以忽略不计。

  1. -Xss设置虚拟机栈大小,-Xoss设置本地方法栈大小。但是对于HotSpot来说,-Xoss是无效的,因为HotSpot中根本不区分本地方法栈;
/**
 * Title:JavaVmStackSof 栈溢出
 * Description: -Xss180k
 *
 * @author lin.xu
 * @date 2017/12/4.
 */
public class JavaVmStackSof {
  private int stackLength = 1;

  public void stackLeak() {
    stackLength++;
    stackLeak();
  }

  public static void main(String[] args) {
    JavaVmStackSof jvss = new JavaVmStackSof();
    try {
      jvss.stackLeak();
    } catch (Throwable e) {
      System.out.println("Stack size: " + jvss.stackLength);
      throw e;
    }
  }
}

执行过后,会提示java.lang.StackOverflowError

解决方法:

一般可以依据抛出的异常直接定位到代码的位置。如果是由于过多线程导致的内存溢出,在不能减少线程数的情况下,只能减少最大堆和减少栈容量来换取更多的线程。

方法区和运行时常量池溢出

JDK1.7及之前版本,采用PermSizeMaxPermSize来设置方法区大小,由于JDK1.7开始已经在“去永久代”的实现,在JDK9中如果采用上述两个配置运行本节的示例则直接提示

Ignoring option PermSize; support was removed in 8.0。
Ignoring option MaxPermSize; support was removed in 8.0

JDK9中需要将PermSize替换为MetaspaceSizeMaxPermSize替换为MaxMetaspaceSize

方法区主要用于存放Class相关的信息,如类名、访问修饰符、常量池、字段描述及方法描述等。测试时主要就是加载大量的类来使方法区溢出。

/**
 * Title:JavaMethodAreaOom
 * Description:vm args: -XX:PermSize=10M -XX:MaxPermSize=10M
 *
 * @author lin.xu
 * @date 2017/12/5.
 */
public class JavaMethodAreaOom {

  public static void main(String[] args) {
    while (true) {
      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(OomObject.class);
      enhancer.setUseCache(false);
      enhancer.setCallback(new MethodInterceptor() {
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
            throws Throwable {
          return methodProxy.invokeSuper(o, objects);
        }
      });
      enhancer.create();
    }
  }
}

执行过后,会提示java.lang.OutOfMemoryError: PermGen space

本机直接内存溢出

DirectMemory容量可以通过-XX: MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值相同。

/**
 * Title:DirectMemoryOom
 * Description:vm args: -Xmx10M/-XX:MaxDirectMemorySize=10M
 *
 * @author lin.xu
 * @date 2017/12/5.
 */
public class DirectMemoryOom {
  private static final int _1MB = 1024 * 1024;

  public static void main(String[] args) throws IllegalAccessException {
    Field field = Unsafe.class.getDeclaredFields()[0];
    field.setAccessible(true);
    Unsafe unsafe = (Unsafe) field.get(null);
    while (true) {
      unsafe.allocateMemory(_1MB);
    }
  }
}

执行过后,会提示java.lang.OutOfMemoryError

由DirectMemory导致的内存溢出,明显的特征就是HeapDump文件中不会看见明显的异常。如果发现OOM之后Dump文件很小,而程序中又间接或直接使用了NIO,则可以考虑检查下是不是本机直接内存溢出问题。

JVM Args常用命令列表

命令

说明

-Xms20M

设置Java堆最小内存为20M,必须以M为单位

-Xmx20M

设置Java堆最大内存为20M,必须以M为单位。如果值与-Xms设置的相同,则Java堆不能扩展

-Xss128k

设置JVM栈内存大小为128K。在JDK9中,最小必须设置为180k

-Xoss128k

设置本地方法栈大小为128k。不过在HotSpot虚拟机中,本参数无效。因为HotSpot中不区分JVM栈和本地方法栈

-XX:PermSize=10M

JVM初始分配的永久代容量,必须以M为单位。JDK8过后已无效。

-XX:MaxPermSize=10M

JVM允许分配的永久代最大容量,必须以M为单位。JDK8过后已无效

-Xnoclassgc

关闭JVM对类的垃圾回收

-XX:+TraceClassLoading

查看类的加载信息

-XX:+TraceClassUnLoading

查看类的卸载信息

-XX:NewRatio=4

年轻代:老年代=1:4

-XX:SurvivorRatio=8

2个Survivor:1个Eden=2:8

-Xmn20M

年轻代大小为20M

-XX:+HeapDumpOnOutOfMemoryError

程序抛出异常时,存储堆内存转储快照

-XX:+UseG1GC

JVM使用G1垃圾收集器

-XX:+PrintGCDetails

在控制台上打印出GC具体细节

-XX:+PrintGC

在控制台上打印出GC信息

-XX:PretenureSizeThreshold=3145728

对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位

-XX:MaxTenuringThreshold=1

对象年龄大于1,则自动进入老年代

-XX:CompileThreshold=1000

当一个方法被调用1000次之后,被认为是热点代码,触发即时编译

-XX:+PrintHeapAtGC

查看每次GC前后堆内存布局

-XX:+/-UseTLAB

虚拟机是否启动TLAB

-XX:+PrintTLAB

查看TLAB的使用情况

-XX:+UseSpining

开启自旋锁

-XX:+PreBlockSpin

更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁