欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-02-26 18:22:58
...

参考资料:《深入理解Java虚拟机:JVM高级特性与最佳实践》 周志明 著

     因为有虚拟机的自动内存管理机制,Java程序员不再需要为每个new操作去写配对的delete/free代码(相对C/C++程序员),而且不容易出现内存泄漏和内存溢出问题。

     本文就Java虚拟机(JVM,下文以JVM代替)内存的各个区域,做一个详细的介绍。

运行时数据区域

     JVM在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范(第二版)》的规定,JVM所管理的内存将会包括以下几个运行时数据区域,如下图1:

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

程序计数器

程序计数器(Program Counter Register):一块较小的内存空间。

  • 作用:当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,包括分支、循环、跳转、异常处理和线程回复等基础功能。
  • 类型:线程私有。由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程的指令。为确保线程切换后能恢复到正确的执行位置,每条线程必须有独立的程序计数器。
  • 存储内容:
                若线程执行的是Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;
                若线程执行的是Native(本地方法),程序计数器值为空(Undefined)。
  • 异常情况:不会出现异常(唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的内存区域

Java虚拟机栈

  • 作用:描述Java方法执行的内存模型:每个方法被执行时都会创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用一直到执行完成的过程,对应一个栈帧在Java虚拟机栈中从入栈到出栈的过程。
  • 类型:线程私有;生命周期与线程相同;可扩展。
  • 存储内容:方法被调用时创建的栈帧。
            Java内存区域与内存溢出异常(深入理解Java虚拟机笔记(一))
  • 异常情况:两种。

            *Error异常:线程请求栈深度大于JVM所允许的深度时,抛出该异常;

            OutOfMemoryError异常:虚拟机栈扩展到无法申请到足够内存时,抛出该异常。

本地方法栈

  • 作用:类似于虚拟机栈。为JVM使用到的Native方法服务。
  • 类型:线程私有。
  • 存储内容:栈帧。
  • 异常情况:与Java虚拟机栈相同。
  • 补充:虚拟机规范中对本地方法栈中的方法使用的词语、使用方式、数据结构没有强制规定,具体的JVM可以*实现它。有些JVM把虚拟机栈和本地方法栈合二为一。

Java堆

Java堆(Java Heap):JVM所管理的内存中最大的一块。JVM启动时创建。

  • 作用:

            1.存放对象实例;

            2.垃圾收集器(GC)管理的主要区域,别名“GC堆”;

            3.从内存回收角度,Java堆可细分为:新生代和老年代;

                更细可分为:Eden空间、From Survivor空间、To Survivor空间;

            4.从内存分配角度,可划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

            注意:随着JIT编译器的发展与逃逸分析技术的逐渐成熟,所有的对象都在分配在堆上不是那么绝对了,有些对象实例可以分配在栈上。
  • 类型:线程共享;java堆可以处于物理上不连续的内存空间中,逻辑连续即可;可扩展。
  • 存储内容:对象实例。
  • 异常情况:堆中没有内存完成实例分配,且无法扩展时,抛出OutOfMemoryError异常。

方法区

  • 作用:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 注意:Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却非堆。
  • 类型:线程共享;内存在物理上可不连续;可扩展;可选择不实现垃圾收集。
  • 存储内容:
            方法区:被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;运行时常量池(具有动态性)。

            运行时常量池:存放Class文件中的常量池、翻译出来的直接引用;

            Class文件:存放有类的版本、字段、方法、接口等描述信息、常量池;

            Class文件中常量池:存放编译期生成的各种字面量和符号引用。
  • 异常情况:
            方法区无法满足内存分配需求时,抛出OutOfMemoryError异常;
            运行时常量池无法申请到内存时,抛出OutOfMemoryError异常。

直接内存

1.不是JVM运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但也是被频繁使用的内存部分。

2.在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲去(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用操作。避免了在Java堆和Native堆中来回复制数据,在一些场景中可以显著提高性能。

3.本机直接内存的分配不受Java堆大小的限制,受本机总内存的大小和处理器寻址空间的限制。

4.由于经常忽略直接内存,使得各个内存区域的总和大于物理内存限制,导致动态扩展时出现OutOfMemoryError异常。

对象访问

对象访问涉及Java栈、Java堆、方法区3个重要内存区域联系。以下面代码来分析。

Object obj = new Object();

假设这句代码出现在方法体中,

  1. “Object obj”这部分语义将反映到Java栈的本地变量表中,作为一个reference类型数据出现;
  2. “new Object()”这部分语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存长度不固定。另外,在Java堆中还必须包含能查找到此对象类型数据(对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据存储在方法区中。

reference类型,主流的访问方式有2种:使用句柄、直接指针。

  • 使用句柄访问方式:Java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。如下图:

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

  • 使用直接指针访问方式:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。如下图:

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

使用句柄访问方式的最大好处:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集是移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针访问方式的最大好处:速度更快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。(Sun HotSpot使用的是直接指针访问方式)


上一篇: 学习jvm(一)--java内存区域

下一篇: