JVM内存划分及OOM分析

Java内存区域与内存溢出异常

运行时数据区域

Java在运行时会把它所管理的内存划分为若干个不同的数据区域。

方法区(Method Area)

  • 线程共享
  • 存储
    • 类信息
    • 常量
    • 静态变量
    • 即时编译器编译后的代码
  • 非堆
  • 不需要连续的内存
  • 可以选择固定或可扩展
  • 可选择不实现垃圾收集
    • 针对常量池的回收
      • 运行时常量池(Runtime Constant Pool)
        1. 常量池:存放编译期生成的字面常量和符号引用
        2. 这些常量池中的数据进入方法区后存放在运行时常量池中
        3. 动态性:运行期可将新的常量放入池中,例如:String.intern()方法
    • 对类型的卸载
  • 无法满足内存分配需求时会抛出OutOfMemoryError

    虚拟机栈(VM Stack)

  • 线程私有
  • 生命周期与线程相同
  • 描述Java方法执行的内存模型
    • 栈帧
      • 存储
        • 局部变量表:基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)
          • 64位的long 和double会占用两个局部变量空间,其余的只占一个
          • 编译期完成内存分配
          • 方法运行期间不会改变局部变量表的大小
            • 操作数栈
            • 动态链接
            • 方法出口
            • 入栈
            • 出栈
  • 线程请求的栈深度大于虚拟机允许的深度会抛出StackOverFlowError
  • 如果扩展时无法申请到足够内存会抛出OutOfMemoryError

    本地方法栈(Native Method Stack)

  • 为本地方法(Native Method)服务
    • <--> 区别于虚拟机栈:为虚拟机执行Java方法服务。
  • 抛出StackOverFlowError
  • 抛出OutOfMemoryError

    堆(Heap)

  • Java虚拟机所管理的内存中最大的一块
  • 线程共享
  • 虚拟机启动时创建
  • 存放对象实例及数组
  • 垃圾回收管理(GC)主要区域
    • 分代算法
      • 新生代
        • eden
        • from
        • to
        • 老年代
  • 可划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
  • 可处于物理上不连续的内存空间,保证逻辑连续即可
  • 可通过-Xmx和-Xms控制大小(可扩展)
  • 堆无法再扩展时会抛出OutOfMemoryError

    程序计数器(Program Counter Register)

  • 较小内存空间
  • 当前线程的行号指示器
  • 线程私有的内存
  • 唯一一个没有OutOfMemoryError的区域

    直接内存(Direct Memory)

  • NIO中使用基于通道(Channel)与缓冲区(Buffer)的I/O方式,使用Native函数库直接分配的堆外内存
  • 无法满足内存分配需求时会抛出OutOfMemoryError

    对象的创建

  • 类加载检查机制
  • 为新生对象分配内存
    • 把一块确定大小的内存从堆中划分出来。
      • 内存规整: 指针碰撞(Bump the Point)
      • 内存不规整 空闲列表(Free List):维护列表并记录哪些是可用的。
        • 选择方式由Java堆是否规整决定
        • Java堆是否规整由垃圾收集器是否带有压缩整理功能决定。
        • 由于对象创建是非常频繁的,则在并发情况下不是线程安全的
          • 对分配内存空间的动作进行同步处理
          • 把内存分配的动作按照线程划分在不同的空间中,即TLAB方式。
            • 只有在TLAB用完并分配新TLAB的情况下需要同步锁定
            • 可以使用-XX:+/-UseTLAB来设定
  • 将分配到的内存空间初始化为0(不包含对象头)
  • 对对象进行必要的设置
    • 属于哪个类的实例
    • 如何找到类的元数据信息
    • 对象的哈希值
    • 对象的GC分代年龄
    • 这些数据存放在对象头(Object Header)中
  • 对象得到创建,但是所有的字段为零,在执行new之后会立即执行方法

    对象的内存布局

  • 对象头(Object Header)
    • 存储对象自身运行时数据(Mark Word),通常在32位和64位的机器上占用32bit和64bit的空间
      • 哈希吗
      • GC分代年龄
      • 锁状态标志
      • 线程持有锁
      • 偏向线程ID
      • 偏向时间戳
        • 类型指针: 对象指向它的类元数据的指针
        • 虚拟机通过这个指针来确定这个对象属于哪个类
        • 不是所有的对象实例都保留有类型指针
        • 如果对象是数组,还必须有一块用于记录数组长度的数据
  • 实例数据(Instance Data),真正存储的数据
    • 存储顺序受到虚拟机分配策略参数(Fields Allocation Style)和字段在Java源码中的定义顺序影响
  • 对齐填充(Padding)
    • 不是必要存在的
    • 仅仅起占位作用

      对象访问定位

  • Java通过栈上的引用(reference)数据来操作堆上的具体对象

    句柄访问

    • 堆中划分一块内存作为句柄池
    • reference中存储对象的句柄地址
    • 句柄中包含对象实例数据与类型数据各自的具体地址信息
    • 优点:reference中存储稳定的句柄地址,在对象移动时只会改变句柄中的实例数据指针

      直接指针

      • 考虑如何放置访问类型数据的相关信息
      • reference中直接存储对象地址
      • 优点:速度更快,节省一次指针定位的时间

        内存溢出测试(OutOfMemoryError)

        Java堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
