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

运行时数据区域

程序计数器

  • 用于记录从内存执行的下一条指令的地址,线程私有的一小块内存,也是唯一不会报出OOM异常的区域

Java虚拟机栈

  • Java虚拟机栈(Java Virtual Machine Stack)是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常

  • 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常

本地方法栈

  • 与Java虚拟机栈类似,只不过服务对象不一样,本地方法栈为虚拟机使用到的本地方法服务,Java虚拟机栈为虚拟机执行Java方法(字节码)服务

Java堆

  • 对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存
  • 当堆内存没有足够空间给对象实例分配内存并且堆内存无法扩展时都会抛出OOM异常

方法区

  • 方法区与Java堆类似,也是各个线程共享的区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
  • 通常用别名“非堆”来与Java堆做区分
  • 当方法区没有足够空间满足内存分配要求时,也会抛出OOM异常

运行时常量池

  • 运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量与符号引用
  • 受方法区内存限制,当常量池无法再申请到内存时会抛出OOM异常

直接内存

  • 直接内存并不是运行时数据区的一部分,但它受总内存限制,也可能会出现OOM异常

HotSpot虚拟机对象探秘

对象的创建

在类加载检查通过后,接下来虚拟机将为新生对象分配内存,而内存分配方式主要有两种:

  • 指针碰撞

  • 空闲列表

对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

  • 对象头

    • 存储对象自身运行时数据(Mark Word),如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
    • 类型指针(对象指向其类型元数据的指针)
  • 实例数据

    • 对象真正存储的有效信息,即代码中的各类型字段内容
  • 对齐填充

    • 由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即任何对象大小都是8字节的整数倍,故实例数据部分没有对齐的话需要对齐填充来充当占位符补全

对象的访问定位

Java程序会通过栈上的reference(一个指向对象的引用)数据来操作堆上的具体对象,具体的访问方式由虚拟机实现。

主流访问方式主要有两种:

  • 句柄

  • 直接指针

实战OOM异常

采用不同的JDK及垃圾回收收集器均可能会产生不同的结果,以下实战均以JDK8,ParallelGC垃圾收集器为例运行代码

# 查看默认垃圾收集器VM参数
-XX:+PrintCommandLineFlags -version

Java堆溢出

只要不断创建对象实例,同时又避免垃圾收集器回收,这样达到最大堆容量限制后便能产生OOM异常

public class Hello {
/**
* -Xms:最小堆内存20M -Xmx:最大堆内存20M 两者设置一样避免自动扩展
* VM参数:-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
*/
public static void main(String[] args) {
List<Hello> hellos = new ArrayList<>();
while (true) {
hellos.add(new Hello());
}
}
}

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

《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常

  • 使用-Xss参数减少栈容量
