深入理解Java虚拟机之图解Java内存区域与内存溢出异常
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()
会把首次遇到的字符串复制到的是字符串常量池(方法区),所以都是falseJDK8因为字符串常量池都移动到了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内存区域与内存溢出异常的更多相关文章
- 《深入理解 Java 虚拟机》读书笔记:Java 内存区域与内存溢出异常
前言 最近开始看这本书,记得前段时间拿起这本书的时候,心情是相当沉重的!当时的剧本是这样的-- 内景.家里 - 下午 我(画外):唉,有点无聊啊!(偶然撇过书架)这么多书得看到什么时候啊,要不要拿一本 ...
- 深入理解java虚拟机系列(一):java内存区域与内存溢出异常
文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了開始.假设有什么错误或者遗漏,欢迎指出. 一.概述 先上 ...
- 《深入理解Java虚拟机》-----第2章 Java内存区域与内存溢出异常
2.1 概述 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任 ...
- 《深入理解 Java 虚拟机》学习 -- Java 内存模型
<深入理解 Java 虚拟机>学习 -- Java 内存模型 1. 区别 这里要和 JVM 内存模型区分开来: JVM 内存模型是指 JVM 内存分区 Java 内存模型(JMM)是指一种 ...
- Java内存区域与内存溢出异常——深入理解Java虚拟机 笔记一
Java内存区域 对比与C和C++,Java程序员不需要时时刻刻在意对象的创建和删除过程造成的内存溢出.内存泄露等问题,Java虚拟机很好地帮助我们解决了内存管理的问题,但深入理解Java内存区域,有 ...
- 深入理解Java虚拟机之Java内存区域与内存溢出异常
Java内存区域与内存溢出异常 运行时数据区域 程序计数器 用于记录从内存执行的下一条指令的地址,线程私有的一小块内存,也是唯一不会报出OOM异常的区域 Java虚拟机栈 Java虚拟机栈(Java ...
- 《深入理解Java虚拟机》——Java内存区域与内存溢出异常
程序计数器(Program Counter Register):一块较小的内存空间,可看作是当前线程所执行的字节码的行号指示器.字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令 ...
- 深入理解Java虚拟机02--Java内存区域与内存溢出异常
一.概述 我们在进行 Java 开发的时候,很少关心 Java 的内存分配等等,因为这些活都让 JVM 给我们做了.不仅自动给我们分配内存,还有自动的回收无需再占用的内存空间,以腾出内存供其他人使用. ...
- 《深入理解java虚拟机》第二章 Java内存区域与内存溢出异常
第二章 Java内存区域与内存溢出异常 2.2 运行时数据区域
随机推荐
- <转>单机版搭建Hadoop环境
安装过程: 一.安装Linux操作系统 二.在Ubuntu下创建hadoop用户组和用户 三.在Ubuntu下安装JDK 四.修改机器名 五.安装ssh服务 六.建立ssh无密码登录本机 七.安装ha ...
- Git远程操作(附重要原理图)
原文出处: 阮一峰 Git是目前最流行的版本管理系统,学会Git几乎成了开发者的必备技能. Git有很多优势,其中之一就是远程操作非常简便.本文详细介绍5个Git命令,它们的概念和用法,理解了这些内容 ...
- who 命令的实现
who 命令显示 当前已经登录的用户 查看连接帮助: man who who(1) 表示who的小结编号. NAME 包含命令的名字以及对这个命令的简短说明 SYNOPSYS 给出命令的用法说明,命 ...
- Docker 安装&卸载
不同版本可能有差异具体信息查看官网 官网:https://docs.docker.com/engine/install/centos/ #环境准备 #查看环境 uname -r # 系统内核在3.10 ...
- CF1076B Divisor Subtraction 题解
Content 给定一个数 \(n\),执行如下操作: 如果 \(n=0\) 结束操作. 找到 \(n\) 的最小质因子 \(d\). \(n\leftarrow n-d\) 并跳到操作 \(1\). ...
- Jenkins安装部署使用图文详解(非常详细)
前言 最近公司需要弄一套自动化运维部署,于是抽空学习了一下,用了两天左右完成Jenkins的安装部署和各种项目的配置化,于是整理一下进行分享. 介绍 Jenkins是一个独立的开源软件项目,是基于Ja ...
- qt5之设置无边窗口移动
Note qt version: 5.12 qt creator: 4.13 本文将介绍 设置无边窗口和设置窗口的移动 你要知道: QDialog 和 QMainWindow都是 QWidget的派生 ...
- 【LeetCode】762. Prime Number of Set Bits in Binary Representation 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 遍历数字+质数判断 日期 题目地址:https:// ...
- The Expressive Power of Neural Networks: A View from the Width
目录 概 主要内容 定理1 定理2 定理3 定理4 定理1的证明 Lu Z, Pu H, Wang F, et al. The expressive power of neural networks: ...
- CS5210完全替代AG6202|HDMI转VGA不带音频输出的芯片+原理图|替代兼容AG6202
CS5210完全替代AG6202|HDMI转VGA不带音频输出的芯片+原理图|替代兼容AG6202 安格AG6202是一个HDMI转VGA不带音频解决方案,用于实现HDMI1.4高分辨率视频转VGA转 ...