文章

JVM Runtime Data Area(运行时内存区域)

JVM Runtime Data Area(运行时内存区域)

JVM Runtime Data Area(Java 内存模式)

注意和 JMM(Java 内存模型区分开)

Runtime Data Area 运行时数据区介绍

Runtime Data Area 是存放数据的。分为五部分:StackHeapMethod AreaPC RegisterNative Method Stack。几乎所有的关于 Java 内存方面的问题,都是集中在这块。下图是关于 Run-time Data Areas 的描述:
ccmh5

线程私有的数据区,包含程序计数器、虚拟机栈、本地方法栈

  1. 程序计数器 (PC Register) ,记录正在执行的虚拟机字节码的地址
  2. 虚拟机栈 (Stack),方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧
  3. 本地方法栈 (Native Method Stack),虚拟机的 Native 方法执行的内存区

所有线程共享的数据区,包含 Java 堆、方法区(有常量池)

  1. Java 堆 (Heap),对象分配内存的区域
  2. 方法区 (Method Area),存放类信息、常量、静态变量、编译器编译后的代码等数据;常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。

程序计数器、虚拟机栈、本地方法栈这 3 个区域是线程私有的,会随线程消亡而自动回收,所以不需要 GC 管理;垃圾收集只需要关注堆和方法区,而方法区的回收,往往性价比较低,因为判断可以回收的条件比较苛刻,而垃圾收集回报率高的是堆中内存的回收

详细内存模型:
yo5n3

Stack 虚拟机栈  (线程私有)

栈是先进后出 (FILO) 的数据结构。虚拟机栈在 JVM 运行过程中存储当前线程运行方法所需的数据,指令、返回地址。Java 虚拟机栈是基于线程的,哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。

栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如 -Xss256k

JVM 的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性。在线程中执行一个方法时,我们会创建一个栈帧入栈并执行,如果该方法又调用另一个方法时会再次创建新的栈帧然后入栈,方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧,随后虚拟机将会丢弃此栈帧。

栈组成 – 栈帧

Stack 是 Java 栈内存,它等价于 C 语言中的栈,栈的内存地址是不连续的,每个线程都拥有自己的栈。栈里面存储着的是 StackFrame,在《JVM Specification》中文版中被译作 java 虚拟机框架,也叫做 栈帧StackFrame 包含三类信息:局部变量表、操作数栈、动态连接、返回地址。
0v3te

1. 局部变量表 (Local Variable Table)

局部变量表 (Local Variable Table) 是一组变量值存储空间,用于存放我们的局部变量的。用于存放方法参数和方法内定义的局部变量 (基本数据类型,对象的引用)。虚拟机通过索引定位的方法查找相应的局部变量。

1
2
3
4
5
6
public class TestStack {
    public static int NUM1 = 100;
    public int sub(int a, int b) {
        return a-b-NUM1;
    }
}

对应字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public sub(II)I
   L0
    LINENUMBER 19 L0
    ILOAD 1
    ILOAD 2
    ISUB
    GETSTATIC com/example/jvm/TestStack.NUM1 : I
    ISUB
    IRETURN
   L1
    LOCALVARIABLE this Lcom/example/jvm/TestStack; L0 L1 0  // 局部变量表第0个位置为this
    LOCALVARIABLE a I L0 L1 1 // 局部变量表第1个位置为a
    LOCALVARIABLE b I L0 L1 2 // 局部变量表弟2个位置为b
    MAXSTACK = 2
    MAXLOCALS = 3

几个局部变量?

答案是 3 个,除了 a 和 b,还有 this;对应实例对象方法编译器都会追加一个 this 参数。如果该方法为静态方法则为 2 个了

2. 操作数栈

通过局部变量表我们有了要操作和待更新的数据,我们如果对局部变量这些数据进行操作呢?通过操作数栈。
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

3. 动态连接

Java 语言特性多态(需要类运行时才能确定具体的方法)。

4. 返回地址

正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)

