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

C/C++ 手动管理内存

Java 由 JVM 自动管理内存

运行时数据区域

JVM 在执行 JAVA 程序过程中将其管理的内存划分为若干不同的数据区域

image-UIsK.png

程序计数器

记录指令的位置,改变计数器执行下一条指令,实现:分支、循环、跳转、异常处理、线程恢复等功能。

内存空间较小,每个线程有独立的程序计数器,不会出现 OutOfMemoryError 情况的区域。

虚拟机栈

是线程内存模型:栈帧(局部变量表、操作数栈、动态链接、方法出口),方法从调用到结束对应着虚拟机栈的入栈和出栈。(第 8 章介绍栈帧结构)

线程请求的栈深度超过虚拟机允许的深度,StackOverFlowError;

线程中栈扩展到无法申请到足够内存时,OutOfMemoryError。

本地方法栈

虚拟机栈为虚拟机执行 Java 方法,本地方法栈为虚拟机执行本地方法。

同虚拟机栈一样,有 StackOverFlowError、OutOfMemoryError。

唯一目的:存放对象的实例

所有线程共享,是虚拟机中占用内存最大的一块,同时也是垃圾收集器管理的内存区域

通过参数 -Xmx 和 -Xms 扩展堆的内存大小

如果堆中没有内存完成对象实例的分配,并且堆无法扩展时,OutOfMemoryError。

方法区

存储被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等数据

对该区域的垃圾收集主要目标是常量池回收和类型卸载。

如果该区域无法满足新的内存分配需求,OutOfMemoryError

运行时常量池

是方法区的一部分,存放编译期生成的字面量和符号引用,也有符号引用翻译出来的直接引用

在 Class 文件中包含:类的版本、字段、方法、接口等描述信息、常量池表

常量池的内存不够了,OutOfMemoryError

直接内存

不是虚拟机运行时数据区域中的内存。

JDK1.4中的NIO类,引入通道(Channel)+缓冲区(Buffer)的IO方式,使用Native函数库直接分配堆外内存,通过Java堆中的DirectByteBuffer对象对这块内存的引用进行操作。

受本机总内存(包括:物理内存、SWAP分区或分页文件)大小和处理器寻址空间的限制,OutOfMemoryError

HotSpot虚拟机对象

对象创建

  1. 类加载

    1. JVM 执行 New 指令时,在常量池中定位类的符号引用。如果符号引用代表的类没有被加载、解析、初始化,执行类加载过程。

    2. 类加载后,对象需要的内存大小就确定了。

  2. 内存分配

    1. Java 堆内存绝对规整的情况下,采用“指针碰撞”(只移动指针就可以分配内存了)的方式进行分配

      1. Java 堆是否规整,由垃圾收集器是否带有空间压缩整理能力决定:Serial、ParNew收集器带有此功能

    2. Java堆内存并不规整的情况下,采用“空闲列表”(虚拟机维护一张表,表里记录哪些内存块可以使用)的方式进行分配

      1. CMS 是基于清除(Sweep)算法的收集器,没有空间压缩整理能力

  3. 内存空间初始化

    1. 并发情况下分配内存是不安全的,指针的位置可能不能预测,有以下两种方式解决:

      1. CAS 配上失败重试

      2. 每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲,TLAB),哪个线程分配内存,先在该线程的内存缓冲区中分配,空间不够的才进行同步

        1. -XX:+/-UseTLAB 参数设定使用第二种方式

  4. 对象设置

    1. 将分配到的内存空间的初始化为0值,不包括对象头

    2. 采用TLAB方式分配内存的,可以在分配时就设置内存空间的初始化值

  5. 对象初始化

    1. 设置对象头:哪个类的实例、如何找到类的元数据信息、对象哈希码、对象GC分代年龄信息

对象内存布局

