Java内存区域与内存溢出

简介:
内存区域
   Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域。Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。下面详细阐述各数据区所存储的数据类型。
  程序计数器(Program Counter Register)
  一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,字节码解释器 工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。
  当线程在执行一个Java方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是Native方法(调用本地 操作系统方法)时,该计数器的值为空。另外,该内存区域是唯一一个在Java虚拟机规范中么有规定任何OOM(内存溢出:OutOfMemoryError)情况的区域。
  Java虚拟机栈(Java Virtual Machine Stacks)
  该区域也是线程私有的,它的生命周期也与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,栈它是用于支持续虚拟机进行方法调用和方法执行的数据结构。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的Code属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
   在Java虚拟机规范中,对这个区域规定了两种异常情况:
  1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
  2、如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
  这里有一点要重点说明,在多线程情况下,给每个线程的栈分配的内存越大,越容易产生内存溢出异常。操作系统为每个进程分配的内存是有限制的,虚拟机提供了参数来控制Java堆和方法区这两部分内存的最大值,忽略掉程序计数器消耗的内存(很小),以及进程本身消耗的内存,剩下的内存便给了虚拟机栈和本地方法栈,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少。因此,如果是建立过多的线程导致的内存溢出,在不能减少线程数的情况下,就只能通过减少最大堆和每个线程的栈容量来换取更多的线程。当由于创建过量线程发生OOM时,会报错:java.lang.OutOfMemoryError, unable to create new native thread。
  本地方法栈(Native Method Stacks)
  该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError与OutOfMemoryError异常。
   Java堆(Java Heap)
  Java Heap是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域,几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
  根据Java虚拟机规范的规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。
  注意:随着JIT编译器的发展与逃逸技术逐渐成熟,所有对象都分配在堆上也逐渐变得不是那么绝对了,线程共享的Java堆中也可能划分出线程私有的分配缓冲区(TLAB)。
   方法区(Method Area)
  方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,而且它和Java Heap一样不需要连续的内存,可以选择固定大小或可扩展,另外,虚拟机规范允许该区域可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域比较少出现。不过,这部分区域的回收是有必要的,如果这部分区域永远不回收,那么类型就无法卸载,我们就无法加载更多的类,HotSpot的该区域有实现垃圾回收。
  根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
直接内存(Direct Memory)
  直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,因此不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式,可以直接从操作系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。
  当使用超过虚拟机允许的直接内存时,虚拟机会抛出OutOfMemoryError异常,由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常。一般来说,如果发现OOM后Dump文件很小,那就应该考虑一下,是不是这块内存发生了溢出。
   内存溢出
  Java堆内存溢出
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
  运行以上代码时,可以增加运行参数-Xms20m -Xmx20m,该参数限制Java堆大小为20M,不可扩展。运行结果如下:
<span style="color: #ff0000;">Exception in thread "main" java.lang.OutOfMemoryError: Java heap space</span>
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:440)
at HeapOOM.main(HeapOOM.java:17)
  可以看到,在堆内存溢出时,除了会报错java.lang.OutOfMemoryError外,还会跟着进一步提示Java heap space。
   虚拟机栈和本地方法栈溢出
  要让虚拟机栈内存溢出,我们可以使用递归调用:因为每次方法调用都需要向栈中压入调用信息,当栈的大小固定时,过深的递归将向栈中压入过量信息,导致
StackOverflowError:
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();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
  运行以上代码,输出如下:
  stack length:10828
  <span style="color: #ff0000;">Exception in thread "main" java.lang.StackOverflowError</span>
  at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
  at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
  at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
  可以看到,在我的电脑上运行以上代码,最多支持的栈深度是10828层,当发生栈溢出时,会报错java.lang.StackOverflowError。
   方法区溢出
  方法区用于存放Class的相关信息,如类名、访问修饰符、字段描述等,对于这个区域的测试,基本思路是运行时使用CGLib产生大量的类去填充方法区,直到溢出:
public class JavaMethodAreaOOM {
static class OOMObject {
}
public static void main(String[] args) {
while( true) {
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();
}
}
}
  运行时增加虚拟机参数:-XX:PermSize=10M -XX:MaxPermSize=10M,限制永久代大小为10M,最后报错为java.lang.OutOfMemoryError: PermGen space。报错信息明确说明,溢出区域为永久代。
   总结
  本文主要说明Java虚拟机一共分为哪几块内存区域,以及这几块内存区域是否会内存溢出,如果这些区域发生内存溢出报错如何。了解这些知识后,以后遇到内存溢出报错,我们就可以定位到具体内存区域,然后具体问题,具体分析。

最新内容请见作者的GitHub页:http://qaseven.github.io/
相关文章
|
9月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
267 4
|
9月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
9月前
|
Web App开发 缓存 监控
内存溢出与内存泄漏:解析与解决方案
本文深入解析内存溢出与内存泄漏的区别及成因,结合Java代码示例展示典型问题场景,剖析静态集合滥用、资源未释放等常见原因,并提供使用分析工具、优化内存配置、分批处理数据等实用解决方案,助力提升程序稳定性与性能。
2087 1
|
10月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
1116 3
|
10月前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
1053 17
|
11月前
|
存储 监控 算法
Java垃圾回收机制(GC)与内存模型
本文主要讲述JVM的内存模型和基本调优机制。
|
11月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
11月前
|
边缘计算 算法 Java
Java 绿色计算与性能优化:从内存管理到能耗降低的全方位优化策略与实践技巧
本文探讨了Java绿色计算与性能优化的技术方案和应用实例。文章从JVM调优(包括垃圾回收器选择、内存管理和并发优化)、代码优化(数据结构选择、对象创建和I/O操作优化)等方面提出优化策略,并结合电商平台、社交平台和智能工厂的实际案例,展示了通过Java新特性提升性能、降低能耗的显著效果。最终指出,综合运用这些优化方法不仅能提高系统性能,还能实现绿色计算目标,为企业节省成本并符合环保要求。
338 0
|
11月前
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
614 2
|
12月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
587 0