浅谈 JVM 内存模型

Posted by klaus_turbo on 2022-08-31
JVM

前言

作为一个Java工作者,我们每天都在码代码,运行代码,和Java不停的打交道。你有没有好奇过你写的这段代码是怎么运行的?他在机器上又是如何排布的?对于每个开发来说,了解其原理有助于我们更好的去理解我们写下的每一行代码,让我们更好的去优化我们的代码,理解问题,解决问题,甚至于当我们说出某一个术语的时候不至于因为不了解而被同行鄙视(胡说八道)。

相信科班出身的你不会忘记这四个字母:WORA,即:Write Once ,Run Anywhere,一次编写,到处运行。这是当时 Sun 推荐 Java 时候的噱头,用于展示 Java 的跨平台的语言特性。而这所谓的 WORA 所依托的正是一个叫做JVM的虚拟机,通过Java的虚拟机,Java 语言成功的实现了平台不相关性。我比较喜欢解耦,所以也可以说实现了开发语言与硬件平台的解耦,因为只要安装了 JVM 的虚拟机,就可以在毫无修改的情况下运行已编译好的 Java 代码。

说了这么多,可以意识到 JVM 对于 Java 的重要性有多么大。其中内容非常多,诸如类加载机制,JVM 运行时数据区域,GC 算法,JVM 内存模型等等,在这里我们就简单的看看 JVM 内存模型是什么样子的。

JVM内存模型

总的来说,JVM 的内存模型总共划分了两大块区域:非堆区和堆区。非堆区,JVM 用永久代(PermanetGeneration)来存放方法区,(
在JDK的HotSpot虚拟机中,可以认为方法区就是永久代,但是在其他类型的虚拟机中,没有永久代的概念)。
一块是堆区。堆区分为两大块,一个是 Old 区(老年代),一个是 Young 区(新生代)。
Young区分为两大块,一个是 Survivor(S0+S1),一块是 Eden 区。 Eden:S0:S1=8:1:1 S0 和S1一样大,也可以叫 From 和 To。整个JVM内存模型很符合Ungar的分代垃圾回收,具体的不展开,
总体样子如下:

jvm1.png

我们知道,一般对象和数组的创建会在堆中分配内存空间,关键问题是,堆中有这么多的区域,哪一个对象的创建到底是在哪个区域呢?

一般情况下,新创建的对象都会被分配到 Eden 区,一些特殊的大的对象会直接分配到 Old 区。
比如有对象A,B,C等创建在 Eden 区,但是 Eden 区的内存空间肯定有限,比如有100M,假如已经使用了100M或者达到一个设定的临界值,
这时候就需要对 Eden 内存空间进行清理,即垃圾收集,这样的 GC 我们称之为 Minor GC,Minor GC 指得是 Young 区的 GC,
有的也会称之为是 YGC。经过 GC 之后,有些对象就会被清理掉,有些对象可能还存活着,对于存活着的对象需要将其复制到 Survivor 区,
然后再清空 Eden 区中的这些对象。

Survivor 区详解

从上面的图我们可以看到,Survivor 区分为两块 S0 和 S1,也可以叫做 From 和 To。
在同一个时间点上,S0 和 S1 只能有一个区有数据,另外一个是空的。接着上面的 GC 来说,比如一开始只有 Eden 区和 From 中有对象,
To 中是空的。此时进行一次 GC 操作,From 区中对象的年龄就会+1,我们知道 Eden 区中所有存活的对象会被复制到 To 区,
From 区中还能存活的对象会有两个去处。若对象年龄达到之前设置好的年龄阈值,此时对象会被移动到 Old 区,
如果 Eden 区和 From 区 没有达到阈值的对象会被复制到To区。 此时 Eden 区和 From 区已经被清空(被 GC 的对象肯定没了,没有被 GC 的对象都有了各自的去处)。
这时候 From 和 To 交换角色,之前的 From 变成了 To,之前的 To 变成了 From。也就是说无论如何都要保证名为 To 的 Survivor 区域是空的。
Minor GC 会一直重复这样的过程,直到 To 区被填满,然后会将所有对象复制到老年代中。通常也叫 YGC。

Old 区详解

从上面的分析可以看出,一般 Old 区都是年龄比较大的对象,或者相对超过了某个阈值的对象。
在 Old 区也会有 GC 的操作,Old 区的 GC 我们称作为 Major GC ,也叫 FGC。

Java 对象的生命周期

一个普通的 Java 对象一开始的时候是在 Eden 区的,它可能会在 Eden 区域停留很长的时间。当 Eden 区在某一个时间点,对象实在是太多了,新生对象就会被迫前往 Survivor 区的 From 区。
当一个对象到达了 Survivor 区之后,它会在 From 和 To 区之间漂泊。直到该对象满足一定的条件,该对象就会被分配到 Old 区去(在分代垃圾回收中,对象在 GC 中存活了一定的次数之后上升到老年代中去,这个过程也叫做”晋升”)。
下图展现了一个对象的分配过程。