对象在堆内存中的存储布局:对象头(Header)、实例数据(Instance Data)、对齐填充(padding)

  • 对象头:包括两类信息

    • 存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

    • 类型指针:对象指向它的类型元数据的指针,通过该指针知道该对象是谁的实例。如果对象是数组还会记录数组长度信息

  • 实例数据:对象真正存储的有效信息,程序代码中定义的字段内容

    • 虚拟机分配策略参数 -XX: FieldsAllocationStyle:影响存储的顺序,默认分配顺序 longs/doubles、ints、shorts/chars、bytes/boolean、oops

    • +XX:CompactFields(默认为 true),子类中较窄的变量插入父类变量的空隙中,节省空间

  • 对齐填充:占位符

    • HotSpot 虚拟机内存自动管理系统要求对象的起始地址必须是8字节的整数倍,任何对象大小都是8字节的整数倍

对象的访问定位

Java 程序通过栈上的 reference 数据操作堆上的具体对象。reference 类型是一个指向对象的引用,一般有两种实现:

  • 句柄访问

    • 通过句柄访问对象,如图:

    • 优点:句柄池地址稳定,对象被移动时(GC)只用改变句柄中实例数据的指针,reference本身不用修改。

  • 直接指针访问

    • 通过直接指针访问对象,如图:

    • 优点:速度快,节省了指针定位的时间开销,HotSpot 采用该方式访问对象

实战:OutOfMemoryError

