这一次,终于系统的学习了 JVM 内存结构

最近在看《 JAVA并发编程实践 》这本书,里面涉及到了 Java 内存模型,经过 Java 内存模型瓜熟蒂落的来到的 JVM 内存结构,关于 JVM 内存结构的认知还停留在上大学那会的课堂上,一直没有系统的学习这一块的知识,因此这一次我把《 深刻理解Java虚拟机JVM高级特性与最佳实践 》、《 Java虚拟机规范 Java SE 8版 》这两本书中关于 JVM 内存结构的部分都看了一遍,算是对 JVM 内存结构有了新的认识。JVM 内存结构是指:Java 虚拟机定义了若干种程序运行期间会使用的运行时数据区,其中有一些会随着虚拟机启动而建立,随着虚拟机退出而销毁,另外一些则与线程一一对应,随着线程的开始而建立,随着线程的结束而销毁。具体的运行时数据区以下图所示:java

JVM 内存结构

在 Java 虚拟机规范中,定义了五种运行时数据区,分别是 Java 堆、方法区、虚拟机栈、本地方法区、程序计数器,其中 Java 堆和方法区是线程共享的。接下来就具体看看这 五种运行时数据区。面试

Java 堆(Heap)

Java 堆是全部线程共享的一块内存区域,它在虚拟机启动时 就会被建立,而且单个 JVM 进程有且仅有一个 Java 堆。Java 堆是用来存放对象实例及数组,也就是说咱们代码中经过 new 关键字 new 出来的对象都存放在这里。因此这里也就成为了垃圾回收器的主要活动营地了,因而它就有了一个别名叫作 GC 堆,根据垃圾回收器的规则,咱们能够对 Java 堆进行进一步的划分,具体 Java 堆内存结构以下图所示:编程

Java 堆内存结构

咱们能够将 Java 堆划分为新生代和老年代两个大模块,在新生代中,咱们又能够进一步分为 Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1),Survivor 空间有一个为空,用于发生 GC 时存放存活对象,老年代存放的是通过屡次 Minor GC 仍然存活的对象或者是一些大对象,FGC 就是发生在老年代。数组

上面就是 Java 堆的具体结构,咱们也知道 Java 堆中的各空间大小,咱们是能够动态控制的,这个在图中我也进行了简单的标注,下面咱们一块儿来详细的了解一下这三个参数:微信

  • -Xms:JVM启动时申请的初始Heap值,默认为操做系统物理内存的1/64,例如-Xms20m
  • -Xmx:JVM可申请的最大Heap值,默认值为物理内存的1/4,例如-Xmx20m,咱们最好将 -Xms 和 -Xmx 设为相同值,避免每次垃圾回收完成后JVM从新分配内存;
  • -Xmn:设置新生代的内存大小,-Xmn 是将NewSize与MaxNewSize设为一致,咱们也能够分别设置这两个参数

在 Java 堆中会发生 OOM 异常,当咱们的 Java 堆内有足够的空间去完成实例分配时,而且堆也没法扩展,将会抛出咱们常见的OutOfMemoryError 异常,以下图所示:数据结构

OutOfMemoryError 异常

关于 OOM 异常,我仍是想多说一句,网上有一道很是火的面试题:JVM 堆内存溢出后,其余线程是否可继续工做?,我我的以为很多回答是错误的,有兴趣的能够研究一下。多线程

方法区(Method Area)

方法区(Method Area)与 Java 堆同样,是各个线程共享的内存区域,是 Java 虚拟机中惟二的内存共享区域。在 Java 虚拟机规范中是这样定义方法区的:它存储了每一个类的结构信息,例如运行时常量池、字段、方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。并发

方法区在虚拟机启动的时候被建立,虽然方法区是堆的逻辑组成部分,可是简单的虚拟机实现能够选择在这个区域不实现垃圾收集与压缩,方法区在实际内存空间中能够不是连续的,对于方法区的容量,你能够是固定的,也能够随着程序的执行动态扩展,而且在不须要过多空间时自动收缩。jvm

