java虚拟机笔记 运行时内存区域划分

前言

java虚拟机在执行java程序的过程中会把它所管理的内存划分成若干个不同的数据区域。jdk8以后,大体的内存划分如下。
在这里插入图片描述

程序计数器

  • 一块很小的内存空间,线程私有,存储当前线程下一条要运行的字节码指令的行号
  • 执行引擎通过行程序计数器中的行号找到对应的字节码指令,然后将字节码翻译成对应的机器语言,交给cpu执行
  • 程序运行的分支、分支、循环、跳转、异常处理、线程切换等功能都需要程序计数器实现
  • 此内存区域是唯一一个java虚拟机规范中没有规定任何OOM(OutOfMemoryError情况的区域),因为只需要要存储下一条字节码指令的地址,所以不会发生OOM。

虚拟机栈

  • java虚拟机栈是什么
    • 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应一次次的Java方法调用
    • 线程私有
  • 生命周期 :和线程一致
  • 作用: 主管java程序的运行,保存着方法的局部变量、部分结果,并参与方法的调用和返回
  • 常见的异常
    • StackOverflowError异常:线程请求的栈深度超过了虚拟机所允许的深度
    • OutOfMemoryError异常(OOM):当虚拟机允许动态扩展但扩展时无法申请到足够的内存,就会抛出OOM异常
  • 设置栈深度:
    • 通过 -Xss参数设置
    • 如 -Xss256k -Xss2m -Xss1g等

栈帧(Stack Frame)

每个线程都有自己栈, 线程上每个正在执行的方法都各自对应着一个栈帧,方法调用时,栈帧入栈;方法返回时,栈帧出栈
在这里插入图片描述

  • 存储结构
    在这里插入图片描述

局部变量表

  • 局部变量表用一个数组的形式存放方法的参数和方法内定义的局部变量
  • 数组的长度在编译期(java代码编译成class字节码)的时候就可以确认下来
  • 局部变量表的容量以Slot(变量槽)为最小单位,一个Slot可以存放一个32位(4字节)以内的数据类型,如boolean、byte、char、short、int、float、reference(引用类型)、returnAddress(返回地址)8种数据类型占据一个Slot;对于64位的数据类型则用两个连续的Slot空间来存储
    在这里插入图片描述

操作数栈

  • 操作数栈的最大深度在编译阶段就被确定
  • 在方法执行的过程种会有各种字节码指令往操作数栈中写入或提取内容,也就是出栈和入栈操作
  • 32位数据类型所占的栈容量为1,64位数据所占的栈容量为2
    在这里插入图片描述
    以上面a+b的操作为例
 
  1. 0 bipush 10 //将10压入操作数栈顶中

  2. 2 istore_1 //弹出栈顶元素,并存储在局部变量表中(索引为1)

  3. 3 bipush 20 //将20压入操作数栈顶中

  4. 5 istore_2 //弹出栈顶元素,并存储在局部变量表中(索引为2)

  5. 6 iload_1 //读取局部变量表索引为1的元素(10),并压入操作数栈栈顶

  6. 7 iload_2 //读取局部变量表索引为2的元素(20),并压入操作数栈栈顶

  7. 8 iadd //将栈顶的两个int值出栈,然后将相加的结果入栈

  8. 9 istore_3 //弹出栈顶元素,并存储在局部变量表中(索引为3)

  9. 10 return

局部变量表如下图
在这里插入图片描述

动态链接

Class文件的常量池中存在着大量的符号引用,这些符号引用一部分会在类的加载阶段的转换成直接引用(即具体的内存地址),这个转换称为静态解析;另外一部分将在每一次运行期间转化为直接引用,这部分称为动态链接。
在这里插入图片描述

方法返回地址

因为程序计数器是线程私有的,且每个线程都有且只有一个程序计数器,它只能记录一个方法执行到哪个位置,但不能同时记录多个方法执行到哪个位置。比如在方法A中调用了方法B,当方法B执行结束后需要回到被调用的位置,所以在A调用B的时候,可以把当前程序计数器的值传给B方法的对应的栈帧作为方法的返回地址。而方法异常退出时,返回地址要通过异常处理器表来确定返回返回地址。

  • 核心概念
    • 一个java进程对应一个JVM实例,一个JVM实例只存在一个堆
    • java堆在JVM启动时即被创建,其空间大小也被确定,是JVM管理的最大一块内存空间(堆大小可以调节)
    • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间,但在逻辑上要是连续的
    • 堆也是内存管理的核心区域
      在这里插入图片描述

堆空间大小的设置

  • “-Xms” 用于设置堆空间(新生代+老年代)的起始内存,默认情况下为 物理电脑内存/64
  • “-Xmx” 用于设置堆区的最大内存,默认情况下堆区的最大内存大小为物理电脑内存/4
  • 通常将两个参数设置为相同的值,目的是为了能够在gc完后不需要重新分割计算堆区的大小,从而提高性能