public class Hello {
/**
* VM参数:-Xss128k
*/
private int stackLength = 1;
public void stackLeak() {
stackLength++;
// 递归调用方法,不断入栈
stackLeak();
}
public static void main(String[] args) throws Throwable {
Hello oom = new Hello();
try {
// 调用方法,入栈
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}

  • 定义了大量的本地变量,增大此方法帧中本地变量表的长度(即调整栈帧大小)
public class Hello {
private static int stackLength = 0; public static void test() {
// 局部变量多,栈帧增大
long unused1, unused2, unused3, unused4, unused5,
unused6, unused7, unused8, unused9, unused10,
unused11, unused12, unused13, unused14, unused15,
unused16, unused17, unused18, unused19, unused20,
unused21, unused22, unused23, unused24, unused25,
unused26, unused27, unused28, unused29, unused30,
unused31, unused32, unused33, unused34, unused35,
unused36, unused37, unused38, unused39, unused40,
unused41, unused42, unused43, unused44, unused45,
unused46, unused47, unused48, unused49, unused50,
unused51, unused52, unused53, unused54, unused55,
unused56, unused57, unused58, unused59, unused60,
unused61, unused62, unused63, unused64, unused65,
unused66, unused67, unused68, unused69, unused70,
unused71, unused72, unused73, unused74, unused75,
unused76, unused77, unused78, unused79, unused80,
unused81, unused82, unused83, unused84, unused85,
unused86, unused87, unused88, unused89, unused90,
unused91, unused92, unused93, unused94, unused95,
unused96, unused97, unused98, unused99, unused100;
stackLength++;
// 递归调用,不断入栈
test();
unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10
= unused11 = unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19
= unused20 = unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27 = unused28
= unused29 = unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37
= unused38 = unused39 = unused40 = unused41 = unused42 = unused43 = unused44 = unused45 = unused46
= unused47 = unused48 = unused49 = unused50 = unused51 = unused52 = unused53 = unused54 = unused55
= unused56 = unused57 = unused58 = unused59 = unused60 = unused61 = unused62 = unused63 = unused64
= unused65 = unused66 = unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73
= unused74 = unused75 = unused76 = unused77 = unused78 = unused79 = unused80 = unused81 = unused82
= unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 = unused90 = unused91
= unused92 = unused93 = unused94 = unused95 = unused96 = unused97 = unused98 = unused99 = unused100 = 0;
} public static void main(String[] args) {
try {
test();
} catch (Error e) {
System.out.println("stack length:" + stackLength);
throw e;
}
}
}

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

  • 方法区容量控制
public class Hello {
/**
* JDK8前VM参数: -XX:PermSize=6M -XX:MaxPermSize=6M
* JDK8VM参数:-XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M
*/
public static void main(String[] args) {
// 使用Set保持常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<>();
// 在short范围内足以让6M大小的PermSize(永久代,JDK8前有,JDK8及之后版本都已采用元空间替代)产生OOM了
short i = 0;
// JDK8前,抛出OOM异常
// JDK8下,正常情况会进入死循环,并不会抛出任何异常
while (true) {
// String.intern()进入字符串常量池
set.add(String.valueOf(i++).intern());
}
}
}

上述代码在JDK8环境下并不会抛出任何异常,这是因为字符串常量池已经被移至Java堆之中,控制方法区容量的大小对Java堆并没有什么影响

  • String.intern()方法介绍:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回常量池中这个字符串的String对象;否则,将此String对象包含的字符复制添加到常量池中,并返回此String对象的引用
/**
* JDK6:false false
* JDK8:true false
*/
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);
}
  • JDK6因为new StringBuilder()分配到的是Java堆内存,而String.intern()会把首次遇到的字符串复制到的是字符串常量池(方法区),所以都是false

  • JDK8因为字符串常量池都移动到了Java堆中,new StringBuilder()分配到Java堆内存后,字符串常量池也记录到了首次遇到的实例引用,那么String.intern()new StringBuilder()都是同一个了(true);而因为java字符串在sun.misc.Version类加载时已进入常量池,那么intern()方法就返回当前常量池的String对象,new StringBuilder()在堆中重新创建了一个,自然也就不一样了(false)

  • 方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,因此运行时产生大量的类填满方法区也可以造成方法区溢出

/*
* 借助CGLib造成方法区溢出
* VM参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
*/
public class Hello {
public static void main(String[] args) {
while (true) {
// 创建CgLib增强对象
Enhancer enhancer = new Enhancer();
// 设置被代理的类
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
// 指定拦截器
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
// 创建代理对象
enhancer.create();
}
} static class OOMObject {
}
}

本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致

// 使用unsafe分配本机内存
public class Hello {
// VM参数:-Xmx20M -XX:MaxDirectMemorySize=10M
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);
}
}
}

参考资料

《深入理解Java虚拟机》(第三版) 第2章:Java内存区域与内存溢出异常

关注我

