关注“Java 后端技术全栈”
回复“000”获取大量电子书
后面的文章中曾经有所提到过堆,只是大抵介绍了一下。本文就来具体聊聊 JVM
中的堆。
在 JVM
中,堆被划分成两个不同的区域:新生代 (Young)、老年代 (Old)。
新生代 (Young) 又被划分为三个区域:Eden 区、From Survivor 区、To Survivor 区。
这样划分的目标是为了使 JVM
可能更好的治理堆内存中的对象,包含内存的调配以及回收。
依据之前对于 Heap 的介绍能够晓得,个别对象和数组的创立会在堆中分配内存空间,要害是堆中有这么多区域,那一个对象的创立到底在哪个区域呢?
对象创立所在区域
个别状况下,新创建的对象都会被调配到 Eden 区(朝生夕死),一些非凡的大的对象会间接调配到 Old 区。
比方有对象 A,B,C 等创立在 Eden 区,然而 Eden 区的内存空间必定无限,比方有 100M
,如果曾经应用了100M
或者达到一个设定的临界值,这时候就须要对 Eden 内存空间进行清理,即垃圾收集 (Garbage Collect),这样的GC
咱们称之为 Minor GC
,Minor GC
指得是 Young 区的GC
。
通过 GC
之后,有些对象就会被清理掉,有些对象可能还存活着,对于存活着的对象须要将其复制到 Survivor 区,而后再清空 Eden 区中的这些对象。
TLAB 的全称是 Thread Local Allocation Buffer,JVM
默认给每个线程开拓一个 buffer 区域,用来减速对象调配。这个 buffer 就放在 Eden 区中。
这个情理和 Java 语言中的 ThreadLocal
相似,防止了对公共区的操作,以及一些锁竞争。
对象的调配优先在 TLAB
上 调配,但 TLAB
通常都很小,所以对象绝对比拟大的时候,会在 Eden 区的共享区域进行调配。
TLAB
是一种优化技术,相似的优化还有对象的栈上调配(这能够引出逃逸剖析的话题,默认开启)。这属于十分细节的优化,不做过多介绍,但偶然面试也会被问到。
Survivor 区详解
由图解能够看出,Survivor 辨别为两块 S0 和 S1,也能够叫做 From 和 To。在同一个工夫点上,S0 和 S1 只能有一个区有数据,另外一个是空的。
接着下面的 GC
来说,比方一开始只有 Eden 区和 From 中有对象,To 中是空的。
此时进行一次 GC
操作,From 区中对象的年龄就会 +1,咱们晓得 Eden 区中所有存活的对象会被复制到 To 区,From 区中还能存活的对象会有两个去处。
若对象年龄达到之前设置好的年龄阈值(默认年龄为 15 岁,能够自行设置参数‐XX:+MaxTenuringThreshold
),此时对象会被挪动到 Old 区,如果 Eden 区和 From 区 没有达到阈值的对象会被复制到 To 区。
此时 Eden 区和 From 区曾经被清空 (被GC
的对象必定没了,没有被 GC
的对象都有了各自的去处)。
这时候 From 和 To 替换角色,之前的 From 变成了 To,之前的 To 变成了 From。也就是说无论如何都要保障名为 To 的 Survivor 区域是空的。
Minor GC
会始终反复这样的过程,晓得 To 区被填满,而后会将所有对象复制到老年代中。
Old 区概览
从下面的剖析能够看出,个别 Old 区都是年龄比拟大的对象,或者绝对超过了某个阈值 (-XX:PretenureSizeThreshold
, 默认为 0,示意全副进 Eden 区) 的对象。在 Old 区也会有 GC
的操作,Old 区的 GC
咱们称作为Major GC
。
对象的整个生命周期
对象 1
我是一个一般的 Java 对象,我出世在 Eden 区, 在 Eden 区我还看到和我长的很像的小兄弟,咱们在 Eden 区中玩了挺长时间。
有一天 Eden 区中的人切实是太多了,我就被迫去了 Survivor 区的“From”区,自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的“From”区,有时候在 Survivor 的“To”区,居无定所。
直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。
于是我就去了年轻代那边,年轻代里,人很多,并且年龄都挺大的,我在这里也意识了很多人。在年轻代里, 我生存了 20 年 (每次GC
就加一岁),而后被回收。
对象 2
我天生就是个特例,不同凡响,出世就和小孩儿一样大,于是 Eden 区说你太大了,咱们这里不你适宜,而后就间接把我送到了老年区。在老年混着混着就老死了(被回收了)。
堆中常见问题
如何了解 Minor/Major/Full GC?
面试频率也是相当高的,个别问你:请说一下 Minor/Major/Full GC 别离发送在哪个区域。
Minor GC:产生在年老代的 GC
Major GC:产生在老年代的 GC。
Full GC: 新生代 + 老年代,比方 Metaspace 区引起年老代和老年代的回收。
为什么须要 Survivor 区? 只有 Eden 不行吗?
如果没有 Survivor,Eden 区每进行一次 Minor GC,并且没有年龄限度的话,存活的对象就会被送到老年代。
这样一来,老年代很快被填满, 触发 Major GC(因为 Major GC 个别随同着 Minor GC,也能够看做触发了 Full GC)。
老年代的内存空间远大于新生代,进行一次 Full GC 耗费的工夫比 Minor GC 长得多。
执行工夫长有什么害处?
频发的 Full GC 耗费的工夫很长, 会影响大型程序的执行和响应速度。
可能你会说,那就对老年代的空间进行减少或者较少咯。
如果减少老年代空间,更多存活对象能力填满老年代。尽管升高 Full GC 频率,然而随着老年代空间加大,一旦产生 FullGC,执行所须要的工夫更长。
如果缩小老年代空间,尽管 Full GC 所需工夫缩小,然而老年代很快被存活对象填满,Full GC 频率减少。
所以 Survivor 的存在意义,就是缩小被送到老年代的对象,进而缩小 Full GC 的产生,Survivor 的预筛选保障,只有经验 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?
新生代中的可用内存:复制算法用来担保的内存为 9:1,所以只会造成 10% 的空间节约。
可用内存中 Eden:S1
区为 8:1
即新生代中Eden:S1:S2
= 8:1:1
这个比例,是由参数 -XX:SurvivorRatio
进行配置的(默认为 8)。
举荐浏览:
《MySQL 技术底细(第 5 版)》.pdf
《高效程序员的 45 个习惯:麻利开发修炼之道》.pdf
《Docker 全攻略》.pdf
关注公众号“Java 后端技术全栈”
收费获取 500G 最新学习材料