`
talentkep
  • 浏览: 100507 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

JVM内存管理:深入Java内存区域与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

与程序计数器一样, 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 堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照可扩展来实现的(通过 -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 参数时,会根据实际内存设置 -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 {

       }

 

       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 异常。

 

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();

              } 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 栈分配内存越多,就越容易把剩下的内存耗尽。

 

清单 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();

              }

       }

 

       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 异常

/**

  * 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)

 

 

方法区

 

上文讲过,方法区用于存放 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 {

                                   return proxy.invokeSuper(obj, args);

                            }

                     });

                     enhancer.create();

              }

       }

 

       static class OOMObject {

 

       }

}

 

运行结果:

Caused by: java.lang.OutOfMemoryError: PermGen space

       at java.lang.ClassLoader.defineClass1(Native Method)

       at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)

       at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

       ... 8 more

 

本机直接内存

 

DirectMemory 容量可通过 -XX:MaxDirectMemorySize 指定,不指定的话默认与 Java 堆( -Xmx 指定)一样,下文代码越过了 DirectByteBuffer ,直接通过反射获取 Unsafe 实例进行内存分配( Unsafe 类的 getUnsafe() 方法限制了只有引导类加载器才会返回实例,也就是基本上只有 rt.jar 里面的类的才能使用),因为 DirectByteBuffer 也会抛 OOM 异常,但抛出异常时实际上并没有真正向操作系统申请分配内存,而是通过计算得知无法分配既会抛出,真正申请分配的方法是 unsafe.allocateMemory()

 

/**

  * VM Args -Xmx20M -XX:MaxDirectMemorySize=10M

  * @author zzm

  */

public class DirectMemoryOOM {

 

       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);

              }

       }

}

 

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError

       at sun.misc.Unsafe.allocateMemory(Native Method)

       at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)

 

 

总结

到此为止,我们弄清楚虚拟机里面的内存是如何划分的,哪部分区域,什么样的代码、操作可能导致 OOM 异常。虽然 Java 有垃圾收集机制,但 OOM 仍然离我们并不遥远,本章内容我们只是知道各个区域 OOM 异常出现的原因,下一章我们将看看 Java 垃圾收集机制为了避免 OOM 异常出现,做出了什么样的努力。

分享到:
评论

相关推荐

    Tomcat JVM内存设置方法

    在深入探讨如何设置Tomcat的JVM内存之前,我们需要先了解JVM内存的基本结构。JVM内存主要分为以下几个部分: 1. **堆内存(Heap Memory)**:这是JVM管理的主要内存区域之一,用于存储对象实例以及数组等数据。堆...

    JVM内存管理、调优与监控考据

    总的来说,JVM内存管理、调优与监控是一项综合性的技术工作,需要对JVM内部机制有深入理解,并结合具体应用场景进行细致的分析和实践。由于JVM的实现可能存在差异,且规范与实现之间存在一定的不一致性,因此,进行...

    java入门、java内存区域和OOM、垃圾回收器和垃圾回收策略

    本教程将涵盖Java的基础知识,特别是关于内存管理的重要概念——Java内存区域、Out of Memory (OOM)错误以及垃圾回收器和垃圾回收策略。 1. **Java入门**: Java的学习始于基础语法,包括变量、数据类型、运算符、...

    笔记,1、虚拟机的前世今生,深入理解JVM内存区域1

    《深入理解JVM内存区域》 Java虚拟机(JVM)是Java语言的运行环境,支持多种语言,包括Scala、Kotlin、Groovy等。虚拟机历史了解即可,无需关注Hotspot。 JVM内存区域主要分为五部分:程序计数器、虚拟机栈、本地...

    jvm内存的运作

    #### 一、Java内存区域与OOM 在深入了解JVM内存运作机制之前,我们需要认识到Java与C/C++之间的显著区别之一在于内存管理。C/C++程序员需要手动管理内存,这意味着他们需要显式地分配和释放内存资源。而Java程序员...

    Tomcat内存溢出的解决方法(java.util.concurrent.ExecutionException)

    在Java中,这主要与JVM(Java虚拟机)的内存模型有关,该模型包括堆(Heap)、栈(Stack)、方法区(Method Area)和程序计数器(PC Register)等几个区域。当堆或方法区的内存耗尽时,就会抛出`OutOfMemoryError`。...

    Java虚拟机_JVM_参数配置

    通过深入了解和调整JVM参数,我们可以提升Java应用的性能,减少垃圾收集的开销,避免内存溢出等问题,从而确保应用程序稳定高效地运行。在阅读《Java 6 JVM参数选项大全(中文版)》这份文档时,可以找到更多具体的...

    精选_毕业设计_基于JAVA的内存管理模拟_完整源码

    本毕业设计项目——“基于JAVA的内存管理模拟”深入探讨了Java内存模型及其管理机制,通过模拟实现来帮助理解这些概念。下面将详细阐述相关知识点。 1. **Java内存模型**:Java内存模型(JVM Memory Model)定义了...

    jvm_jvm新手_jvm_

    7. **JVM内存模型**(JMM): - **主内存**:所有线程共享的内存区域,包含变量。 - **工作内存**:每个线程有自己的工作内存,存储副本变量。 - **可见性和一致性**:通过volatile、synchronized、final关键字来...

    java内存分析-内存泄露问题.rar

    本文将深入探讨Java内存分析和内存泄露问题。 首先,我们需要了解Java内存模型的基础。Java内存主要分为三个区域:堆(Heap)、栈(Stack)和方法区(Method Area)。堆用于存储对象实例,栈用于存储方法调用及局部...

    SPARK内存管理机制最全!

    Java堆内存是JVM管理的内存区域,用于存放对象实例,而堆外内存则是不经过JVM堆直接通过Java本地接口分配的内存。堆外内存的管理赋予了Spark更高效的内存使用,包括减少GC(垃圾回收)的开销。 Spark内存管理机制...

    基于Java的内存泄露分析及定位

    Java内存管理是一个关键的议题,尤其对于开发大型和长期运行的应用程序来说,内存泄漏可能导致性能下降,甚至引发严重的系统故障。内存泄漏通常发生在程序错误地管理内存,导致某些不再使用的对象无法被垃圾收集器...

    java解决nested exception is java.lang.OutOfMemoryError Java heap space

    为了解决这个问题,我们需要深入理解Java内存模型以及如何调整JVM的内存设置。Java内存主要分为三个区域:堆(Heap)、栈(Stack)和方法区(Method Area),其中堆是用于存储对象实例的主要区域,当堆空间不足时,...

    用于复现 OOM bug,模拟JVM调优经历-JVMTest.zip

    通过对`JVMTest`项目的实践,我们可以更深入地理解JVM内存管理,学会如何设置合适的JVM参数,避免`OOM`,提升应用的稳定性和性能。同时,这也是一种模拟真实环境的训练方式,对于开发者来说,具有很高的学习价值。

    jvm crash的崩溃日志详细分析及注意点

    同时,还要检查堆栈跟踪,确定哪些线程或方法在崩溃时刻正在执行,并结合Java堆、方法区、元数据等内存区域的状态进行分析。 总之,理解和分析JVM崩溃日志是诊断和解决Java应用程序性能问题的关键步骤。通过深入...

    Java内存各部分OOM出现原因及解决方法(必看)

    本文将深入探讨Java内存的各个区域,以及它们可能导致的OOM问题及其解决方案。 首先,Java内存主要分为以下几个区域: 1. **程序计数器**:这是一个非常小的内存区域,记录了当前线程正在执行的字节码的行号指示器...

    JVM成神之路笔记整理版

    8. **JVM内存溢出与异常**:学习如何识别和处理常见的JVM错误,如OOM(Out Of Memory)错误,以及如何通过调整JVM配置避免这些问题。 9. **JDK和JRE**:JDK包含JRE和开发工具,如编译器javac和调试器jdb。JRE则包含...

    JAVA内存溢出详解.doc

    Java内存溢出(Out Of Memory,OOM)是Java应用程序运行时常见的问题,它通常发生在程序对内存需求超过了Java虚拟机(JVM)所能提供的可用内存时。本文将深入探讨Java内存溢出的原因、表现以及如何解决。 1. **Java...

Global site tag (gtag.js) - Google Analytics