对象生命周期.png

为什么需要 Survivor 区?只有 Eden 不行吗?

先回答这么做的目的:减少 Full GC 。如果没有 Survivor,Eden 区每进行一次 Minor GC ,并且没有年龄限制的话, 存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发 Major GC (因为Major GC 一般伴随着 Minor GC,也可以看做触发了 Full GC )。
老年代的内存空间远大于新生代,进行一次 Full GC 消耗的时间比 Minor GC 长得多。
执行时间长有什么坏处?频发的 Full GC 消耗的时间很长,会影响大型程序的执行和响应速度(也许你听过 Stop the world,所有版本的虚拟机,升级的目的只有一个,那就是减少垃圾回收给程序带来的影响)。可能你会说,那就对老年代的空间进行增加或者较少咯。
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低 Full GC 频率,但是随着老年代空间加大,一旦发生 FullGC ,执行所需要的时间更长。
假如减少老年代空间,虽然 Full GC 所需时间减少,但是老年代很快被存活对象填满,Full GC 频率增加。所以 Survivor 的存在意义,
就是减少被送到老年代的对象,进而减少 Full GC 的发生,Survivor 的预筛选保证,只有经历16次(默认第16次就去老年代) Minor GC 还能在新生代中存活的对象,
才会被送到老年代。

为什么需要两个 Survivor 区?

最大的好处就是解决了碎片化。也就是说为什么一个 Survivor 区不行?第一部分中,我们知道了必须设置 Survivor 区。
假设现在只有一个 Survivor 区,我们来模拟一下流程:刚刚新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,
Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,
此时进行 Minor GC,Eden 和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,
也就导致了内存碎片化。永远有一个 Survivor space 是空的,另一个非空的 Survivor space 无碎片。所以通过这哥步骤就很好的解决了碎片化严重的问题。

新生代中 Eden:S1:S2 为什么是 8:1:1?

GC 是统计学测算出当内存使用超过98%以上时,内存就应该被 minor gc 时回收一次。
但是实际应用中,我们不能较真的只给 他们留下2%,换句话说当内存使用达到98%时才GC 就有点晚了,应该是多一些预留10%内存空间,
这预留下来的空间我们称为S区(有两个s区 s1 和 s0),S区是用来存储新生代GC后存活下来的对象,大多数的对象都是朝生夕死,
生命周期短(大多是web应用,比如一个订单下好了,就好了)。而我们知道新生代GC算法使用的是复制回收算法。
所以我们实际GC发生是在,新生代内存使用达到90%时开始进行,复制存活的对象到S1区,要知道GC结束后在S1区活下来的对象,
需要放回给S0区,也就是对调(对调是指,两个S区位置互换,意味着再一次minor gc 时的区域 是eden 加上一次存活的对象放入的S区),既然能对调,
其实就是两个区域一般大。这也是为什么会再有个10%的S0区域出来。这样比例就是8:1:1了 ,这里的eden区(80%) 和其中的一个 S区(10%)
合起来共占据90%,GC就是清理的他们,始终保持着其中一个 S 区是空留的,保证GC的时候复制存活的对象有个存储的地方。
所以,其实不一定非得是 8:1:1,就像 hashmap 的 0.75。

最后

这些大概就是我对 JVM 内存模型的一些理解了,期间的垃圾回收非常的复杂,有兴趣的小伙伴可以去看看中村成洋的《垃圾回收的算法与实现》,这本书讲的是很不错的,一直从最开始的清除算法到后面的RC Immix算法
从简单到深入,我感觉写的还是不错的。一步步的从各方面解释算法,以及优化。还有就是比较基础的一些概念可以看周志明的《深入理解JAVA虚拟机》。

怎么说呢,个人认为对于虚拟机的理解是很重要的,只要是写java,必然就绕不开虚拟机,不理解就不能很好的去调优,就像医生一样,你不可能用一味药治好所有的病,作为java开发,你也不可能用那几句调优的命令去把你的程序优化到比较好的一个程度。

只能说,Java 很难。与大家共勉,有不对的希望大家指出,愿意与伙伴们一起更好的学习了解JVM。

本人关于图片作品版权的声明:

  1. 本人在此刊载的原创作品,其版权归属本人所有。

  2. 任何传统媒体、商业公司或其他网站未经本人的授权许可,不得擅自从本人转载、转贴或者以任何其他方式复制、使用上述作品。

  3. 传统媒体、商业公司或其他网站对上述作品的任何使用,均须事先与本人联系。

  4. 对于侵犯本人的合法权益的公司、媒体、网站和人员,本人聘请的律师受本人的委托,将采取必要的措施,通过包括法律诉讼在内的途径来维护本人的合法权益。

特此声明,敬请合作。