图解JVM(一) - 内存模型

简介

商业Java虚拟机有很多,例如:HotSpot、JRockit、IBM J9、Alibaba VM、Graal VM等等数不胜数。而我们平时自己使用的、以及现在官方默认的则是HotSpot,所以平时没有特意指明什么JVM,那就默认是HotSpot。全文也将以HotSpot为核心。

可以通过 java -version 查看

运行时数据区

通常意义上的JVM内存模型指的就是它的运行时数据区或者叫Java内存区域。这里我们不深入讲解它的运行原理,而是讲它的内部结构以及各个区域作用大致描述一下,后面再逐个深入,尽可能直白的语言描述。
通常我们将它分成五部分:程序计数器、虚拟机栈、本地方法栈、堆、方法区。
先上图:
在这里插入图片描述
黄色部分代表是线程共享的。绿色则是线程私有的,每个线程都有一份。

所以它也可以是这样:
在这里插入图片描述

程序计数器

程序计数器(program counter register)也叫PC寄存器,最先讲它是因为它是整个内存区域里面最简单的,它也算是内存区域最小的一块区域。

  1. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,我们代码中if-else、try-catch等分支、循环、跳转、异常处理、线程恢复都需要依赖它来完成。
  2. 我们都知道多线程环境中,一个处理器(多核处理器则指它的一个内核)在任何一个确定的时刻只会处理一条线程的指令,整体不断的进行执行上下文切换。为了线程切换回来后能恢复到正常的执行位置,每个线程都要一个私有的程序计数器。
  3. 它是内存区域中唯一一块《Java虚拟机规范》中没有规定任何OutOfMemoryError(之后都简称OOM)情况的区域,因为它就存储一个字节码指令的地址,不会出现内存溢出情况。

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

不理解的话继续看图:

  1. 线程切换
    在这里插入图片描述
  2. 跳转:看几句简单的字节码在这里插入图片描述
    当进行流程控制语句时,程序计数器的就需要正确指向下个需要执行的字节码指令。

iinc表示用常数增量局部变量,暂时简单理解为自增、自减。

Java虚拟机栈

跟PC寄存器一样Java虚拟机栈也是线程私有的,它们的声明周期跟线程相同。它负责Java方法的执行,每个方法被执行时,JVM都会同步创建一个栈帧用于存储局部变量表、操作数栈,动态链接、方法返回地址等信息(后面再详细讲)。一个方法被执行到执行结束,对应着栈帧进栈到出栈的过程。
所以它应该时这样的:在这里插入图片描述
当我们debug时。调试界面会出现这样的界面:在这里插入图片描述
其中的frame指的就是栈帧(Stack Frame),main方法中调用ifTest()方法。

局部变量表 - Local Variable Table

  1. 局部变量表中存储着局部变量,方法参数等信息。当方法为成员方法时,局部变量表中的第一位则为当前调用对象(this)。
    如下:在这里插入图片描述
    ifTest方法没有参数,只有定以在方法内的int类型变量i;
  2. 局部变量表中数据存储空间用局部变量槽(Slot)表示,一个槽4个字节,所以long、double类型数据数据就得占用两个槽存储。就像这样:在这里插入图片描述

溢出情况以及参数设置

《Java虚拟机规范》规定了两类这块内存区域的异常情况,当线程请求的深度大于虚拟机允许的深度时(就是调用的方法太多了),将抛出StackOverflowError异常,如果Java虚拟机栈容量可以动态扩展,当栈无法申请到足够的内存时会抛出OOM异常。

  1. HotSpot中时不允许栈的动态扩展的。所以不会出现动态扩展失败而出现OOM。如果将栈的大小设置(栈空间默认为1024kb,可以通过 -Xss 或者 -XX:进行修改栈的固定内存大小)
    成大于JVM最大内存时,也可以出现OOM。
  2. 栈设置的大小根据具体业务以及物理机情况而定。

本地方法栈

本地方法栈(Native Method Stack)的作用与虚拟机栈的作用十分类似,它是服务于本地方法的。《Java虚拟机规范》对它使用的语言、使用方式与数据结构没有任何强制规定。所以具体的虚拟机可以自由地实现它,甚至有的JVM将它和虚拟机栈合二为一(HotSpot就是哈哈哈,看到这的同学应该再回头想想前面虚拟机栈的知识,以及最开始提到的执行本地方法时,为什么程序计数器为undefined),它的异常情况同于虚拟机栈。

本地方法

本地方法(Native Method)简单说就是通过native关键字调用一个非Java语言编写的方法,多为操作系统的方法。它的最用就是与Java环境外交互、或者和操作系统交互,毕竟JVM只是运行再操作系统之上,具体对内存或者磁盘的操作都要依赖操作系统。本地方法在稍微底层点的类中随处可见,例如Runtime类中:在这里插入图片描述

Java堆

  1. 堆(Heap)时JVM所管理的内存中最大的一块,它是线程共享的,它只负责存储实例对象,‘几乎’所有的对象都存储在堆中。Java堆是垃圾回收器管理的重点区域,根据垃圾收集器的不同(G1出现之前都为经典分带),堆内存划分也不同,这点放在后面的文章再详细讲。
  2. 《Java虚拟机规范》中允许堆的内存空间是不连续的。但在逻辑上应该是连续的。但对于大对象(比如大数组)多数虚拟机出于简单高效考虑,会要求它连续的。
  3. 堆空间并不绝对是线程共享的,为了提高分配效率,堆中还划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。小于堆空间的1/64的对象会优先分配在TLAB中,在TLAB中分配不会出现线程安全问题,可以有效提高分配效率。所以它可以是这样:在这里插入图片描述
  1. 随着即时编译技术的进步,尤其时逃逸分析技术的不断进步,出现栈上分配、标量替换等技术,导致对象在栈上分配成为可能(对象在堆中分配需要许多繁杂的过程,用完后还需要回收,所以将一些作用域只在单个方法中的对象,都分配到堆中是一种不划算的方法),所以对象并不一定都存储在堆中
  2. 堆的大小可以通过-XX:mx=…(堆的最大内存,默认为物理机内存的1/4)以及-XX:ms=…(堆的初始内存,默认为物理机内存的1/64)设置.通常情况我们会讲它们的值设置成一样,避免运行过程中动态请求内存、释放内存带来的运行负担,具体大小根据具体业务以及环境而定。

方法区

方法区(Method Area)也是线程共享的区域,它主要存储被虚拟机加载的类型信息、常量、静态、静态变量的引用、即时编译器编译后的代码缓存等数据(后面再详细描叙这一块)。在《Java虚拟机规范》中方法区被描述为堆的逻辑部分,而它有个别称叫‘非堆’(Non-Heap),目的就是为了将它区分开。在HotSpot的落地实现中,从JDK8以前称为‘永久代’,使用的是JVM管理的内存,JDK8开始引入本地内存(Native Memory,也就是物理内存,了解过NIO的同学应该都不陌生),改名为元空间(Meta Space)



关于Java虚拟机的内存模型就粗浅的讲到着了,下章讲讲一个新对象内存如何分配的。才写了草草几百字,发现JVM的知识是真难写,不断的会有后面的知识会穿插进来(我已经极力避免了),如前言所说JVM的系统性非常强,刚开始接触算是最难的一段时间了,需要不断被新概念冲击着,这里再次感概周志明老师的强大。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值