深入浅出JVM系列(一):JVM内存结构

java常见面试考点

往期文章推荐:
  java常见面试考点(十六):类加载器的常见考点
  深入浅出JVM系列(二):垃圾收集算法
  深入浅出JVM系列(三):JVM生命周期


【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权);

本博客的内容来自于:深入浅出JVM系列(一):JVM内存结构

学习、合作与交流联系q384660495;

本博客的内容仅供学习与参考,并非营利;


一、前言

  这篇文章是JVM这个专题的第一篇文章,对于这一个专题,我打算尽可能的深入浅出的记录下JVM里必须要掌握的所有知识点,希望和大家一起进步。

二、JVM的整体结构

首先,先看一下JVM的整体结构,如下图所示:
JVM整体结构
JVM的内存结构可以分为上中下3层。

  上层主要是类加载子系统,负责将字节码文件加载到内存中生成class对象。类加载又分为具体的三个环节,加载(loading)、链接(linking)、初始化(Initialization)。详情可以参考我的这一篇文章java常见面试考点(十六):类加载器的常见考点

  中层是运行时数据区,包括方法区、堆区、栈区(通常所说的java虚拟机栈)、程序计数器、本地方法栈。

  下层是执行引擎,Java本地接口 (JNI),本地方法库。JVM将class文件加载到内存中以后,执行引擎负责执行代码。执行引擎会将高级语言翻译成机器指令。Java本地接口 (JNI): JNI 会与本地方法库进行交互并提供执行引擎所需的本地库。本地方法库:它是一个执行引擎所需的本地库的集合。执行引擎包括解释器、JIT编译器、垃圾回收器3部分。

解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。

JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用JIT编译器将全部字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提高了系统的性能。

  • 中间代码生成器 – 生成中间代码
  • 代码优化器 – 负责优化上面生成的中间代码
  • 目标代码生成器 – 负责生成机器代码或本机代码
  • 探测器(Profiler) – 一个特殊的组件,负责寻找被多次调用的方法

垃圾回收器:收集并删除未引用的对象。可以通过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。详情可参考我的这一篇文章深入浅出JVM系列(二):垃圾收集算法

三、运行时数据区

  JVM 的运行时数据区主要包括:堆、栈、方法区、程序计数器等。而 JVM 的优化问题主要在线程共享的数据区中:堆、方法区。
运行时数据区

1、程序计数器

  程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

  为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。

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

此内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。
程序计数器

2、虚拟机栈和本地方法栈

  JVM 中的栈包括 Java 虚拟机栈和本地方法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 方法服务,本地方法栈则为 JVM 使用到的 Native 方法服务。两者作用是极其相似的,本文主要介绍 Java 虚拟机栈,以下简称栈。

JDK 中有很多方法是使用 Native 修饰的。Native 方法不是以 Java 语言实现的,而是以本地语言实现的(比如 C 或 C++)。比如通知垃圾收集器进行垃圾回收的代码 System.gc(),就是使用 native 修饰的。

  栈是后进先出的结构。他是线程私有的,他的生命周期与线程相同。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间。

  栈帧是栈的元素。每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。
栈结构

局部变量表

  栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

  局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。

  JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。
slot复用

  为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。Slot 复用虽然节省了栈帧空间,但是会伴随一些额外的副作用。比如,Slot 的复用会直接影响到系统的垃圾收集行为。详情参考一文搞懂JVM内存结构

操作数栈

  操作数栈是一个后进先出栈。操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。详情参考一文搞懂JVM内存结构

  使用 -Xss 设置栈大小,通常几百K就够用了。由于栈是线程私有的,线程数越多,占用栈空间越大。

动态链接

  Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。

方法返回

  无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行

3、方法区

  方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。
  注:JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。对于元空间和方法区的改动,可以参考我的这篇文章:java常见面试考点(十七):为什么要去除永久代,换成元空间元空间两个参数:

MetaSpaceSize:初始化元空间大小,控制发生GC阈值 MaxMetaspaceSize :
限制元空间大小上限,防止异常占用过多物理内存

4、堆

  堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)。

  Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

  年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。

  老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。

5、非堆

按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。 堆是在 Java 虚拟机启动时创建的。在JVM中,堆之外的内存称为非堆内存(Non-heap memory)”。

可以看出JVM主要管理两种类型的内存:堆和非堆。
简单来说:

堆就是Java代码可及的内存,是留给开发人员使用的;
非堆就是JVM留给自己用的,所以

  • 方法区、
  • JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、
  • 每个类结构(如运行时常数池、字段和方法数据)
  • 方法和构造方法 的代码

都在非堆内存中。

堆外内存的优点和缺点
堆外内存,其实就是不受JVM控制的内存。相比于堆内内存有几个优势:

  1. 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作(可能使用多线程或者时间片的方式,根本感觉不到)
  2. 加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。

而福之祸所依,自然也有不好的一面:

  1. 堆外内存难以控制,如果内存泄漏,那么很难排查
  2. 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合

总结

JVM结构

参考资料

JVM的整体结构,JAVA代码的执行流程,JVM的生命周期
一文搞懂JVM内存结构
Java JVM——9.方法区

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏天的爱人是绿色

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值