likes
comments
collection
share

JVM之运行时数据区

作者站长头像
站长
· 阅读数 62

前言

本篇较为详细讲解了 JVM的运行时数据区, 包括堆, 虚拟机栈, 本地方法栈, 方法区, 字符串常量池, 程序计数器和 JVM内存模型变迁史

在整个 JVM构成主要由三部分组成: 类加载系统, 运行时数据区, 执行引擎

运行时数据区按照线程使用情况和职责分成两大类

  • 线程独享(程序执行区域)
    • 虚拟机栈, 本地方方法栈, 程序计数器
    • 不需要垃圾回收
  • 线程共享(数据存储区域)
    • 堆和字符串常量池
    • 存储类的静态数据和对象数据
    • 需要垃圾回收

java1.8开始, 运行时常量池就不在运行时常量池中了

JVM之运行时数据区

图片来自于guide哥网站

java堆在 JVM启动时创建内存区域去实现对象, 数组与运行时常量的内存分配, 它是虚拟机管理最大的, 也是垃圾回收的主要内存区域

同时, 堆的唯一目的就是存放对象实例, 几乎所有的对象实例以及数组都在这里分配内存

JDK1.7开始默认开启逃逸分析, 如果某些方法中的对象饮用没有被返回或者未被外面使用, 那么该对象可以直接在栈上进行内存分配

对象逃逸: 当在某个方法内创建了一个对象, 且该对象被方法体之外其他变量所饮用, 那么当该方法执行完毕之后不会被GC回收, 这就是对象逃逸 逃逸对象的内存在堆中, 未逃逸对象的内存分配在栈中

Java堆是垃圾收集器管理的主要区域, 因此也被称之为GC堆, 下图是 java 1.7及之前版本的堆内存划分, 其他版本可看后面的 JVM内存模型变迁

JVM之运行时数据区

大部分情况下, 对象会先在 Eden区域进行分配, 在一次新生代垃圾回收之后, 如果对象存活, 那么会进入到 S0或者 S1区域, 且年龄加一, 当对象的年龄达到某个阈值时, 该对象会进入老年代中

阈值参见: issue552“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。

动态年龄计算的代码如下

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {  
  //survivor_capacity是survivor空间的大小  
 size_t desired_survivor_size = (size_t)((((double)   >survivor_capacity)*TargetSurvivorRatio)/100);  
 size_t total = 0;  
 uint age = 1;  
 while (age < table_size) {  
   total += sizes[age];//sizes数组是每个年龄段对象大小  
   if (total > desired_survivor_size) break;  
   age++;  
 }  
 uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;  
  ...  
}  

如果 Java 堆中没有足够的内存来完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

虚拟机栈

栈帧

栈帧是用于支持虚拟机进行方法执行的数据结构

栈帧存储了方法的局部变量表, 操作数栈, 动态连接和返回地址等地址, 每个方法从调用到执行完成的过程, 都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程

栈内存为线程私有的空间, 每个线程都会创建私有的栈没存, 生命周期与线程相同, 除了一些 Native方法调用是通过本地方法栈实现的, 其他所有java方法调用都是栈来实现的

异常

  • 如果线程请求的栈深度大于虚拟机所允许的栈深度, 将抛出StackOverflowError异常
  • 如果 java虚拟机栈的容量允许动态扩展, 当栈扩展时如果无法申请到足够的内存就会抛出OutOfMemoryError异常

本地方法栈

和虚拟机栈所发挥的作用非常相似, 区别如下:

  • 虚拟机栈为虚拟机执行 java方法服务
  • 本地方法栈为虚拟机使用到的 Native方法(例如 C++程序)服务

方法区

方法区是各个线程共享的内存区域, 它作用于已经被虚拟机加载的类型信息, 常量, 静态变量, 即时编译器编译后的代码缓存等数据, 目的是与 java堆进行区分

方法区的具体实现: 永久代, 元空间

永久代和元空间的区别:

  • 版本更新
    • jdk1.8之前的方法区是永久代
    • jdk1.8之后的方法区是元空间
  • 存储位置
    • 永久代使用的内存区域是 JVM进程所使用的区域, 大小受 JVM的大小限制
    • 元空间使用的是物理内存区域, 元空间大小只受物理内存大小的限制
  • 存储内容不同
    • 永久代主要存储方法区存储内容中的数据
    • 元空间只存储类的元信息, 而静态变量和运行时常量池都在堆中

方法区存储数据:

  • class
    • 类型信息
    • 类型的常量池
    • 字段信息
    • 方法信息
    • 类变量
    • 指向类加载器的引用
    • 指向 class实例的引用
    • 方法表
  • 运行时常量池(字符串常量池)
  • JIT编译器编译之后的代码缓存

如果方法区无法满足新的内存分配需求时,将会抛出 OutOfMemoryError 异常

字符串常量池

字符串常量池是 JVM为了提升性能和减少内存消耗针对字符串(String类)专门开辟的一块区域, 主要目的是为了避免字符串的重复创建, 其数据结构采用了 StringTable的数据结构, 类似于哈希表结构

  • 单独使用 "" 创建的字符串都是常量, 编译时就存储在字符串常量池中
  • 使用只包含常量的字符串连接符, 如 "ning"+"xuan"创建的也是常量, 编译时就存储在字符串常量池中
  • 使用 new Stirng("") 创建的对象会存储到 heap中, 是运行时新创建的
  • 使用包含变量的字符串连接 "ning" + xuan, 会存储到 heap中, 是运行时新创建的
  • 运行期刁永刚 String的 inten()方法可以向 字符串常量池中动态添加对象

程序计数器

程序计数器是一块较小的内存空间, 他可以看做是当前线程所执行的字节码的行号指示器

每条线程都有一个独立的程序计数器, 各线程之间的计数器互不影响, 为线程私有

程序计数器的作用:

  • 多线程的情况下, 程序计数器用来记录当前执行的位置, 当线程切换回来时能正确运行
  • 字节码解释器通过改变程序计数器来以此读取指令, 从而实现代码的流程控制: 如: 选择, 循环, 顺序执行, 异常处理等

程序计数器是唯一不会出现OutOfMemoryError异常的内存区域, 其生命周期随着线程的创建而生, 线程的销毁为亡

JVM内存模型变迁

java1.7

JVM之运行时数据区

  • Young区: 年轻区, 主要保存年轻对象, 分为三个部分 Eden区, 两个 Survivor区
  • Tenured: 老年区, 主要保存年长对象, 当对象在 Young区复制转移一定的次数之后, 对象就会被转移到 Tenured区
  • Perm 永久区: 主要保存 class, method, filed对象, 这部分的空间一般不会溢出, 除非一次性加载了很多的类, 不过在涉及到热部署的应用服务器的时候, 有时候会遇到 OOM: PermGen space的错误
  • Virtual区: 最大内存和初始内存的差值

java1.8

JVM之运行时数据区

由两部分组成:

  • 新生代(Eden + 两个 Survivor)
  • 老年代(OldGen)

Metaspace 所占用的内存空间不是在虚拟机内部, 而是在本地内存空间中, 区别于 java1.7

java1.9

JVM之运行时数据区

取消新生代, 老年代的物理划分, 将堆划分为多个区域(Region), 这些区域中包含有了逻辑上的新生代和老年代

本文内容到此结束了

如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。

如有错误❌疑问💬欢迎各位大佬指出。

我是 宁轩 , 我们下次再见

转载自:https://juejin.cn/post/7249288087820910651
评论
请登录