StackFrame 在方法被调用时创建,在某个线程中,某个时间点上,只有一个框架是活跃的,该框架被称为 Current Frame,而框架中的方法被称为 Current Method,其中定义的类为 Current Class。局部变量和操作数栈上的操作总是引用当前框架。当 Stack Frame 中方法被执行完之后,或者调用别的 StackFrame 中的方法时,则当前栈变为另外一个 StackFrame。Stack 的大小是由两种类型,固定和动态的,动态类型的栈可以按照线程的需要分配。下面两张图是关于栈之间关系以及栈和非堆内存的关系基本描述(来自http://www.programering.com/a/MzM3QzNwATA.html
zxhjr

栈的大小

JVM 允许栈的大小是固定的或者是动态变化的。在 Oracle的关于参数设置的官方文档中有关于Stack的设置 是通过 -Xss 来设置其大小。关于 Stack 的默认大小对于不同机器有不同的大小,并且不同厂商或者版本号的 jvm 的实现其大小也不同,如下表是 HotSpot 的默认大小:
mi97k

栈之 GC

栈是不需要垃圾回收的,尽管说垃圾回收是 Java 内存管理的一个很热的话题,栈中的对象如果用垃圾回收的观点来看,他永远是 live 状态,是可以 reachable 的,所以也不需要回收,它占有的空间随着 Thread 的结束而释放。

另外栈上有一点得注意的是,对于本地代码调用,可能会在栈中申请内存,比如 C 调用 malloc(),而这种情况下,GC 是管不着的,需要我们在程序中,手动管理栈内存,使用 free() 方法释放内存。

栈异常

栈一般会发生以下两种异常:

  1. 当线程中的计算所需要的栈超过所允许大小时,会抛出 StackOverflowError
  2. 当 Java 栈试图扩展时,没有足够的存储器来实现扩展,JVM 会报 OutOfMemoryError

代码优化

我们一般通过减少常量,参数的个数来减少栈的增长,在程序设计时,我们把一些常量定义到一个对象中,然后来引用他们可以体现这一点。另外,少用递归调用也可以减少栈的占用。

PC Register 程序计数器 (不会发生 OOM)  (线程私有)

PC Register 是程序计数寄存器,每个 Java 线程都有一个单独的 PC Register,他是一个指针,由 Execution Engine 读取下一条指令。如果该线程正在执行 java 方法,则 PC Register 存储的是正在被执行的指令的地址,如果是本地方法,PC Register 的值没有定义。PC 寄存器非常小,只占用一个字宽,可以持有一个 returnAdress 或者特定平台的一个指针。

较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。
由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。

程序计数器也是 JVM 中唯一不会 OOM(OutOfMemory) 的内存区域

Native Method Stack 本地方法栈 (合并到虚拟机栈) (线程私有)

Native Method Stack 是供本地方法(非 java)使用的栈。每个线程持有一个 Native Method Stack。

  1. 当一个 JVM 创建的线程调用 native 方法后,JVM 不再为其在虚拟机栈中创建栈帧,JVM 只是简单地动态链接并直接调用 native 方法
  2. 虚拟机规范无强制规定,各版本虚拟机自由实现。HotSpot 直接把本地方法栈和虚拟机栈合二为一

Heap 堆 (线程共享)

Heap 是用来存放对象信息的,和 Stack 不同,Stack 代表着一种运行时的状态。换句话说,栈是运行时单位,解决程序该如何执行的问题,而堆是存储的单位,解决数据存储的问题。
Heap 是伴随着 JVM 的启动而创建,负责存储所有对象实例和数组的

Heap 组成

在 JVM 初始化的时候,我们可以通过参数来分别指定,堆的大小、以及 Young Generation 和 Old Generation 的比值、Eden 区和 From Space 的比值,从而来细粒度的适应不同 JAVA 应用的内存需求。

Heap Space

堆的存储空间和栈一样是不需要连续的,它分为 Young GenerationOld Generation(也叫 Tenured Generation)两大部分。Young Generation 分为 EdenSurvivorSurvivor 又分为 From SpaceToSpace

JDK1.7 堆内部组成:

or17k

JDK1.8 堆内部组成,其中永久代 (Perm) 换成了元空间。

fvu66

堆内存逻辑角度:: 堆=新生代 + 老年代 + 永久代或者元空间;

堆内存物理角度:由新生代 ( Young ) 和老年代 ( Old ) 组成,公式如下:

堆内存的实际大小=新生代的大小 + 老年代的大小。

Young Generation 新生代
Eden

Eden 存放新生的对象,对象优先分配至 Eden 区,当空间不足时,将触发 MinorGC

Survivor

Survivor 主要用于存储垃圾回收之后的存活对象

  1. From Space
  2. To Space
Old Generation(Tenured Generation) 老年代

用于存放生命周期较长的大对象

对象的转移

Eden 区里存放的是新生的对象;From Space 和 To Space 中存放的是每次垃圾回收后存活下来的对象,所以每次垃圾回收后,Eden 区会被清空;存活下来的对象先是放到 From Space,当 From Space 满了之后移动到 To Space;当 To Space 满了之后移动到 Old Space。Survivor 的两个区是对称的,没先后关系,所以同一个区中可能同时存在从 Eden 复制过来的对象和从前一个 Survivor 复制过来的对象,而复制到 Old Space 区的只有从第一个 Survivor 复制过来的对象。而且,Survivor 区总有一个是空的。同时,根据程序需要,Survivor 可以配置多个(多于 2 个),这样可以增加对象在 Young Generation 中存在的时间,减少被放到 Old Generation 的可能。
Old Space 中则存放生命周期比较长的对象,而且有些比较大的新生对象也放在 Old Space 中。

大对象:长期存活的对象,对象每在 Survivor 经历一次 MinorGC,Age 增加 1,当增长到 15 时,就直接晋升到老年代;如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄段对象就可以直接进入老年代。

Ref: JVM 堆的对象转移与年龄判断 - JVM 入门教程-慕课网

MinorGC、MajorGC 和 FullGC

  1. MinorGC 发生在 Yong Generation 的 GC

MinorGC 非常频繁,一般回收速度也非常快

  1. MajorGC 发生在 Tenured space 的 GC

发生在老年代的 GC,出现了 MajorGC,通常伴随至少一次 MinorGC,MajorGC 速度通常比 MinorGC 慢 10 倍以上

  1. FullGC 整个 Heap(both Young and Tenured spaces)

堆内存大小改变

堆的大小通过 -Xms-Xmx 来指定最小值和最大值。

通过 -Xmn 来指定 Young Generation 的大小(一些老版本也用 -XX:NewSize 指定),即 Eden 加 FromSpace 和 ToSpace 的总大小。然后通过 -XX:NewRatio 来指定 Eden 区的大小,在 Xms 和 Xmx 相等的情况下,该参数不需要设置。通过 -XX:SurvivorRatio 来设置 Eden 和一个 Survivor 区的比值。

堆异常

堆异常分为两种,一种是 Out of Memory(OOM),一种是 Memory Leak(ML)。Memory Leak 最终将导致 OOM。
关于异常的处理,确定 OOM/ML 异常后,一定要注意保护现场,可以 dump heap,如果没有现场则开启 GCFlag 收集垃圾回收日志,然后进行分析,确定问题所在。如果问题不是 ML 的话,一般通过增加 Heap,增加物理内存来解决问题,是的话,就修改程序逻辑。

Method Area 方法区 (Permanent Space)(线程共享)

方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池。 Method Area 在 HotSpot JVM 的实现中属于非堆区。

方法区是一种规范,不同的虚拟机实现不一样。JDK1.7 及之前方法区的实现是永久代;JDK1.8 后永久代被移除,取而代之的是元空间。

非堆区包括两部分:Permanent Generation 和 Code Cache。

  1. Method Area 属于 Permanent Generation 的一部分,Permanent Generation 用来存储类信息,比如说:class definitions,structures,methods, field, method (data and code) 和 constants。
  2. Code Cache 用来存储 Compiled Code,即编译好的本地代码,在 HotSpot JVM 中通过 JIT(Just In Time) Compiler 生成,JIT 是即时编译器,他是为了提高指令的执行效率,把字节码文件编译成本地机器代码

方法区线程安全

方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地 JVM 外内存

Permanent Space(永久代)

很多开发者都习惯将方法区称为 “ 永久代 “,其实这两者并不是等价的。主要存放的是 Java 类定义信息,与垃圾收集器要收集的 Java 对象关系不大。

HotSpot 虚拟机使用永久代来实现方法区,但在其它虚拟机中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一说。因此,方法区只是 JVM 中规范的一部分,可以说,在 HotSpot 虚拟机中,设计人员使用了永久代来实现了 JVM 规范的方法区。

Java7 及以前版本的 Hotspot 中方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。但在 Java7 中永久代中存储的部分数据已经开始转移到 Java Heap 或 Native Memory 中了。比如,符号引用 (Symbols) 转移到了 Native Memory;字符串常量池 (interned strings) 转移到了 Java Heap;类的静态变量 (class statics) 转移到了 Java Heap。

  • 设置永久代空间大小
    JDK1.7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;

Metaspace(元空间)

Java8,HotSpots 取消了永久代,元空间 (Metaspace) 登上舞台,方法区存在于元空间 (Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。

本地内存(Native memory),也称为 C-Heap,是供 JVM 自身进程使用的。当 Java Heap 空间不足时会触发 GC,但 Native memory 空间不够却不会触发 GC。
kzym7

元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中 “java.lang.OutOfMemoryError: PermGen space” 这种错误

元空间大小参数

1
2
3
4
-XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
-XX:MaxMetaspaceSize,可以为class metadata分配的最大空间。默认是没有限制的。
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。

Java8 为什么使用元空间替代永久代,这样做有什么好处呢?

  1. 永久带会为 GC 带来不必要的复杂性,并且回收效率偏低,在永久代中元数据可能会随着每一次赋 GC 发生而进行移动,而 hotspot 虚拟机每种类型的垃圾回收器都要特殊处理永久代中的元数据,分离出来以后可以简化赋 GC,以及以后并发隔离元数据等方面进行优化。
  2. 移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

3hhjx

运行时常量在不同版本的位置

  • 在 jdk 1.6 中,运行时常量池位于方法区中。
  • jdk 1.7 开始将运行时常量池放置于 java 堆中。
  • jdk 1.8 之后删去了方法区这个数据区,使用位于直接内存中的元空间取代了方法区,这时运行时常量池在元空间中

从底层深入理解运行时数据区

工具 HSDB 查询内存分配,内存地址

其他

堆外内存(本地内存)

不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;

  1. 如果使用了 NIO,这块区域会被频繁使用,在 Java 堆内可以用 directByteBuffer 对象直接引用并操作;
  2. 这块内存不受 Java 堆大小限制,但受本机总内存的限制,可以通过 MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常;

内存溢出

栈溢出 (StackOverFlowError)

HotSpot 版本中栈的大小是固定的,是不支持拓展的。java.lang.StackOverflowError  一般的方法调用是很难出现的,如果出现了可能会是无限递归。
虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算法中:递归和非递归 (循环来实现) 都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。
OutOfMemoryError:不断建立线程,JVM 申请栈内存,机器没有足够的内存。(一般演示不出,演示出来机器也死了)

JVM 设置栈内存参数:-Xss1m

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * 栈溢出 -Xss1m
 */
public class StackOverFlow {

    public void king(){//一个栈帧--虚拟机栈运行
        king();//无穷的递归
    }
    public static void main(String[] args)throws Throwable {
        StackOverFlow javaStack = new StackOverFlow(); //new一个对象
        javaStack.king();
    }
}

输出:

1
2
3
4
5
6
Exception in thread "main" java.lang.StackOverflowError
	at com.example.oom.StackOverFlow.king(StackOverFlow.java:10)
	at com.example.oom.StackOverFlow.king(StackOverFlow.java:10)
	// ...
	at com.example.oom.StackOverFlow.king(StackOverFlow.java:10)

堆溢出 (OutOfMemoryError)

申请内存空间,超出最大堆内存空间

JVM 参数设置堆内存:-Xms,-Xmx 参数

示例代码 1:

1
2
3
4
5
6
7
// VM Args:-Xms30m -Xmx30m -XX:+PrintGCDetails 堆内存溢出(直接溢出)
public class HeapOom {
    public static void main(String[] args) {
        String[] strings = new String[35 * 1000 * 1000];  //35m的数组(堆)
        System.out.println("HeapOom demo");
    }
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[GC (Allocation Failure) [PSYoungGen: 843K->480K(9216K)] 843K->488K(29696K), 0.0023969 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 480K->384K(9216K)] 488K->392K(29696K), 0.0018090 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 384K->0K(9216K)] [ParOldGen: 8K->298K(20480K)] 392K->298K(29696K), [Metaspace: 2814K->2814K(1056768K)], 0.0053457 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(9216K)] 298K->298K(29696K), 0.0012725 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(9216K)] [ParOldGen: 298K->286K(20480K)] 298K->286K(29696K), [Metaspace: 2814K->2814K(1056768K)], 0.0050989 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 246K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 3% used [0x00000007bf600000,0x00000007bf63d890,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 20480K, used 286K [0x00000007be200000, 0x00000007bf600000, 0x00000007bf600000)
  object space 20480K, 1% used [0x00000007be200000,0x00000007be247b28,0x00000007bf600000)
 Metaspace       used 2846K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 296K, capacity 386K, committed 512K, reserved 1048576K
:JavaTestCases:HeapOom.main() spend 324ms
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.example.oom.HeapOom.main(HeapOom.java:10)
      00:03.97   :JavaTestCases:compileJava
      00:00.32   :JavaTestCases:HeapOom.main()
      00:00.06   :libCommonJava:compileJava

示例代码 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * VM Args:-Xms30m -Xmx30m -XX:+PrintGC
 * 堆的大小30M 造成一个堆内存溢出(分析下JVM的分代收集)
 * GC调优---生产服务器推荐开启(默认是关闭的) -XX:+HeapDumpOnOutOfMemoryErro
 */
public class HeapOom2 {
    public static void main(String[] args) {
        //GC ROOTS
        List<Object> list = new LinkedList<>(); // list   当前虚拟机栈(局部变量表)中引用的对象  是1,不是走2
        int i = 0;
        while (true) {
            i++;
            if (i % 10000 == 0) System.out.println("i=" + i);
            list.add(new Object());
        }

    }
}

输出:

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at com.example.oom.HeapOom2.main(HeapOom2.java:20)
      00:05.82   :JavaTestCases:HeapOom2.main()
      00:00.18   :JavaTestCases:compileJava
      00:00.05   :libCommonJava:compileJava

方法区溢出

  1. 运行时常量池溢出
  2. 方法区中保存的 Class 对象没有被及时回收掉或者 Class 信息占用的内存超过了我们配置

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * cglib动态生成
 * Enhancer中 setSuperClass和setCallback, 设置好了SuperClass后, 可以使用create制作代理对象了
 * 限制方法区的大小导致的内存溢出
 * VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 * */
public class MethodAreaOutOfMemory {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(TestObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
                    return arg3.invokeSuper(arg0, arg2);
                }
            });
            enhancer.create();
        }
    }

    public static class TestObject {
        private double a = 34.53;
        private Integer b = 9999999;
    }
}

Class 要被回收条件:

  1. 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例
  2. 加载该类的 ClassLoader 已经被回收
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  4. 没有设置 -Xnoclassgc
    klj4g

本机直接内存溢出

直接内存的容量可以通过 MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常;
由直接内存导致的内存溢出,一个比较明显的特征是在 HeapDump 文件中不会看见有什么明显的异常情况,如果发生了 OOM,同时 Dump 文件很小,可以考虑重点排查下直接内存方面的原因。

示例代码:

1
2
3
4
5
6
7
8
9
10
// NIO
/**
 * VM Args:-XX:MaxDirectMemorySize=100m 堆外内存(直接内存溢出)
 */
public class DirectOom {
    public static void main(String[] args) {
        //直接分配128M的直接内存(100M)
        ByteBuffer bb = ByteBuffer.allocateDirect(128 * 1024 * 1204);
    }
}

输出:

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.example.oom.DirectOom.main(DirectOom.java:11)

对象的分配策略

见 [[Java对象创建流程&对象内存分配策略]]

本文由作者按照 CC BY 4.0 进行授权