什么是垃圾?
没有任何引用指向的对象,就是垃圾
如何找到垃圾?(2 种方法)
过程:先找到正在使用的对象,然后把没有正在使用的对象进行回收
1.引用数-Reference-Count
被引用数为 0 的即为垃圾,在 Java 领域,至少主流的 Java 虚拟机里面都没有选用引用计数算法来管理内存。原因是该算法不能回收循环引用,有缺陷,怎么办呢?根可达算法可以弥补这个缺陷(Root Searching)
2.根可达算法-Root Searching(这个必须要牢牢记住)
-
如何理解根可达?
算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的 -
GC Roots(根对象)都有哪些东西?
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
- 类的静态成员变量的引用:T.class 对静态变量初始化能够访问到的对象是根对象
- 常量池:一个 class 用到的其他的 class 对象叫做根对象
- JNI 指针:本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
- 什么是根对象?
一个线程启动时需要的对象就是根对象
引用数和根可达算法都提到了“引用”,Java中有哪些引用类型?
-
强引用(不被回收)
当内存空间不足,系统撑不住了,JVM 就会抛出OutOfMemoryError 错误。即使程序会异常终止,这种对象也不会被回收。这种引用属于最普通最强硬的一种存在,只有在和 GC Roots 断绝关系时,才会被消灭掉 -
软引用(内存够不回收,不够再回收)
在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。 -
弱引用(只要发生GC,就会被回收)
当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用拥有更短的生命周期 -
虚引用
这是一种形同虚设的引用,在现实场景中用的不是很多
如何清除垃圾?(三种算法,必须背过)
-
标记清除-Mark-Sweep
先标记,再清除,存活对象比较多的时候,清除的效率比较高,但是需要扫描两遍(如何理解 2 遍?第一次扫描先找到那些有用的,第二次扫描再找到那些没用的并清除),效率偏低,很容易产生碎片 -
拷贝-Copying
拷贝也称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存 活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复 制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有 空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷 也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费太多了一 点,但是效率是最高的
优点:适用于存活对象较少的情况,而且只扫描一次,效率比较高,且没有碎片
缺点:空间浪费,移动复制对象,需要调整对象引用 -
标记压缩为紧凑-Mark-Compact
清理垃圾的过程中,把存活的对象全部扔到前面的位置,然后大块的内存就出来了
优点:不会产生碎片,不会产生内存减半的问题
缺点:也需要扫描两次,第一次标记有用对象,第二次移动对象,如果移动的过程是多线程的效率就会低很多
堆内存逻辑分区
研究表明,大部分对象,可以分为 2 类
-
大部分对象的生命周期都很短;
-
其他对象则很可能会存活很长时间
大部分死的快,其他的活的长。这个假设称之为弱代假设。
现在的垃圾回收器,都会在物理上或者逻辑上,把这两类对象进行区分。我们把死的快的对象所占的区域,叫作年轻代(Young generation)。把其他活的长的对象所占的区域,叫作老年代(Old generation)
年轻代
-
年轻代采用的算法
采用复制算法(Copying)。因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。复制算法会造成一定的空间浪费,所以年轻代中间也会分很多区域:年轻代分为:一个伊甸园空间(Eden ),两个幸存者空间(Survivor )
-
年轻代的 GC 过程
一个对象产生之后首先在栈上分配,如果栈上空间不够,会进入伊甸区(Eden),当年轻代中的 Eden 区分配满的时候,就会触发年轻代的 GC(Minor GC)。具体过程如下:
1. 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区(以下简称 from);
2. Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理。存活的对象会被复制到 to 区;接下来,只需要清空 from 区就可以了。
所以在这个过程中,总会有一个 Survivor 分区是空置的。Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。这个比例,是由参数-XX:SurvivorRatio进行配置的(默认为 8)。 -
一个对象的分配逻辑
1.栈上分配
栈上分配对象比在堆上分配要快很多
2.TLAB 上分配
TLAB 的全称是 Thread Local Allocation Buffer,JVM 默认给每个线程在 Eden 区中开辟一个 buffer 区域,默认占用 1%Eden 的空间,用来加速对象分配,这个道理和 Java 语言中的 ThreadLocal 类似,避免了对公共区的操作,以及一些锁竞争。
3.Eden 分配
老年代
- 老年代垃圾回收算法
老年代一般使用“标记-清除”、“标记-整理”算法,因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,还不如采取就地收集的方式。 - 对象是怎么进入老年代的?
-
提升(Promotion):年龄达到阈值则进入老年代
如果对象够老,会通过“提升”进入老年代。关于对象老不老,是通过它的年龄(age)来判断的。每当发生一次 Minor GC,存活下来的对象年龄都会加 1。直到达到一定的阈值,就会把这些“老顽固”给提升到老年代。这些对象如果变的不可达,直到老年代发生 GC 的时候,才会被清理掉。这个阈值,可以通过参数‐XX:+MaxTenuringThreshold 进行配置,最大值是 15,因为它是用 4bit 存储的(所以网络上那些要把这个值调的很大的文章,是没有什么根据的)。 -
分配担保:Survivor 空间不够,直接分配到老年代空间
每次存活的对象,都会放入其中一个幸存区,这个区域默认的比例是 10%。但是我们无法保证每次存活的对象都小于 10%,当 Survivor 空间不够,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配 -
大对象直接在老年代分配
超出某个大小的对象将直接在老年代分配。这个值是通过参数-XX:PretenureSizeThreshold进行配置的。默认为 0,意思是全部首选 Eden 区进行分配。 -
动态对象年龄判定
有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。比如,如果幸存区中相同年龄对象大小的和,大于幸存区空间大小的一半,大于或等于 age 的对象将会直接进入老年代。
JVM 内存分代模型
除了 ZGC,Epsilon , Shenandiah 之外,都是使用逻辑分代模型
G1 是逻辑分代,物理不分代,除此之外不仅逻辑分代,而且物理分代
YGC:年轻代空间耗尽时触发
FullGC:在老年代无法继续分配空间的时候触发,新生代,老年代同时进行回收
逃逸分析
https://www.cnblogs.com/javastack/p/12923778.html
逃逸:某个变量只在某个方法内部有效叫做无逃逸,如果不止在某个方法内有效,则是逃逸的。类的成员变量是逃逸的,方法变量则是无逃逸的
- 查看 JVM 参数
java -XX:+PrintCommandLineFlags -version
垃圾收集器跟内存大小的关系
- Serial 几十兆
- PS 上百兆 - 几个G
- CMS - 4~6G以下的堆内存
- G1 - 6G以上的
- ZGC - 4T - 16T(JDK13)
常见垃圾回收器组合参数设定
-XX:+UseSerialGC 年轻代和老年代都用串行收集器
-XX:+UseParNewGC 年轻代使用ParNew,老年代使用 Serial Old
-XX:+UseParallelGC 年轻代使用Paraller Scavenge,老年代使用Serial Old
-XX:+UseParallelOldGC 新生代Paraller Scavenge,老年代使用Paraller Old
-XX:+UseConcMarkSweepGC,表示年轻代使用ParNew,老年代的用CMS + Serial Old
-XX:+UseG1GC 使用G1垃圾回收器
-XX:+UseZGC 使用ZGC垃圾回收器
STW
如果在垃圾回收的时候(不管是标记还是整理复制),又有新的对象进入怎么办?
为了保证程序不会乱套,最好的办法就是暂停用户的一切线程。也就是在这段时间,你是不能 new 对象的,只能等待。表现在 JVM 上就是短暂的卡顿,什么都干不了。这个头疼的现象,就叫作 Stop the world。简称 STW。
标记阶段,大多数是要 STW 的。如果不暂停用户进程,在标记对象的时候,有可能有其他用户线程会产生一些新的对象和引用,造成混乱。
现在的垃圾回收器,都会尽量去减少这个过程。但即使是最先进的 ZGC,也会有短暂的 STW过程。我们要做的就是在现有基础设施上,尽量减少 GC 停顿。
1. Serial(回收时会停顿,现在用的很少)
单线程清理垃圾,(STW)Stop The World
其中有一个 safe point 的概念需要注意:线程并不是立马就停下来,而是找一个安全点停止
单机 CPU 效率最高,内存比较小的时候可以接受这种垃圾回收器,内存越大,垃圾回收的时间越长,因此这种垃圾回收器在 server 端使用的越来越少。通常使用在客户端上
2.Serial Old 组合
单线程在老年代回收垃圾
3.ParallelScavenge + Parallel Old 组合(简称 PS+PO 还有很多公司在使用,如果不设置,就是这么一个组合)
PS 使用复制算法清除垃圾
PO 使用标记整理算法清除垃圾
多线程清理垃圾
4.ParNew(Parallel New)+ CMS 组合
ParNew 是 ParallelScavenge 的一个增强变种,为了和 CMS 搭配使用
(STW)Stop The World