深入理解Java虚拟机之图解Java内存区域与内存溢出异常的更多相关文章

  1. 《深入理解 Java 虚拟机》读书笔记:Java 内存区域与内存溢出异常

    前言 最近开始看这本书,记得前段时间拿起这本书的时候,心情是相当沉重的!当时的剧本是这样的-- 内景.家里 - 下午 我(画外):唉,有点无聊啊!(偶然撇过书架)这么多书得看到什么时候啊,要不要拿一本 ...

  2. 深入理解java虚拟机系列(一):java内存区域与内存溢出异常

    文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了開始.假设有什么错误或者遗漏,欢迎指出. 一.概述 先上 ...

  3. 《深入理解Java虚拟机》-----第2章 Java内存区域与内存溢出异常

    2.1 概述 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任 ...

  4. 《深入理解 Java 虚拟机》学习 -- Java 内存模型

    <深入理解 Java 虚拟机>学习 -- Java 内存模型 1. 区别 这里要和 JVM 内存模型区分开来: JVM 内存模型是指 JVM 内存分区 Java 内存模型(JMM)是指一种 ...

  5. Java内存区域与内存溢出异常——深入理解Java虚拟机 笔记一

    Java内存区域 对比与C和C++,Java程序员不需要时时刻刻在意对象的创建和删除过程造成的内存溢出.内存泄露等问题,Java虚拟机很好地帮助我们解决了内存管理的问题,但深入理解Java内存区域,有 ...

  6. 深入理解Java虚拟机之Java内存区域与内存溢出异常

    Java内存区域与内存溢出异常 运行时数据区域 程序计数器 用于记录从内存执行的下一条指令的地址,线程私有的一小块内存,也是唯一不会报出OOM异常的区域 Java虚拟机栈 Java虚拟机栈(Java ...

  7. 《深入理解Java虚拟机》——Java内存区域与内存溢出异常

    程序计数器(Program Counter Register):一块较小的内存空间,可看作是当前线程所执行的字节码的行号指示器.字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令 ...

  8. 深入理解Java虚拟机02--Java内存区域与内存溢出异常

    一.概述 我们在进行 Java 开发的时候,很少关心 Java 的内存分配等等,因为这些活都让 JVM 给我们做了.不仅自动给我们分配内存,还有自动的回收无需再占用的内存空间,以腾出内存供其他人使用. ...

  9. 《深入理解java虚拟机》第二章 Java内存区域与内存溢出异常

    第二章 Java内存区域与内存溢出异常 2.2 运行时数据区域  

随机推荐

  1. ciscn_2019_s_6

    例行检查 没有开启nx保护,考虑用shellcode来做这道题 程序放入ida查看 我们可以输入48个字符覆盖0使printf打印出bp的值 继续看这里,buf的大小实际上只有0x38的大小,但是re ...

  2. 【译】使用 Visual Studio 调试外部源代码

    您是否曾经需要调试并进入依赖于 NuGet 或 .NET 库的代码,而这些库并没有构建为您的解决方案的一部分? 现在,调试它们并不像调试作为解决方案一部分的项目那么容易.从 Visual Studio ...

  3. AtCoder Beginner Contest 169 题解

    AtCoder Beginner Contest 169 题解 这场比赛比较简单,证明我没有咕咕咕的时候到了! A - Multiplication 1 没什么好说的,直接读入两个数输出乘积就好了. ...

  4. 自动化集成:Pipeline流水语法详解

    前言:该系列文章,围绕持续集成:Jenkins+Docker+K8S相关组件,实现自动化管理源码编译.打包.镜像构建.部署等操作:本篇文章主要描述Pipeline流水线用法. 一.Webhook原理 ...

  5. python 字符编码讲解

    ANSI不是一种具体的编码格式 ANSI在中文Windows操作系统代码指的是GBK编码 ANSI在中文Mac操作系统代码指的是UTF-8编码 ANSI在其他国家的操作系统中有其他的编码格式 #ASC ...

  6. mkdir创建目录时,如果上级目录没有是创建不成功的

    mkdir创建目录时,如果上级目录没有是创建不成功的 ,此时必须用 mkdirs()方法方可.

  7. JAVA通过正则匹配html里面body标签的内容,去掉body标签

    /** * 获取html中body的内容 包含body标签 * @param htmlStr html代码 * @return */ public static String getBody(Stri ...

  8. JAVA中SpringMVC获取bean方法,在工具类(utils)注入service

    有时候我们会出现无法用注解 @Autowired 注入bean的情况,这个时候可以 通过contextLoader获取 WebApplicationContext ctx = ContextLoade ...

  9. 【LeetCode】459. Repeated Substring Pattern 解题报告(Java & Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 遍历子串 日期 [LeetCode] 题目地址:ht ...

  10. 【LeetCode】43. Multiply Strings 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...