logo头像

小时候,哭是我们解决问题的绝招。长大后,笑是我们面对现实的武器。

JVM内存管理

JVM内存管理

作为三大工业级别语言之一的JAVA如此受企业青睐有加,离不开她背后JVM的默默复出。只是由于JAVA过于成功以至于我们常常忘了JVM平台上还运行着像Clojure/Groovy/Kotlin/Scala/JRuby/Jython这样的语言。我们享受着JVM带来跨平台“一次编译到处执行”的便利和自动内存回收的安逸。

JVM是JAVA的核心基础,也是掌握JAVA语言的重难点,如果没有理解JVM的知识体系,就不要说自己是JAVA高手。最近我打算换工作,所以来重新回顾JVM的相关知识,为面试准备。JVM体系内知识点很多,通过以下关键词来简单概括下:

类结构类加载器加载链接初始化双亲委派热部署隔离方法区计数器内存回收执行引擎调优工具JVMTIJDWPJDI热替换字节码ASMCGLIBDCEVM

本文从JVM的内存结构入手,介绍JVM逻辑内存的分布和管理方式,同时列举常用的JVM调优工具和使用方法。

内存结构

逻辑分区

JVM内存从应用逻辑上可分为如下区域:

程序计数器

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

简单理解就是程序计数器保证了程序的正常执行。

虚拟机栈

方法执行时创建栈帧(存储局部变量,操作栈,动态链接,方法出口)编译时期就能确定占用空间大小,线程请求的栈深度超过jvm运行深度时抛StackOverflowError,当jvm栈无法申请到空闲内存时抛OutOfMemoryError,通过-Xss,-Xsx来配置初始内存

本地方法栈

执行本地方法,如操作系统native接口

存放对象的空间,通过-Xmx,-Xms配置堆大小,当堆无法申请到内存时抛OutOfMemoryError

方法区

存储类数据,常量,常量池,静态变量,通过MaxPermSize参数配置

对象访问

初始化一个对象,其引用存放于栈帧,对象存放于堆内存,对象包含属性信息和该对象父类、接口等类型数据(该类型数据存储在方法区空间,对象拥有类型数据的地址)

内存模型

堆内存

堆内存是运行时的数据区,从中分配所有的Java类实例和数组的内存,可以理解为目标应用依赖的对象。堆在JVM启动时创建,并在应用程序运行时可能会增大或减小。可以使用-Xms`选项指定堆的大小。堆可以是固定大小或可变大小,具体取决于垃圾收集策略。可以使用-Xmx选项设置最大堆大小。默认情况下,最大堆大小设置为64M。

JVM堆内存在物理上分为两部分:新生代老年代。新生代是为分配新对象而保留堆空间。当新生代占用完时,Minor GC垃圾收集器会对新生代区域执行垃圾回收动作。其中在新生代中生活了足够长的所有对象被迁移到老生代,从而释放新生代空间以进行更多的对象分配。此垃圾收集称为Minor GC。新生代分分为三个子区域:伊甸园Eden区和两个幸存区S0``S1

关于新生代空间:

  • 大多数新创建的对象都位于Eden区内存空间
  • 当Eden区填满对象时,执行Minor GC并将所有幸存对象移动到其中一个幸存区空间
  • Minor GC还会检查幸存区对象并将其移动到其他幸存者空间,也即是幸存区总有一个是空的
  • 在多次GC后还存活的对象被移动到老年代内存空间。至于经过多少次GC晋升老年代则由参数配置,通常为15

当老年区填满时,老年区同样会执行垃圾回收。老年区还包含那些经过多Minor GC后还存活的长寿对象。垃圾收集器在老年代内存中执行的垃圾回收称为Major GC,通常需要更长的时间。

Full GC ???

非堆内存

JVM堆以外的内存称为非堆内存。也即是JVM自身预留的内存区域,包含JVM缓存空间,类结构如常量池、字段和方法数据,方法,构造方法。类非堆内存的默认最大大小为64MB。可以用 -XX: MaxPermSize VM选项更改此选项,非堆内存通常包含如下性质的区域空间:

  • 元空间(Metaspace)

在Java 8以上版本已经没有Perm Gen这块区域了,这也意味着不会再由关于“java.lang.OutOfMemoryError:PermGen”内存问题存在了。与驻留在Java堆中的Perm Gen不同,Metaspace不是堆的一部分。类元数据多数情况下都是从本地内存中分配的。默认情况下,元空间会自动增加其大小(直接又底层操作系统提供),而Perm Gen始终具有固定的上限。可以使用两个新标志来设置Metaspace的大小,它们是:“ - XX:MetaspaceSize ”和“ -XX:MaxMetaspaceSize ”。Metaspace背后的含义是类的生命周期及其元数据与类加载器的生命周期相匹配。也就是说,只要类加载器处于活动状态,元数据就会在元数据空间中保持活动状态,并且无法释放。

  • 代码缓存

运行Java程序时,它以分层方式执行代码。在第一层,它使用客户端编译器(C1编译器)来编译代码。分析数据用于服务器编译的第二层(C2编译器),以优化的方式编译该代码。默认情况下,Java 7中未启用分层编译,但在Java 8中启用了分层编译。实时(JIT)编译器将编译的代码存储在称为代码缓存的区域中。它是一个保存已编译代码的特殊堆。如果该区域的大小超过阈值,则该区域将被刷新,并且GC不会重新定位这些对象。Java 8中已经解决了一些性能问题和编译器未重新启用的问题,并且在Java 7中避免这些问题的解决方案之一是将代码缓存的大小增加到一个永远不会达到的程度。

  • 方法区

方法区域是Perm Gen中空间的一部分,用于存储类结构(运行时常量和静态变量)以及方法和构造函数的代码。

  • 内存池

内存池由JVM内存管理器创建,用于创建不可变对象池。内存池可以属于Heap或Perm Gen,具体取决于JVM内存管理器实现。

  • 常量池

常量包含类运行时常量和静态方法,常量池是方法区域的一部分。

  • Java堆栈内存

Java堆栈内存用于执行线程。它们包含特定于方法的特定值,以及对从该方法引用的堆中其他对象的引用。

  • Java堆内存配置项

Java提供了许多内存配置项,我们可以使用它们来设置内存大小及其比例,常用的如下:

VM Switch 描述
-Xms 用于在JVM启动时设置初始堆大小
-Xmx 用于设置最大堆大小
-Xmn 设置新生区的大小,剩下的空间用于老年区
-XX:PermGen 用于设置永久区存初始大小
-XX:MaxPermGen 用于设置Perm Gen的最大尺寸
-XX:SurvivorRatio 提供Eden区域的比例
-XX:NewRatio 用于提供老年代/新生代大小的比例,默认值为2

名词术语

  1. Full GC:新生代和老年代同时回收

References: