JDK8的JVM内存模型简述

2,843 阅读7分钟

背景

    入职新公司已经将近3个月3个多月,近期从内网技术wiki上看到一个比较陌生的词:“永久代”,对于习惯了在HotSpot JVM上开发、部署的程序员来说,都习惯性地愿意将方法区中的内容称作永久代(永久代是方法区的一种实现方法),而JDK8中方法区不见了,伴生出的是元数据空间(Metaspace),本文将简单总结JDK8对于JVM内存分布的修改与JVM内存模型。

JDK版本

java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

方法区是什么

在了解方法区之前,要先简单了解一下JDK8之前的方法区中存放的是什么数据:

     1)类信息:类名/继承关系/类本身的属性

     2)方法信息 随类的加载而加载,方法名/方法属性/相关异常信息 等

     3)常量池

     4)类变量 即非final类变量

     5)java.lang.Class的实例引用 每个加载的类都创建一个java.lang.Class的实例,这个实例的引用存放在方法区内

     6)方法表 方便快速激活方法

     7)对ClassLoader的引用

    可以看到方法区中绝大部分的数据都是随类的加载而加载的,基本是类的元数据,往往加载完成后不再有改动。

    方法区中存在gc吗?---答案是肯定的,但可以看到除了类变量可能存在变动需要gc外,其他的类数据在该类无实例的情况下也会gc

    方法区的大小是固定的吗? ---否,JDK8之前的方法区空间是占用JVM内存空间的,方法区默认初始大小为20.75M,用户可以手动修改方法区大小:

-XX:PermSize #设置永久代初始分配空间
-XX:MaxPermSize #设定永久代最大可分配空间

元空间为什么取代永久代

    在Java8中,方法区存在于元空间(Metaspace)。元空间不再与堆连续,而且是存在于本地内存中。

    默认情况下元空间是可以无限使用本地内存的,但JVM同样提供了一些参数来限制元空间的使用:

-XX:MetaspaceSize #元空间初始内存,单位bytes
-XX:MaxMetaspaceSize #元空间最大内存,默认无限制
-XX:MinMetaspaceFreeRatio #GC后元空间的最小剩余百分比
-XX:MaxMetaspaceFreeRatio #GC后元空间的最大剩余百分比

    思考一下,为什么使用元空间替换永久代?

原因一:一个Java工程构建时我们往往不知道会有多少个类,因此class meta数据大小未知,初始空间和最大空间无论默认/设计多少都不太合适,最佳的解决方法就是将永久代从堆内存解放出来。

原因二:永久代回收效率偏低,会为GC带来不必要的复杂性,永久代的扫描频率可以比老年代低很多;如果放在堆内存中老年代GC时候也会扫描永久代,造成了资源浪费。

以上是我个人对元空间替代永久代原因的理解,欢迎大家讨论~

    堆Heap是OOM故障的高发地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用;通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗完所有的空间;堆的内存空间既可以固定大小,也可运行时动态地调整,相关参数:

-Xms #初始堆内存
-Xmx #最大堆内存

新生代

    在JVM中,堆被划分成两个不同的区域:新生代(Young Gen)、老年代(Old Gen)。新生代又被划分为三个区域:Eden区、From Survivor区、To Survivor区。一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到老年代。当新生代的占用空间达到临界值时,会对Eden区进行GC,这样的GC我们称为Minor GC。Minor GC过程中大部分对象就会被清理掉,有些对象可能还存活着(朝不保夕),对于存活着的对象需要将其复制到From Survivor区,然后再清空Eden区中的对象。

    下一次Minor GC发生时,From Survivor区中对象的年龄就会+1,Eden区中存活的对象会被复制到To Survivor区,From Survivor区中还能存活的对象会有两个去处:

1.年龄达到年龄阈值,对象会被移动到老生代

2.年龄未达到年龄阈值,对象会被移动到To Survivor区

    也就是说,在这一次Minor GC中,From Survivor区会被清空,To Survivor区中存放没达到年龄阈值的对象,达到年龄阈值的对象进入老年代,如此反复,From/To Survivor区就像两个筛子,不断的将达到要求的对象移入老年代,From Survivor区不可能同时存有对象。

    两个Survivor区的空间大小比例为1:1(与复制算法有关)

    Eden:To:From的空间大小比例为 8:1:1(这是一个很微妙的默认比例,IBM论文里说据他们统计95%的对象存活时间极短,大家可以自己算一算),参数设定:

-XX:SurvivorRatio #默认值8

年龄阈值设置参数:

XX:+MaxTenuringThreshold #默认值15

老年代

    从上面的分析可以看出,一般Old区都是年龄比较大的对象,在老年代也会有GC,老年代的GC我们称作为Major GC/Full GC。

    老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多(STW)。

    频发的Full GC消耗的时间很长,会影响程序的执行和响应速度。

JVM虚拟机栈

    栈是线程私有的内存区域,这里不会出现数据安全问题,描述的是每一个函数执行的内存模型,一个栈帧(Stack Frame)存储局部变量表、操作数栈、动态链接、方法出口等信息。JVM对栈区规定了两种异常情况:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

    本地方法栈与JVM虚拟机栈之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

程序计数器

    程序计数器是一块很小的内存区域,对于java方法(即没用native关键字修饰的方法),存储的是执行过程中当前指令的地址;程序计数器是线程私有的,记录的是每一个线程的执行情况。程序计数器也是JVM中唯一不会报内存溢出(OutOfMemoryError)的区域。

有关JVM内存模型的示意图,我就不画了~懒