JVM-内存管理

Posted turbosha

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM-内存管理相关的知识,希望对你有一定的参考价值。

一、运行时数据区域

JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。如下图所示:

技术图片

程序计数器

程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

当执行的线程数量超过 CPU 数量时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令,从而在线程切换后能恢复到正确的执行位置。各条线程间的计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存。

  • 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
  • 如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。

?? 注意:此内存区域是唯一一个在 JVM 中没有规定任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stacks) 也是线程私有的,它的生命周期与线程相同。 

每个 Java 方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储 局部变量表、操作数栈、常量池引用 等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

技术图片

  • 局部变量表 - 32 位变量槽,存放了编译期可知的各种基本数据类型、对象引用、ReturnAddress 类型。
  • 操作数栈 - 基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
  • 动态链接 - 每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态链接。
  • 方法出口 - 返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。

?? 注意:该区域可能抛出以下异常:

  • 如果线程请求的栈深度超过最大值,就会抛出 StackOverflowError 异常;
  • 如果虚拟机栈进行动态扩展时,无法申请到足够内存,就会抛出 OutOfMemoryError 异常。

?? 提示:可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小

java -Xss=512M HackTheJava

本地方法栈

本地方法栈(Native Method Stack) 与虚拟机栈的作用相似。

二者的区别在于:虚拟机栈为 Java 方法服务;本地方法栈为 Native 方法服务。本地方法并不是用 Java 实现的,而是由 C 语言实现的。

技术图片

?? 注意:本地方法栈也会抛出 StackOverflowError 异常和 OutOfMemoryError 异常。

Java 堆

Java 堆(Java Heap) 的作用就是存放对象实例,几乎所有的对象实例都是在这里分配内存。

Java 堆是垃圾收集的主要区域(因此也被叫做"GC 堆")。现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法。

因此虚拟机把 Java 堆分成以下三块:

  • 新生代(Young Generation)
    • Eden - Eden 和 Survivor 的比例为 8:1
    • From Survivor
    • To Survivor
  • 老年代(Old Generation)
  • 永久代(Permanent Generation)

当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。

技术图片

?? 注意:Java 堆不需要连续内存,并且可以动态扩展其内存,扩展失败会抛出 OutOfMemoryError 异常。

?? 提示:可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。

java -Xms=1M -Xmx=2M HackTheJava

方法区

方法区(Method Area)也被称为永久代。方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

?? 注意:和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

?? 提示:

  • JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收。可通过参数 -XX:PermSize 和 -XX:MaxPermSize 设置。
  • JDK 1.8 之后,取消了永久代,用 **metaspace(元数据)**区替代。可通过参数 -XX:MaxMetaspaceSize 设置。

运行时常量池

运行时常量池(Runtime Constant Pool) 是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容会在类加载后被放入这个区域。

  • 字面量 - 文本字符串、声明为 final 的常量值等。
  • 符号引用 - 类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。

?? 注意:当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域。

在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

?? 注意:直接内存这部分也被频繁的使用,且也可能导致 OutOfMemoryError 异常。

?? 提示:直接内存容量可通过 -XX:MaxDirectMemorySize 指定,如果不指定,则默认与 Java 堆最大值(-Xmx 指定)一样。

Java 内存区域对比

内存区域内存作用范围异常
程序计数器 线程私有
Java 虚拟机栈 线程私有 StackOverflowError 和 OutOfMemoryError
本地方法栈 线程私有 StackOverflowError 和 OutOfMemoryError
Java 堆 线程共享 OutOfMemoryError
方法区 线程共享 OutOfMemoryError
运行时常量池 线程共享 OutOfMemoryError
直接内存 非运行时数据区 OutOfMemoryError

以上是关于JVM-内存管理的主要内容,如果未能解决你的问题,请参考以下文章

详解Jvm内存结构

JVM - 内存管理

JVM自动内存管理机制——Java内存区域

JVM如何管理内存

JVM JVM内存模型

JVM内存区域管理算法-垃圾回收算法