内存划分

在这里插入图片描述

  • 默认情况下 -XX:NewRadio=2 代表新生代占1,老年代占2,即新生代占堆的1/3,可以通过修改该参数修改新生代和老年代的内存占比。
  • 新生代中,默认情况下内存空间分配为 Eden:survivor0:survivor =8:1:1,但是实际中,还存在内存自适应分配策略,所以不一定为8:1:1.
    • 通过 -XX:SurvivorRadio 参数可以设置新生代内存的分配比率
    • 通过 -XX:-UseAdaptiveSizePolicy 可以关闭自适应的内存分配策略
  • from区和to区是相对的,一次gc后,哪个区是空的, 哪个是to区
  • from区满时.不会触发minor gc,只有eden区满了才会触发minor gc,from区也会进行gc(即from区只能被动gc)
    在这里插入图片描述

对内存分代的原因

  • 唯一的目的是优化gc的性能
    • 绝大多数的对象都是朝生夕死的(70%-99%),如果把新创建的对象都放在同一个区域,gc的时候优先对这块区域进行清理,就会腾出很大的空间,而且gc的速度更快

方法区

  • 方法区是一块独立于java堆的内存空间
  • 方法区和堆一样是线程共享的区域
  • 方法区的大小可以设置成固定大小或者可扩展的
  • 方法区的大小决定了系统可以保存多个类,如果定义的类太多,方法区也会发生OOM异常。
  • 方法区在JVM虚拟机启动时被创建,关闭JVM时方法区被释放
  • 方法区在jdk1.7及其之前被称为永久代(本质上,方法区和永久代并不等价,仅对于hotspot虚拟机而言,hotspot在jdk1.7以前用永久代来实现方法区),在jdk1.8开始用元空间来实现方法区
  • 元空间不在虚拟机设置的内存中,而是使用本地内存(更不容易出现OOM)
  • 永久代和元空间的区别是什么
    • 元空间和永久代类似,都是对jvm规范中方法区的落地实现
    • 元空间、永久代不仅名字变了,内部结构也不一样
    • 最大区别是元空间使用了本地内存
  • 为什么永久代要被元空间替换?
  • 官方文档解释:这是JRockit虚拟机和HotSpot虚拟机进行融合的一部分,因为JRockit虚拟机没有永久代,而是采用了元空间的方式实现方法区,所以HotSpot虚拟机也改用元空间的方法来实现方法区,就不用去配置永久代了。
  • 为永久代设置空间大小是很难确定的,某些场景下(比如一个web工程),如果动态加载的类过多,容易产生OOM,而方法区使用了本地内存,默认情况下,元空间的大小仅受本地内存限制
  •  
  • 《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存

参数设置(jdk1.8及之后)

  • -XX:MetaspaceSize =size
    • 设置方法区的初始大小。默认大小取决于平台(window环境下默认21M)。
    • 有一个高水位线的概念,水位线的初始值等于方法区的初始大小。当方法区使用的内存首次大于水位线,会触发full gc,然后根据释放的元空间的大小重新设置水位线的值:如果释放的元空间过多,适当降低水位线的值,如果释放的空间不足,那么在不超过MaxMetaspaceSize的情况下适当提高该值
    • 所以为了避免频繁的full gc,可以适当提高MetaspaceSize的的值
  • -XX:MaxMetaspaceSize =size
    设置可以分配给方法区的最大本地内存。默认情况下,大小不受限制(即 =-1)。
    如将方法区的最大空间为256 MB:
    -XX:MaxMetaspaceSize = 256m

运行时常量池

  • 运行时常量池(Runtime Constant Pool)是方法区的一部分
  • 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放在方法区的运行时常量池中
  • 运行时常量池中包含多种不同的常量,包括编译期已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池表中的符号地址,这里换为真实地址(动态链接阶段完成)

方法区的演进

  1. 首先明确只有HotSpot才有永久代
  2. HotSpot中方法区的变化
    在这里插入图片描述
    • jdk1.6及其之前,虽然静态变量放在永久代中,如果它引用的是一个对象,对象始终在堆中分配,变量本身保存在永久代中,对应的值是对象在堆中起始地址。
    • 为什么从jdk1.7开始要将字符串常量池从方法区移到堆中?
      • 因为永久代的回收效率很低,只有full gc的时候才会触发垃圾回收,而full gc只有在老年代空间不足、永久代空间不足的时候才会触发。这就导致了字符串常量池回收效率不高,而我们开发中,会有大量的字符串被创建,放到堆里,能及时回收内存。
原文地址:https://www.cnblogs.com/ma13461749958/p/14249771.html