~~~上一篇章了解到了JVM内存分布,之所以把堆区放在第二篇来讲,是因为堆在我们整个JVM学习过程中起着最为关键和重要的角色,同时对于我而言,这块有必要着重详细地记录下笔记。~~~
学习java之初,我们知道类中创建的实例对象、数组都是存放在堆区(Heap),对象的引用、局部变量存放栈区(Stacks),类、方法、全局变量、静态变量、常量池存放在方法区。
(ps~~~这是Java8之前的版本存储分布),看到对象,应该感到眼睛一亮吧。没错,java设计之初就是基于面对对象而开发的语言。那么,我们来好好看看这个堆吧。。。。。。
1. 堆区介绍
堆,这块OOM(OutOfMemory)最主要发生的区域,它也是内存区域中最大的一块区域。被所有线程共享,存储着几乎所有的实例对象、数组,所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析技术【JIT逃逸】逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。(ps:世事变化,凡事无绝对,否则打脸啪啪响)
伴随着OOM的发生,自然需要垃圾回收,因此堆也是垃圾收集器管理的主要区域,也称为“GC堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代。再细一点,可以继续将新生代分为 Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1)等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
2. 堆区的默认空间分配
查看虚拟机的默认配置:
### java -XX:+PrintFlagsFinal -version
>java -XX:+PrintFlagsFinal -version
[Global flags]
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
..........
..........
..........
uintx InitialBootClassLoaderMetaspaceSize = 4194304 {product}
uintx InitialCodeCacheSize = 2555904 {pd product}
uintx InitialHeapSize := 201326592 {product}
uintx InitialRAMFraction = 64 {product}
uintx InitialSurvivorRatio = 8 {product}
uintx InitialTenuringThreshold = 7 {product}
uintx InitiatingHeapOccupancyPercent = 45
..........
..........
..........
uintx NewRatio = 2 {product}
uintx NewSize := 67108864 {product}
uintx NewSizeThreadIncrease = 5320 {pd product}
参数解释:
3. 堆区调整
Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整。事实上,为满足程序性能上的最优化,也有必要去调整。
通过设置如下参数,可以设定堆区的初始值和最大值,比如 -Xms256M -Xmx 1024M,其中 -X 这个字母代表它是 JVM 运行时参数,ms 是 memory start 的简称,中文意思就是内存初始值,mx 是 memory max 的简称,意思就是最大内存。 在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力所以在线上生产环境中 JVM 的 Xms 和 Xmx 会设置成同样大小,避免在 GC 后调整堆大小时带来的额外压力。
4. 创建对象的分配流程
绝大部分对象在 Eden 区生成,当 Eden 区装填满的时候,会触发 Young Garbage Collection,即 YGC。垃圾回收的时候,在 Eden 区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到 Survivor 区。Survivor 区分为 so 和 s1 两块内存空间。每次 YGC 的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果 YGC 要移送的对象大于 Survivor 区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,就像人到了 18 岁就会成年一样,在 JVM 中 -XX:MaxTenuringThreshold 参数就是来配置一个对象从新生代晋升到老年代的阈值。默认值是 15,可以在 Survivor 区交换 14 次之后,晋升至老年代。
有关GC将会在后面的篇章中讲到,这里简单提前学习些,从图中可以看到新生代的Minor GC和老年代的Full GC,那么这个两个GC有什么区别呢?《深入理解Java虚拟机》讲到:
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
再了解另外一个概念:空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于 ,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每-次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。.取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败( Handle Promotion Failure )。如果出现了HandlePromotionFailure失败,那就 只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的, 但大部分情况下都还是会将HandlePromotionFailure开关打开,避免FullGC过于频繁。
**************************************************************************************************************************************************
技术之路从来就没有捷径可言,以前姿势不对,导致荒废了不少时间,也深刻迫切感到压力。总是很浮躁,但这对于技术学习来说,却是最为致命的。希望为时还不晚,今后系统学习,点滴积累,从基础到扩展,慢慢量变到质变。学习过程中,参考了很多前辈大牛的文章,如觉得侵权积累知识行为,一方面希望多多包涵,另一方面也麻烦告知删除,Thanks~~~