堆内存溢出

  • 配置

    虚拟机启动配置
    -Xms20M
    -Xmx20M
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=D:\workspace\JAVA\JVM\src\内存管理\内存溢出\堆内存溢出.hprof
    image-wTuI.png
  • 代码

    堆内存溢出代码
    package 内存管理.内存溢出;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class 堆内存溢出 {
        static class OOMObject {}
        public static void main(String[] args) {
            List<OOMObject> list = new ArrayList<>();
            while (true) {
                list.add(new OOMObject());
            }
        }
    }
  • 异常

    堆溢出异常
    java.lang.OutOfMemoryError: Java heap space
    Dumping heap to D:\workspace\JAVA\JVM\src\�ڴ����\�ڴ����\���ڴ����.hprof ...
    Heap dump file created [28370799 bytes in 0.057 secs]
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    	at java.util.Arrays.copyOf(Arrays.java:3210)
    	at java.util.Arrays.copyOf(Arrays.java:3181)
    	at java.util.ArrayList.grow(ArrayList.java:267)
    	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
    	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
    	at java.util.ArrayList.add(ArrayList.java:464)
    	at 内存管理.内存溢出.堆内存溢出.main(堆内存溢出.java:11)
    
    进程已结束,退出代码为 1
  • 文件

    内存堆转储快照文件
  • 分析

    • 通过内存映像分析工具对Dump出来的堆转储快照进行分析

    • 判断时内存泄漏还是内存溢出

      • 内存泄漏:查看泄漏对象到 GC Roots 的引用链,找到对象创建位置

      • 内存溢出:

        • 提高堆参数,要参考机器内存

        • 优化代码,减少程序运行时期内存消耗

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

  • 配置

    栈大小配置
    -Xss128K
  • 代码

    栈溢出-栈深度过深
    package 内存管理.内存溢出;
    
    public class 栈内存溢出 {
    
        private int stackLength = 1;
        public void stackLeak() {
            stackLength++;
            stackLeak();
        }
    
        public static void main(String[] args) {
            栈内存溢出 oom = new 栈内存溢出();
            try {
                oom.stackLeak();
            } catch (Throwable e) {
                System.out.println("stack length:" + oom.stackLength);
                throw e;
            }
        }
    栈溢出-局部变量表过大
    package 内存管理.内存溢出.栈内存溢出;
    
    public class 栈内存局部变量表溢出 {
    
        private static int stackLength = 1;
    
        public static void stackLeak() {
            long a1, a2, a3, a4, a5, a6, a7, a8, a9, a10,
                 a11, a12, a13, a14, a15, a16, a17, a18, a19, a20,
                 a21, a22, a23, a24, a25, a26, a27, a28, a29, a30,
                 a31, a32, a33, a34, a35, a36, a37, a38, a39, a40,
                 a41, a42, a43, a44, a45, a46, a47, a48, a49, a50,
                 a51, a52, a53, a54, a55, a56, a57, a58, a59, a60,
                 a61, a62, a63, a64, a65, a66, a67, a68, a69, a70,
                 a71, a72, a73, a74, a75, a76, a77, a78, a79, a80,
                 a81, a82, a83, a84, a85, a86, a87, a88, a89, a90,
                 a91, a92, a93, a94, a95, a96, a97, a98, a99, a100;
    
            stackLength++;
            stackLeak();
    
            a1 = a2 = a3 = a4 = a5 = a6 = a7 = a8 = a9 = a10 =
            a11 = a12 = a13 = a14 = a15 = a16 = a17 = a18 = a19 = a20 =
            a21 = a22 = a23 = a24 = a25 = a26 = a27 = a28 = a29 = a30 =
            a31 = a32 = a33 = a34 = a35 = a36 = a37 = a38 = a39 = a40 =
            a41 = a42 = a43 = a44 = a45 = a46 = a47 = a48 = a49 = a50 =
            a51 = a52 = a53 = a54 = a55 = a56 = a57 = a58 = a59 = a60 =
            a61 = a62 = a63 = a64 = a65 = a66 = a67 = a68 = a69 = a70 =
            a71 = a72 = a73 = a74 = a75 = a76 = a77 = a78 = a79 = a80 =
            a81 = a82 = a83 = a84 = a85 = a86 = a87 = a88 = a89 = a90 =
            a91 = a92 = a93 = a94 = a95 = a96 = a97 = a98 = a99 = a100 = 1;
        }
    
        public static void main(String[] args) {
            try {
                stackLeak();
            } catch (Error e) {
                System.out.println("stack length:" + stackLength);
                throw e;
            }
        }
    }
    栈溢出-线程创建过多
    package 内存管理.内存溢出.栈内存溢出;
    // -Xss2M
    public class 栈内存消耗线程溢出 {
        private void dontStop() {
            while (true) {
            }
        }
    
        private void stackLeakByThread() {
            while (true) {
                new Thread(() -> {
                    dontStop();
                }).start();
            }
        }
        public static void main(String[] args) {
            栈内存消耗线程溢出 oom = new 栈内存消耗线程溢出();
            oom.stackLeakByThread();
        }
    }
  • 异常

    栈溢出-深度过深
    stack length:984
    Exception in thread "main" java.lang.StackOverflowError
    	at 内存管理.内存溢出.栈内存溢出.栈内存溢出.stackLeak(栈内存溢出.java:7)
    	at 内存管理.内存溢出.栈内存溢出.栈内存溢出.stackLeak(栈内存溢出.java:8)
    	at 内存管理.内存溢出.栈内存溢出.栈内存溢出.stackLeak(栈内存溢出.java:8)
    	at 内存管理.内存溢出.栈内存溢出.栈内存溢出.stackLeak(栈内存溢出.java:8)
    	at 内存管理.内存溢出.栈内存溢出.栈内存溢出.stackLeak(栈内存溢出.java:8)
    	at 内存管理.内存溢出.栈内存溢出.栈内存溢出.stackLeak(栈内存溢出.java:8)
    	at 内存管理.内存溢出.栈内存溢出.栈内存溢出.stackLeak(栈内存溢出.java:8)
    	at 内存管理.内存溢出.栈内存溢出.栈内存溢出.stackLeak(栈内存溢出.java:8)
    栈溢出-变量表过大
    stack length:53
    Exception in thread "main" java.lang.StackOverflowError
    	at 内存管理.内存溢出.栈内存溢出.栈内存局部变量表溢出.stackLeak(栈内存局部变量表溢出.java:19)
    	at 内存管理.内存溢出.栈内存溢出.栈内存局部变量表溢出.stackLeak(栈内存局部变量表溢出.java:20)
    	at 内存管理.内存溢出.栈内存溢出.栈内存局部变量表溢出.stackLeak(栈内存局部变量表溢出.java:20)
    	at 内存管理.内存溢出.栈内存溢出.栈内存局部变量表溢出.stackLeak(栈内存局部变量表溢出.java:20)
    	at 内存管理.内存溢出.栈内存溢出.栈内存局部变量表溢出.stackLeak(栈内存局部变量表溢出.java:20)
    	at 内存管理.内存溢出.栈内存溢出.栈内存局部变量表溢出.stackLeak(栈内存局部变量表溢出.java:20)
    	at 内存管理.内存溢出.栈内存溢出.栈内存局部变量表溢出.stackLeak(栈内存局部变量表溢出.java:20)
    	at 内存管理.内存溢出.栈内存溢出.栈内存局部变量表溢出.stackLeak(栈内存局部变量表溢出.java:20)
    栈溢出-线程创建过多
    Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    	at java.lang.Thread.start0(Native Method)
    	at java.lang.Thread.start(Thread.java:719)
    	at 内存管理.内存溢出.栈内存溢出.栈内存消耗线程溢出.stackLeakByThread(栈内存消耗线程溢出.java:13)
    	at 内存管理.内存溢出.栈内存溢出.栈内存消耗线程溢出.main(栈内存消耗线程溢出.java:18)
  • 分析

    • HotSpot 的虚拟机栈和本地方法栈是一起的

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

  • 配置

    JDK8元空间配置
    -XX:MaxMetaspaceSize  设置元空间最大值,默认-1,不限制
    -XX:MetaspaceSize     指定元空间初始大小,达到该值触发垃圾收集器,进行类型卸载
    -XX:MinMetaspaceFreeRatio  垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致                  的垃圾收集的频率
    -XX:MaxMetaspaceFreeRatio  最大元空间剩余容量百分比
  • 代码

    字符串常量池溢出
    package 内存管理.内存溢出.方法区内存溢出;
    
    import java.util.HashSet;
    import java.util.Set;
    
    public class 字符串常量池溢出 {
    
        public static void main(String[] args) {
            Set<String> strSet = new HashSet<>();
            int i = 0;
            while (true) {
                strSet.add(String.valueOf(i++).intern());
            }
        }
    }

  • 异常

    字符串常量池溢出异常
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    	at java.util.HashMap.newNode(HashMap.java:1774)
    	at java.util.HashMap.putVal(HashMap.java:632)
    	at java.util.HashMap.put(HashMap.java:613)
    	at java.util.HashSet.add(HashSet.java:220)
    	at 内存管理.内存溢出.方法区内存溢出.字符串常量池溢出.main(字符串常量池溢出.java:12)
  • 分析

    • JDK8之前使用的是永久代的方式实现方法区,JDK8之后是使用元空间

    • JDK7之前字符串常量池在永久代实现的方法区中,而JDK7之后,字符串常量池在堆中

    • 实际使用中,Spring、Hibernate等框架都会使用CGLib字节码技术来增强类,增强类越多需要的方法区就越大

直接内存溢出

  • 配置

    直接内存容量配置
    -XX:MaxDirectMemorySize  如果不指定,默认和java堆的最大值(-Xmx)一致
  • 代码

    直接内存溢出
    package 内存管理.内存溢出.直接内存溢出;
    
    import sun.misc.Unsafe;
    
    import java.lang.reflect.Field;
    
    public class 直接内存溢出 {
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) throws IllegalAccessException {
            Field unsafeField = Unsafe.class.getDeclaredFields()[0];
            unsafeField.setAccessible(true);
            Unsafe unsafe = (Unsafe) unsafeField.get(null);
            while (true) {
                unsafe.allocateMemory(_1MB);
            }
        }
    }
  • 异常

    直接内存溢出异常
    Exception in thread "main" java.lang.OutOfMemoryError
    	at sun.misc.Unsafe.allocateMemory(Native Method)
    	at 内存管理.内存溢出.直接内存溢出.直接内存溢出.main(直接内存溢出.java:15)
  • 分析

    • 直接内存溢出情况:heap dump 文件无明显异常,且 dump 文件比较小,程序中之间或间接的使用了DirectMemory(典型的间接使用是NIO),考虑直接内存溢出

