深入jvm虚拟机内存区域与OOM

2024-05-06 16:38

本文主要是介绍深入jvm虚拟机内存区域与OOM,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却
想出来。
概述:
对于从事C、 C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基
础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任。
对于Java程序员来说,不需要在为每一个new操作去写配对的delete/free,不容易出现内容泄漏和内存溢
出错误,看起来由JVM管理内存一切都很美好。不过,也正是因为Java程序员把内存控制的权力交给了JVM,
一旦出现泄漏和溢出,如果不了解JVM是怎样使用内存的,那排查错误将会是一件非常困难的事情。
VM运行时数据区域
JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。根据
《Java虚拟机规范(第二版)》(下文称VM Spec)的规定, JVM包括下列几个运行时数据区域:
1.程序计数器( Program Counter Register):
每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令,对于非Native方法,
这个区域记录的是正在执行的VM原语的地址,如果正在执行的是Natvie方法,这个区域则为空
( undefined)。此内存区域是唯一一个在VM Spec中没有规定任何OutOfMemoryError情况的区域。
2.Java虚拟机栈( Java Virtual Machine Stacks)
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 87 / 121 页
与程序计数器一样, VM栈的生命周期也是与线程相同。 VM栈描述的是Java方法调用的内存模型:每个方
法被执行的时候,都会同时创建一个帧( Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等
信息。每一个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。在后文中,我们将着重
讨论VM栈中本地变量表部分。
经常有人把Java内存简单的区分为堆内存( Heap)和栈内存( Stack),实际中的区域远比这种观点复
杂,这样划分只是说明与变量定义密切相关的内存区域是这两块。其中所指的“堆”后面会专门描述,而所
指的“栈”就是VM栈中各个帧的本地变量表部分。本地变量表存放了编译期可知的各种标量类型
( boolean、 byte、 char、 short、 int、 float、 long、 double)、对象引用(不是对象本身,仅仅是一个
引用指针)、方法返回地址等。其中long和double会占用2个本地变量空间( 32bit),其余占用1个。本
地变量表在进入方法时进行分配,当进入一个方法时,这个方法需要在帧中分配多大的本地变量是一件完全
确定的事情,在方法运行期间不改变本地变量表的大小。
在VM Spec中对这个区域规定了2中异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛
出StackOverflowError异常;如果VM栈可以动态扩展( VM Spec中允许固定长度的VM栈),当扩展时无
法申请到足够内存则抛出OutOfMemoryError异常。
3.本地方法栈( Native Method Stacks)
本地方法栈与VM栈所发挥作用是类似的,只不过VM栈为虚拟机运行VM原语服务,而本地方法栈是为虚拟
机使用到的Native方法服务。它的实现的语言、方式与结构并没有强制规定,甚至有的虚拟机(譬如Sun
Hotspot虚拟机)直接就把本地方法栈和VM栈合二为一。和VM栈一样,这个区域也会抛
出StackOverflowError和OutOfMemoryError异常。
4.Java堆( Java Heap)
对于绝大多数应用来说, Java堆是虚拟机管理最大的一块内存。 Java堆是被所有线程共享的,在虚拟机启
动时创建。 Java堆的唯一目的就是存放对象实例,绝大部分的对象实例都在这里分配。这一点在VM
Spec中的描述是:所有的实例以及数组都在堆上分配(原文: The heap is the runtime data area from
which memory for all class instances and arrays is allocated),但是在逃逸分析和标量替换优化技术
出现后, VM Spec的描述就显得并不那么准确了。
Java堆内还有更细致的划分:新生代、老年代,再细致一点的: eden、 from survivor、 to survivor,甚至
更细粒度的本地线程分配缓冲( TLAB)等,无论对Java堆如何划分,目的都是为了更好的回收内存,或者
更快的分配内存,在本章中我们仅仅针对内存区域的作用进行讨论, Java堆中的上述各个区域的细节,可参
见本文第二章《JVM内存管理:深入垃圾收集器与内存分配策略》。
根据VM Spec的要求, Java堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁
盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 88 / 121 页
可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛
出OutOfMemoryError异常。
5.方法区( Method Area)
叫“方法区”可能认识它的人还不太多,如果叫永久代( Permanent Generation)它的粉丝也许就多了。
它还有个别名叫做Non-Heap(非堆),但是VM Spec上则描述方法区为堆的一个逻辑部分(原文: the
method area is logically part of the heap),这个名字的问题还真容易令人产生误解,我们在这里就不
纠结了。
方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。 VM Space描述中对这个
区域的限制非常宽松,除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以
选择不实现垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永
久代不会发生GC(至少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类
的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。
6.运行时常量池( Runtime Constant Pool)
Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool
table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语
言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内
容放入常量池(最典型的String.intern()方法)。
运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛
出OutOfMemoryError异常。
7.本机直接内存( Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,它根本就是本机内存而不是VM直接管理的区域。但是这部
分内存也会导致OutOfMemoryError异常出现,因此我们放到这里一起描述。
在JDK1.4中新加入了NIO类,引入一种基于渠道与缓冲区的I/O方式,它可以通过本机Native函数库直接分
配本机内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样
能在一些场景中显著提高性能,因为避免了在Java对和本机堆中来回复制数据。
显然本机直接内存的分配不会受到Java堆大小的限制,但是即然是内存那肯定还是要受到本机物理内存(包
括SWAP区或者Windows虚拟内存)的限制的,一般服务器管理员配置JVM参数时,会根据实际内存设置-
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 89 / 121 页
Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作
系统级的限制),而导致动态扩展时出现OutOfMemoryError异常。
实战OutOfMemoryError
上述区域中,除了程序计数器,其他在VM Spec中都描述了产生OutOfMemoryError(下称OOM)的情
形,那我们就实战模拟一下,通过几段简单的代码,令对应的区域产生OOM异常以便加深认识,同时初步介绍
一些与内存相关的虚拟机参数。下文的代码都是基于Sun Hotspot虚拟机1.6版的实现,对于不同公司的不同版
本的虚拟机,参数与程序运行结果可能结果会有所差别。
Java堆
Java堆存放的是对象实例,因此只要不断建立对象,并且保证GC Roots到对象之间有可达路径即可产
生OOM异常。测试中限制Java堆大小为20M,不可扩展,通过参数-
XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现OOM异常的时候Dump出内存映像以便分析。(关
于Dump映像文件分析方面的内容,可参见本文第三章《JVM内存管理:深入JVM内存异常分析与调优》。)
清单1: Java堆OOM测试
/**
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author zzm
*/
public class HeapOOM {
static class OOMObject {
}
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 90 / 121 页
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]
VM栈和本地方法栈
Hotspot虚拟机并不区分VM栈和本地方法栈,因此-Xoss参数实际上是无效的,栈容量只由-Xss参数设
定。关于VM栈和本地方法栈在VM Spec描述了两种异常: StackOverflowError与OutOfMemoryError,当栈
空间无法继续分配分配时,到底是内存太小还是栈太大其实某种意义上是对同一件事情的两种描述而已,在笔
者的实验中,对于单线程应用尝试下面3种方法均无法让虚拟机产生OOM,全部尝试结果都是获得SOF异常。
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 91 / 121 页
1.使用-Xss参数削减栈内存容量。结果:抛出SOF异常时的堆栈深度相应缩小。
2.定义大量的本地变量,增大此方法对应帧的长度。结果:抛出SOF异常时的堆栈深度相应缩小。
3.创建几个定义很多本地变量的复杂对象,打开逃逸分析和标量替换选项,使得JIT编译器允许对象拆分后
在栈中分配。结果:实际效果同第二点。
清单2: VM栈和本地方法栈OOM测试(仅作为第1点测试程序)
/**
* VM Args: -Xss128k
* @author zzm
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 92 / 121 页
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
运行结果:
stack length:2402
Exception in thread "main" java.lang.StackOverflowError
at
org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20)
at
org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
at
org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
如果在多线程环境下,不断建立线程倒是可以产生OOM异常,但是基本上这个异常和VM栈空间够不够关
系没有直接关系,甚至是给每个线程的VM栈分配的内存越多反而越容易产生这个OOM异常。
原因其实很好理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows限制为2G, Java堆和
方法区的大小JVM有参数可以限制最大值,那剩余的内存为2G(操作系统限制) -Xmx(最大堆) -
MaxPermSize(最大方法区),程序计数器消耗内存很小,可以忽略掉,那虚拟机进程本身耗费的内存不计算
的话,剩下的内存就供每一个线程的VM栈和本地方法栈瓜分了,那自然每个线程中VM栈分配内存越多,就越
容易把剩下的内存耗尽。
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 93 / 121 页
清单3:创建线程导致OOM异常
/**
* VM Args: -Xss2M (这时候不妨设大些)
* @author zzm
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 94 / 121 页
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
特别提示一下,如果读者要运行上面这段代码,记得要存盘当前工作,上述代码执行时有很大令操作系统卡
死的风险。
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create
new native thread
运行时常量池
要在常量池里添加内容,最简单的就是使用String.intern()这个Native方法。由于常量池分配在方法区内,
我们只需要通过-XX:PermSize和-XX:MaxPermSize限制方法区大小即可限制常量池容量。实现代码如下:
清单4:运行时常量池导致的OOM异常
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 95 / 121 页
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author zzm
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池引用,压制Full GC回收常量池行为
List<String> list = new ArrayList<String>();
// 10M的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at
org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
深入java虚拟机 4.1 JVM内存管理:深入Java内存区域与OOM
第 96 / 121 页
方法区
上文讲过,方法区用于存放Class相关信息,所以这个区域的测试我们借助CGLib直接操作字节码动态生成
大量的Class,值得注意的是,这里我们这个例子中模拟的场景其实经常会在实际应用中出现:当前很多主流框
架,如Spring、 Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,当增强的类越多,就需要越大
的方法区用于保证动态生成的Class可以加载入内存。
清单5:借助CGLib使得方法区出现OOM异常
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author zzm
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) throws Throwable {


这篇关于深入jvm虚拟机内存区域与OOM的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/964843

相关文章

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001

Java面试题:通过实例说明内连接、左外连接和右外连接的区别

在 SQL 中,连接(JOIN)用于在多个表之间组合行。最常用的连接类型是内连接(INNER JOIN)、左外连接(LEFT OUTER JOIN)和右外连接(RIGHT OUTER JOIN)。它们的主要区别在于它们如何处理表之间的匹配和不匹配行。下面是每种连接的详细说明和示例。 表示例 假设有两个表:Customers 和 Orders。 Customers CustomerIDCus