上面都是 Java 虚拟机中的规范,来看看具体的实现,拿咱们经常使用的 HotSpot 虚拟机来讲,在 JDK1.8 以前,方法区也被称做为永久代,这个方法区会发生咱们常见的 java.lang.OutOfMemoryError: PermGen space 异常,咱们也能够经过启动参数来控制方法区的大小:函数

  • -XX:PermSize 设置最小空间
  • -XX:MaxPermSize 设置最大空间

在 JDK1.8 以后,HotSpot 虚拟机对方法区进行了不小的改动,完全移除了永久代,将原来存放在永久代的数据迁移至 Java 堆 或者 Metaspace,方法区被移至到了 Metaspace,字符串常量移至 Java Heap,换句话说就是 JDK1.8 开始,Metaspace 也就是咱们所谓的方法区,为何要作这个改变呢?也许是基于如下两点缘由:

  • 因为 PermGen 内存常常会溢出,引起恼人的 java.lang.OutOfMemoryError: PermGen,所以 JVM 的开发者但愿这一块内存能够更灵活地被管理,不要再常常出现这样的 OOM
  • 移除 PermGen 能够促进 HotSpot JVM 与 JRockit VM 的融合,由于 JRockit 没有永久代。

咱们也能够经过设置参数来控制 Metaspace 的空间大小,主要有如下几个命令:

  • -XX:MetaspaceSize :分配给类元数据空间(以字节计)的初始大小。MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收事后,引发下一次垃圾回收的类元数据空间的大小可能会变大。
  • -XX:MaxMetaspaceSize: 分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
  • -XX:MinMetaspaceFreeRatio:表示一次GC之后,为了不增长元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会致使垃圾回收。
  • -XX:MaxMetaspaceFreeRatio:表示一次GC之后,为了不增长元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会致使垃圾回收。

Java 虚拟机栈(JVM Stacks)

每一条 Java 虚拟机线程都有本身私有的 Java 虚拟机栈,这个 Java 虚拟机栈跟线程同时建立,因此它跟线程有相同的生命周期。Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法出口等信息,每个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中的入栈到出栈的过程

局部变量表存放了编译期可知的各类基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象自己,根据不一样的虚拟机实现,它多是一个指向对象起始地址的引用指针,也可能指向一个表明对象的句柄或者其余与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间(Slot),其他的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法须要在帧中分配多大的局部变量空间是彻底肯定的,在方法运行期间不会改变局部变量表的大小。

Java 虚拟机栈既容许被实现成固定的大小,也容许根据计算动态来扩展和收缩,若是采用固定大小的话,每个线程的 Java 虚拟机栈容量能够在线程建立的时候独立选定。在 Java 虚拟机栈中会发生两种异常,这个在虚拟机规范中有指出:

  • 若是线程请求分配的栈容量超过 Java 虚拟机栈容许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异常;
  • 若是 Java 虚拟机栈能够动态扩展,而且在尝试扩展的时候没法申请到足够的内存或者在建立新的线程时没有足够的内存去建立对应的 Java 虚拟机栈,那么虚拟机将会抛出 OutOfMemoryError 异常。

程序计数器(Program Counter Register)

程序计数器也是线程私有的,它只须要一块较小的内存空间,你能够把它看做当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里(仅是概念模型,各类虚拟机可能会经过一些更高效的方式去实现),字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。

咱们知道在多线程的状况下,并非一条线程一直执行完,而是多个线程轮流切换执行,因此为了线程切换后可以恢复到正确的执行位置,咱们就须要程序计数器来告诉线程接下来该执行哪条指令。若是线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,若是正在执行的是 Natvie 方法,这个计数器值则为空(Undefined)。

须要特别注意的是,程序计数器是惟一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 状况的区域

本地方法栈(Native Method Stacks)

本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的做用是很是类似的,其区别不过是 Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并无强制规定,所以具体的虚拟机能够自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

与 Java 虚拟机栈同样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

参考

  • 《 深刻理解Java虚拟机JVM高级特性与最佳实践 》
  • 《 Java虚拟机规范 Java SE 8版 》

最后

目前互联网上不少大佬都有 JVM 内存结构相关文章,若有雷同,请多多包涵了。原创不易,码字不易,还但愿你们多多支持。若文中有所错误之处,还望提出,谢谢,欢迎扫码关注微信公众号:「平头哥的技术博文」,和平头哥一块儿学习,一块儿进步。
平头哥的技术博文

相关文章
相关标签/搜索