垃圾收集器及内存分配策略

程序计数器、虚拟机栈、本地方法栈的内存分配和回收很简单,当线程创建或结束时,系统的内存会自动的分配和回收。

垃圾回收主要处理的是堆和方法区的内存。

三个问题:

哪些内存需要回收?

什么时候回收?

怎么回收?

判断对象是否存活算法

引用计数算法(Reference Counting)

对象中添加引用计数器,每当有地方引用计数器+1,引用失效计数器-1,计数器为0代表没有引用了,对象可以从内存中删除。

使用:Python

优点:简单

缺点:循环引用的情况下,对象永远不死

可达性分析算法(Reachability Analysis)

GC Roots + 引用链,如果对象没有到 GC Roots 的引用链,则该对象是可回收的,参考下图

使用:Java

对象引用

强引用:

Object obj = new Object(),垃圾收集器永远不会回收

软引用:

SoftReference 类实现,在发生内存溢出异常之前回收这种引用的对象

弱引用:

WeakReference 类实现,但系统GC时就会回收弱引用的对象

虚引用:

PhantomReference 类实现,不影响垃圾回收,只不过在垃圾回收该对象的时候通知一下

实战:内存分配与回收策略

对象优先在Eden分配

程序代码
package 内存管理.垃圾收集;

public class 对象优先在Eden分配 {