System.out.println("test");
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}

结果

1
2
3
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java _pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]

分析

  • 内存泄露
    • 导致GC无法自动回收
    • 分析与GC Roots 相关联的路径
  • 内存溢出
    • 检查虚拟机堆参数(-Xmx与-Xms)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* VM Args: -Xss128k
*/
public class StackOverFlow {
private int stackLength = 1;
public void stackLeak(){
stackLength ++;
stackLeak();
}

public static void main(String[] args) {
StackOverFlow oom = new StackOverFlow();
try{
oom.stackLeak();
}catch (Throwable e){
System.out.println("stack length:"+oom.stackLength);
throw e;
}
}
}

结果

1
2
3
4
5
6
7
Exception in thread "main" java.lang.StackOverflowError
stack length:2271
at com.laowang.vm.StackOverFlow.stackLeak(StackOverFlow.java:9)
at com.laowang.vm.StackOverFlow.stackLeak(StackOverFlow.java:10)
at com.laowang.vm.StackOverFlow.stackLeak(StackOverFlow.java:10)
at com.laowang.vm.StackOverFlow.stackLeak(StackOverFlow.java:10)
以下省略.....

方法区和运行时常量溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* VM Args -XX:PermSize=10M -XX:MaxPermSize=10M
* jdk1.6以前的版本,常量池分配在永久代,可以使用以上的参数来限值方法区的大小。
* jdk1.7以后逐步 ”去永久代“
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<>();
int i = 0;
while (true){
//intern()方法的作用是如果常量池中有则返回,如果没有则放入常量池再返回
list.add(String.valueOf(i++).intern());
}
}
}

结果

1
2
3
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native method)
at com.laowang.vm.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:10)

引申:String.intern()返回引用测试

1
2
3
4
5
6
7
8
public class StringIntern {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}

结果

1
2
jdk1.6:false false
jdk1.7: true false

分析

  • jdk1.6:

    • intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中实例的引用
    • StringBuilder创建的字符串实例在堆上
    • 不是同一个引用,所以都是false
  • jdk1.7:

    • intern()方法不会复制实例,只是在常量池中记录首次出现的实例引用
    • 是同一个引用

      方法区OOM代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      /**
      * VM Args -XX:PermSize=10M -XX:MaxPermSize=10M
      * 同样在jdk1.7以后没有效果
      * 在jdk1.6以前会出现OOM
      */
      public class JavaMethodAreaOOM {
      public static void main(final 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(objects,args);
      }
      });
      enhancer.create();
      }
      }
      static class OOMObject{
      }
      }

结果

1
2
3
4
5
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
... 8 more

本机直接内存溢出

  • 直接内存可以使用 -XX:MaxDirectMemorySize 指定
  • 默认与Java堆最大值( -Xms 指定)一样
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
    */
    public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

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

结果

1
2
3
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.laowang.vm.DirectMemoryOOM.main(DirectMemoryOOM.java:18)