二、JVM 入门 —— (四)堆以及 GC
堆的定义
JVM 中最大的一块内存空间,用来存放数据,而且一个JVM实例只存在一个堆内存。
堆的特点
- 堆属于线程共享区域
- 堆空间的大小可以动态调整
- 堆空间在分配时可以不需要再物理上连续,只要在逻辑上链接即可。
- 堆空间会出现大量的垃圾对象(GC主要作用域)
- 堆有属于自己的结构,并且是运行时数据区中默认最大的一块。
存储什么
几乎所有的对象都在堆空间中被创建出来,new 对象和数组。
结构组成
- 物理上,年轻代 + 老年代组成
- 逻辑上,年轻代 + 老年代 + 方法区
因为方法区不是物理上直接相连
新生代
又划分为:
- 新生代又分为两部分: 伊甸园区(Eden space)和幸存者区(Survivor pace) 。
- 幸存者区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。From区和To区。
堆内存内部空间所占比例:
- 新生代
1/3
,老年代2/3
Eden
与S0 S1
的默认比例是:8:1:1
- 或者是
Eden:from:to -> 8:1:1
- 或者是
- 要注意:
from
区是存放由Eden
挪过来的对象,to
区永远都是空的
to 区在 GC 复制算法中也可以更好解释,它为什么一直是空的
再看方法区
首先只是一个概念,具体类信息存放的位置不是方法区这个概念,而是存放在方法区这个概念对应的具体实现上。
方法区的实现有2个:
jdk1.7
及以前实现叫做永久代。jdk1.8
及之后实现叫做元空间。
永久代和元空间的区别:
- 共同点:都是方法区的实现,也都是用来存放类信息。
- 不同点:主要在占用空间上。
对于永久代来说,永久代会占用堆内的空间,导致堆空间变小,势必会导致对象存储变小。未来想把对象放到堆上,就需要 GC,让新对象进来。而 GC 会导致 STW
,频繁发生 GC 用户代码性能降低。
对于元空间来说,元空间不占用堆内空间,而是直接使用物理机内存条。这样堆空间就有更多的空间去容纳新对象
,GC 次数减少,性能就会提高。默认情况下,元空间的大小仅受本地内存限制。
分代机制和分代空间工作流程
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的对象,创建在新生代,在新生代中被垃圾回收。
- 一类是生命周期非常长的对象,创建在新生代,在老年代中被垃圾回收,甚至与JVM生命周期保持 一致。
- 几乎所有的对象创建在伊甸园区,绝大部分对象销毁在新生代,大对象直接进入老年代。
新生代
年轻代是所有对象创建和销毁的主要地方。
Object o = new Object() o = null;
销毁:被GC垃圾回收器回收掉的。
工作过程:
(1)新创建的对象先放在Eden
区。
(2)当 Eden
区的空间用完时,程序又需要创建新对象,此时,触发JVM的垃圾回收器对 Eden
区进行垃圾回收(Minor GC,也叫Young GC),将Eden
区中不再被引用的对象销毁。
(3)然后将 Eden
区的剩余对象移动到空的 S0
区。
(4)此时, Eden
区清空。
(5)被移到 S0
区的对象上有一个年龄计数器,值是1。
from 有对象,minor GC 对 Eden + from GC
from 没对象,minor GC 对 Eden GC
(6)然后再次将新对象放入Eden
区。
(7)如果Eden
区的空间再次用完,则再次触发 Minor GC
,对Eden
区和S0
区进行垃圾回收,销毁不再引用的对象。
(8)此时 S1
区为空,然后将 Eden
区和 S0
区的剩余对象移动到空的 S1
区。
(9)此时,Eden
区和 S0
区清空。
注意:接着jvm会自动将from区和to区的角色进行交换。
from -> to, to -> from
最终记住:to区永远是空的,而且一定要留一个区域出来。留的这个区域永远都是to区。
(10)从 Eden
区被移到 S1
区的对象上有一个年龄计数器,值是 1。从 S0
区被移到S1
区的对象上的年龄计数器+1,值是2。
(11)然后再次将新对象放入 Eden
区。如果再次经历垃圾回收,那么 Eden
区和S1
区的剩余对象移动到S0
区。对象上的年龄计数器+1。
(12)当对象上的年龄计数器达到 15
时(-XX:MaxTenuringThreshold),则晋升到老年代。
新生代小结
总结:
- 针对幸存者s0,s1,复制(复制算法)之后有交换,谁空谁是to
- 垃圾回收时,eden 和 from 区对象会被移动到to区
- 复制--->清空--->互换
为什么有 to 区?
- 复制算法的存在
- to 区进去 老年代阈值只能 调小不能调大 默认15及以下
- 类对象头信息,决定了年龄的
15
阈值不能调大
我们对于类对象进行加锁,就是这样
Sychronized(obj){...
}
那其中的锁标识对象,那么仁义一种锁的都是对象,得有对象才能锁。
原因:任意一个对象都有三部分组成。
- 对象头信息---64位2进制组成010101010101....
- 对象头中包含了 hashcode/锁标识。gc垃圾回收时要判定的对象年龄都在对象头中存储。
- hashcode占用了31,25位是未使用,剩余 8 位
- 4位:给对象的锁标识占用。(任意锁一定得有对象才能用)
- 4位:对象年龄阈值 0000-1111
- 填充数据(字段)
- 对齐字段
老年代存放的对象情况 | 能进入老年代,常见的几种情况
- new 对象 > Eden,直接创建空间 Eden 就不够
- Eden 区放不下,from 区空间也不够,直接老年代
- 年龄阈值到了 15
老年代
经历多次Minor GC仍然存在的对象(默认是15次)会被移入老年代,老年代的对象比较稳定,不会频繁的GC。
若老年代也满了,那么这个时候将产生Major GC(同时触发Full GC),进行老年代的垃圾回收。
MajorGC == FullGC
MajorGC只要进行一次,那么至少会触发一次MinorGC
FullGC:年轻代+老年代+元空间(基本没有垃圾对象)
若老年代执行了full GC之后发现依然无法进行对象的保存,就会产生OOM异常OutOfMemoryError
。
下面图的流程要清楚是怎么流动的。
如果出现java.lang.OutOfMemoryError: Java heap space
异常,说明Java虚拟机的堆内存不够。原因有二:
- Java虚拟机的堆内存设置不够,可以通过参数
-Xms、-Xmx
来调整。 - 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
GC 总结
- 频繁回收新生代---minorgc
- 很少回收老年代--full gc
- 几乎不在永久代/元空间回收
部分收集:
年轻代收集(Minor GC / Young GC):伊甸园区 + 幸存者区垃圾收集
老年代收集(Major GC / Old GC):老年代垃圾收集
混合收集(Mixed GC):收集新生代以及老年代。G1垃圾收集器有这种方式
整堆收集(Full GC):
- 新生代、老年代和方法区的垃圾收集
年轻代GC触发机制(Minor GC ):
年轻代的Eden空间不足,触发Minor GC。
每次Minor GC在清理Eden的同时会清理Survivor From区。
Minor GC非常频繁,回收速度块。
老年代GC触发机制(Major GC 和 Full GC ):
老年代空间不足,触发Major GC。这通常至少会伴随一次Minor GC。
Major GC比Minor GC速度慢的多。
如果Major GC后,内存还不足,就报OOM。
Major GC通常与Full GC等价,因为它收集整个GC堆,包括老年代。
Full GC触发机制:
Full GC(Full Garbage Collection)是Java虚拟机对堆内存中的所有对象进行全面回收的过程。Full GC的 执行时机取决于Java虚拟机的实现和具体的垃圾回收策略。
一般情况下,Full GC发生的情况包括:
- 老年代空间不足,主要是针对大对象直接进入到老年代。
- 元空间空间不足,应用一次加载过多的jar包。
- 老年代空间不足以容纳新生代晋升到老年代的对象。这通常发生在长时间运行的应用程序中,随着对象的逐渐增加,老年代空间可能会变得不足。
- 手动调用
System.gc()
方法或Runtime.getRuntime().gc()
方法可以触发Full GC。
但值得注意的是,这只是建议Java虚拟机进行垃圾回收的请求,并不能保证立即执行Full GC。因为 GC 线程优先级比较低,不能立即执行。
需要注意的是,Full GC是一项资源密集型的操作,会导致应用程序的停顿时间增加(Stop The World - STW),因为在Full GC期间,应用程序的线程会被挂起。
因此,在设计和开发应用程序时,应尽量避免 频繁触发Full GC,以减少对应用程序性能的影响。
调优:不要让 HC 经常发生。能不发生就不发生,真要发生最好是出现 Minor GC。
堆栈方法区关系
HotSpot 的 JVM 是使用指针的方式来访问对象:
- 堆内存用于存放对象和数组
- 堆中会存放指向对象类型数据的地址
- 栈中会存放指向堆中的对象的地址