    /**
     * -verbose:gc  打印GC信息
     * -Xms20M  堆最小内存
     * -Xmx20M  堆最大内存
     * -Xmn10M  新生代 10M 大小
     * -XX:SurvivorRatio=8  新生代 Eden:Survivor 的比例 8:1
     * -XX:+PrintCommandLineFlags 打印参数
     * -XX:+PrintGCDetails  打印GC详细信息
     *
     * @param args
     */
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];   // 出现一次 Minor GC
    }
}
启动参数
-verbose:gc  打印GC信息
-Xms20M  堆最小内存
-Xmx20M  堆最大内存
-Xmn10M  新生代 10M 大小
-XX:SurvivorRatio=8  新生代 Eden:Survivor 的比例 8:1
-XX:+PrintCommandLineFlags 打印参数
-XX:+PrintGCDetails  打印GC详细信息
运行结果
-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
[GC (Allocation Failure) [PSYoungGen: 6464K->872K(9216K)] 6464K->4976K(19456K), 0.0018411 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 7253K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 77% used [0x00000000ff600000,0x00000000ffc3b5a0,0x00000000ffe00000)
  from space 1024K, 85% used [0x00000000ffe00000,0x00000000ffeda020,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
 Metaspace       used 3805K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 424K, capacity 428K, committed 512K, reserved 1048576K
结果分析

1、打印参数中:XX:+UseParallelGC,代表我使用的 JVM 的垃圾回收器是 Parallel GC

2、GC 信息显示,在 GC 后,新生代空间从 6M→ 800K,整堆空间从 6M→5M

大对象直接分配到老年代

程序代码
package 内存管理.垃圾收集;

public class 大对象直接进入老年代 {
    /**
     * -verbose:gc  打印GC信息
     * -Xms20M  堆最小内存
     * -Xmx20M  堆最大内存
     * -Xmn10M  新生代 10M 大小
     * -XX:SurvivorRatio=8  新生代 Eden:Survivor 的比例 8:1
     * -XX:+PrintCommandLineFlags 打印参数
     * -XX:+PrintGCDetails  打印GC详细信息
     * -XX:PretenureSizeThreshold=3145728   超过3145728字节的对象直接进入老年代,对 Parallel GC 不生效
     * -XX:+UseSerialGC 使用串行垃圾收集器
     *
     * @param args
     */
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] allocation1;
        allocation1 = new byte[4 * _1MB];
    }
}
启动参数
-verbose:gc  打印GC信息
-Xms20M  堆最小内存
-Xmx20M  堆最大内存
-Xmn10M  新生代 10M 大小
-XX:SurvivorRatio=8  新生代 Eden:Survivor 的比例 8:1
-XX:+PrintCommandLineFlags 打印参数
-XX:+PrintGCDetails  打印GC详细信息
-XX:PretenureSizeThreshold=3145728   超过3145728字节的对象直接进入老年代,对 Parallel GC 不生效
-XX:+UseSerialGC 使用串行垃圾收集器
运行结果
-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:PretenureSizeThreshold=3145728 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
Heap
 def new generation   total 9216K, used 2532K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  30% used [0x00000000fec00000, 0x00000000fee792a0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 3805K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 424K, capacity 428K, committed 512K, reserved 1048576K
结果分析

1、-XX:PretenureSizeThreshold=3145728 只对 Serial 和 ParNew 收集器有效。

2、-XX:+UseSerialGC 使用 Serical 收集器