Jvm内存结构总结

一、概念

1.JVM

Java Virtual Machine,译:Java虚拟机。与其说是“虚拟的运行环境”,不如说是为Java程序运行提供了一套统一的标准,使得Compile once, Run anywhere成为可能。虚拟机的有很多,像HotSpot,J9,Sun Classic等等,,

计算机领域的任何问题都可以通过增加一个间接的中间件来解决,忘了谁说的了,JVM亦是如此。

2.作用

其实上边已经概括了,任何语言的终点都是二进制,硬件才能够识别,Java也是如此,只不过编写的Java代码不会直接被编译成二进制,会先被编译成.class文件,称为字节码文件,这个.class文件就是JVM的“二进制”,最后由JVM完成最后的编译执行,于是,Java应用程序表面是面向JVM编写的,把权力交给虚拟机。

二、JVM内存结构

1.内存模型

内存模型是一个概念性的标准,通过实现该标准,向上层屏蔽了os和硬件的差异,使得在不同的硬件平台和操作系统下获得相同的访问结果。

2.内存结构

如下,JVM在执行Java程序过程中会将内存区域划分为不同的数据区域,这些区域有各自的用途,以及创建和销毁时间,有的区域随着虚拟机进程启动永久驻留,用的这是依赖用户的线程创建和销毁。
其中虚拟机栈,本地方法栈,程序计数器生命周期均发生在用户线程中,方法区和堆空间则为共享区域。

内存结构图

(1)程序计数器

程序计数器是一块较小的内存空间,永远指向下一条字节码指令地址,有点类似于物理计算机中的PC计数器,它是控制程序运行的关键,比如分支,循环,跳转,异常,线程恢复等。Java虚拟机是通过线程轮流切换,分配处理器执行时间的方式实现的,任意时刻,单个处理器只能执行其中线程中的一条指令,所以,每个线程中都需要单独维护各自的程序计数器,线程之间互不影响,以便每次线程切换都能准确执行正确的代码,这类内存区域,称之为“线程私有”,与之对应的这是“线程共享”内存。

如果线程正在执行的是一个Java方法,计数器记录的是下一条字节码的地址,如果正在执行的是本地方法,这个计数器的值应该为空,此内存区域是唯一一个在《Java虚拟机规范》中没有任何OutOfMemoryError的区域。

(2)Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,虚拟栈是描述Java方法的线程内存模型,每个方法执行的时候,Java虚拟机都会同步创建一个栈帧(方法运行时的数据结构),用于存储局部变量表,操作数栈,动态连接,方法出口等信息,每次方法调用到执行完成,都对应这一个栈帧在虚拟机栈入栈出栈的过程。
局部变量表存储了编译器可知的基本数据类型,对象reference,和returnAddress,这些局部变量表中的存储空间以局部变量槽Slot来表示,其中64位的long和double会占用两个槽,其余数据类型都会占用1个槽。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配的局部变量空间是确定的,运行期间不会改变局部变量表的大小,“大小”指的是槽的数量,每个变量槽的时间大小由虚拟机实现。

Java虚拟机规范对此处空间规定了两个异常,分别是SOE和OOM,线程调用深度大于栈深,栈溢出,如果栈可动态扩展,当栈无法申请足够内存时,内存溢出。HotSpot虚拟机不支持栈的动态扩展

(3)本地方法栈

本地方法栈与虚拟机栈类似,虚拟机栈执行Java方法,也就是字节码。而本地方法栈则是为虚拟机用到的Native本地方法服务。

虚拟机本地方法栈使用的语言并没有做规范,不同虚拟机可以自由实现。

(4)Java堆

Java堆内存时虚拟机管理内存中最大的一块,所有线程共享该区域,该内存区域一般是为了存储对象实例使用(目前Java还不能像c++那样,可以直接在栈上分配对象空间),堆内存是gc的主要工作对象,堆内存一般会划分为多个区域,为的是更好的分配内存和释放内存。从分配内存角度看,所有线程共享的Java堆可以划分出多个线程私有的分配缓存区(Thread Local Allocation Buffer -XX:±UseTLAB),以提升对象的分配效率,实际上,无论怎么划分,本质上对象的分配都是在堆中完成。
堆空间在逻辑上被视为连续的,但物理上可以不连续,但是为了寻址效率,物理连续的内存空间效率更高。

Java堆可以是固定大小的,也可以是可扩展的,目前的虚拟机都是可以扩展的,通过-Xmx和-Xms设置,如果堆无法分配空间,且堆无法扩展,则抛出OOM异常。

什么是Thread Local Allocation Buffer ?
堆空间分配必然面临线程安全问题,解决线程安全要么有序,要么资源隔离,而TLAB的作用就是资源隔离

(5)方法区

方法区与堆一样,属于线程共享内存,它一般用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。

虚拟机规范中把方法区描述为堆的一个逻辑部分,但他却有一个别名“非堆”,目的是与Java堆区分开来

JDK8之前,习惯将方法区称之为“永久代”,本质上,两者并不等价。当时HotSpot虚拟机设计把收集器的分代设计扩展至方法区,或者说用永久代来实现方法区,使得gc能够像管理堆内存一样管理这这部分内存,原则上,方法区的设计不受虚拟机规范约束,不同的虚拟机都可以自由实现方法区部分,只是HotSpot虚拟机使用堆内存管理永久代的方式来实现的方法区。
相对于物理内存而言,虚拟机本身能管理的内存就有限,JDK8之前使用堆内存永久代方式实现方法区,导致虚拟机永久代内存超过永久代限制(-XX:MaxPermSize),则会触发OOM问题。所以,JDK经过7过渡,来到了JDK8,方法区的设计彻底摒弃了永久代的设计方法,采用与J9 JRocket一样在本地内存实现元空间来代替,OOM问题只发生在物理内存不够的时候。

方法区规范比较宽松,不需要连续空间,可以固定大小,也可以扩展,甚至可以不实现垃圾回收,这篇内存的垃圾回收比较少见,回收目标主要是针对常量池的回收和类型的卸载,而且条件非常苛刻,但是回收是有必要的,如果该区域内存一直无法回收,可能会导致内存泄漏。

(6)运行时常量池

运行时常量池是方法区的一部分,class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量表,用于存放编译期生成的各种字面量和符号引用,这部分内存将在类的加载后存放在方法区的运行时常量池中。
虚拟机对class文件的格式有严格规定,但是对常量池没有做细节规范,可以自由实现,这部分内存除了保存class文件的描述符号引用外,还会把由符号翻译出来的直接引用也存储在运行时常量池中。运行时常量池对于class文件常量池的另外一个重要特征就是具备动态性,Java语言不要求常量一定要在编译期才能产生,也就是说并非预置入class文件中常量池的内存才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性利用的比较多的是String.intern()方法。

(7)直接内存

直接内存不是JVM运行时数据区的一部分,但是这部分内存也有可能出现OOM问题,JDK1.4引入了NIO,基于通道channel与Buffer的IO方式,可是用Native直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆和Native内存之间来回复制数据。

三、总结

以上信息概括了JVM在内存结构上的基本概念,相对比较容易理解,理解这里,为后续的gc回收,类加载,字节码执行流程等知识提供了基本的思考轮廓。

参考:
《深入理解Java虚拟机:JVM高级特性与最佳实践》(第三版)周志明著

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值