写点什么

理解 JVM 工作机制(一) JVM 内存区域

作者:ue4
  • 2023-05-23
    上海
  • 本文字数:2237 字

    阅读完需:约 7 分钟

JVM 内存区域

Java 虚拟机的内存空间分为 5 个部分


  • 程序计数器

  • Java 虚拟机栈

  • 本地方法栈

  • 方法区



JDK 1.8 同 JDK 1.7 比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

程序计数器

是一块较小的内存空间,用于执行当前线程的指令、如分支、循环、跳转、异常处理以及线程恢复()等基础功能。


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


特点:


  • 是一块较小的内存空间。

  • 线程私有,每条线程都有自己的程序计数器。

  • 生命周期:随着线程的创建而创建,随着线程的结束而销毁。

  • 是唯一一个不会出现OutOfMemoryError的内存区域。

Java 虚拟机栈

Java 虚拟机栈是描述 Java 方法运行过程的内存模型。


Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如:


  • 局部变量表

  • 操作数栈

  • 动态连接

  • 方法出口

  • .........


压栈出栈过程

当方法运行过程中需要创建局部变量时,就将局部变量的值存入栈帧中的局部变量表中。


Java 虚拟机栈的栈顶的栈帧是当前正在执行的活动栈,也就是当前正在执行的方法,PC 寄存器也会指向这个地址。只有这个活动的栈帧的本地变量可以被操作数栈使用,当在这个栈帧中调用另一个方法,与之对应的栈帧又会被创建,新创建的栈帧压入栈顶,变为当前的活动栈帧。


方法结束后,当前栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。如果没有返回值,那么新的活动栈帧中操作数栈的操作数没有变化。


由于 Java 虚拟机栈是与线程对应的,数据不是线程共享的,因此不用关心数据一致性问题,也不会存在同步锁的问题。

Java 虚拟机栈的特点
  • 局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。

  • Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError 若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常。

  • OutOfMemoryError 若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 异常。

  • Java 虚拟机栈也是线程私有,随着线程创建而创建,随着线程的结束而销毁。


出现 StackOverFlowError 时,内存空间可能还有很多。

本地方法栈

作用和虚拟机栈发挥的作用相似,区别在于虚拟机栈执行 Java 方法(字节码)服务,而本地方法站则是为虚拟机使用到的 Native 方法服务

Java 堆

堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。

堆的特点
  • 线程共享,整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个。

  • 在虚拟机启动时创建。

  • 是垃圾回收的主要场所。

  • 进一步可分为:新生代(Eden 区、From Survior、To Survivor)、老年代。


不同的区域存放不同生命周期的对象,这样可以根据不同的区域使用不同的垃圾回收算法,更具有针对性。


堆的大小既可以固定也可以扩展,但对于主流的虚拟机,堆的大小是可扩展的,因此当线程请求分配内存,但堆已满,且内存已无法再扩展时,就抛出 OutOfMemoryError 异常。


Java 堆处于物理上不连续的空间中。但在逻辑上它应该被视为连续的。而由于堆是被所有线程共享的,所以对它的访问需要注意同步问题,方法和对应的属性都需要保证一致性。



方法区

Java 虚拟机规范中定义方法区是堆的一个逻辑部分。方法区存放以下信息:


  • 类型信息

  • 常量

  • 静态变量

  • 即时编译器编译后代码缓存数据

  • ...............

方法区的特点
  • 线程共享。 方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。

  • 永久代。 方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,把方法区称为“永久代”。

  • 内存回收效率低。 方法区中的信息一般需要长期存在,回收一遍之后可能只有少量信息无效。主要回收目标是:对常量池的回收;对类型的卸载。

  • Java 虚拟机规范对方法区的要求比较宽松。 和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收



运行时常量池

方法区中存放:类信息、常量、静态变量、即时编译器编译后的代码。常量就存放在运行时常量池中。


当类被 Java 虚拟机加载后, .class 文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如 String 类的 intern() 方法就能在运行期间向常量池中添加字符串常量。

直接内存

直接内存是在 Java 虚拟机之外的内存。


NIO 就是在 Native 函数库里直接分配堆外内存,然后通过一个存储在 Java 堆里面的DirectByteBuffer对象来作为这块内存的引用进行操作。


本机直接内存的分配不会受到 Java 堆大小的限制(上限是本机总内存),如果各个内存区域总和大于物理内存限制,会抛出OutOfMemoryError 异常

直接内存与堆内存比较
  • 直接内存申请空间耗费更高的性能

  • 直接内存读取 IO 的性能要优于普通的堆内存。

  • 直接内存作用链: 本地 IO -> 直接内存 -> 本地 IO

  • 堆内存作用链:本地 IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地 IO


服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。



发布于: 刚刚阅读数: 4
用户头像

ue4

关注

还未添加个人签名 2020-08-26 加入

还未添加个人简介

评论

发布
暂无评论
理解JVM工作机制(一) JVM内存区域_Java_ue4_InfoQ写作社区