Java中的每个对象都经历了创建、使用和最终被回收的过程。从对象实例化开始,它可能被程序的多个部分引用,直到最后一个引用消失,对象成为垃圾,等待回收。
JVM垃圾查找算法
(1)引用计数法:已淘汰,为每个对象添加引用计数器,引用为0时判定可以回收,会有两个对象相互引用无法回收的问题
(2)可达性分析法:从GCRoot开始往下搜索,搜索过的路径称为引用链,若一个对象GCRoot没有任何的引用链,则判定可以回收(三色法:未标记、标记中、已标记)
可以作为GC Roots的主要有四种对象:虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中JNI引用的对象
JVM的垃圾回收算法
(1)标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。
(2)复制算法: 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。用在新生代
优点:解决碎片化问题,顺序分配内存简单高效
缺点:只适用于存活率低的场景,如果极端情况下如果对象面上的对象全部存活,就要浪费一半的存储空间。
(3)标记整理算法: 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。用在老年代
(4)分代收集算法:一般将 java 堆分为新生代和老年代,比如在新生代中,选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,选择“标记-清除”或“标记-整理”算法进行垃圾收集。
分代回收机制
- 在年轻代,对象的存活率相对较低,因此采用如标记-复制算法会更为高效。
- 老年代中的对象已经证明了自己的存活能力,所以此处的GC会比年轻代更加稀少,可以使用如标记-清除-整理算法进行处理。
- 年轻代主要分为
Edan
区、Survivor区(from/S1
区、to/S2
区),这三者的区域大小划分比例为8:1:1
- 年轻代中对象被GC清理在S1与S2轮转
15
次之后会被移到老年代(这个值可以通过JVM参数:–XX:SurvivorRatio 设定,默认值为8) - 年轻代与老年代区域大小划分比例为
1:2
(该值可以通过JVM参数:-XX:NewRatio 设定)
当GC确定一些对象为“不可达”时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。## 第一步:
大对象直接进入老年代
长期存活对象将进入老年代1. Eden 区
Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
2. ServivorFrom
上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
3. ServivorTo
保留了一次 MinorGC 过程中的幸存者。
## 第二步:
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:
首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。## 第三步:
当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常.永久代:主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
强引用、软引用、弱引用、虚引用?
在 Java 中,Reference
类及其子类是用于实现引用对象的基类。Reference
类主要有三个子类:SoftReference
、WeakReference
、和 PhantomReference
,它们分别代表软引用、弱引用和虚引用。
- 强引用:是我们经常写的普通对象引用,把一个对象赋给一个引用变量(在 C/C++ 中的名词称为指针变量),这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于
可达状态
,它是不可能被垃圾回收机制回收的,因此强引用是造成 Java 内存泄漏的主要原因之一。 - 软引用:``软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收
- 弱引用:当一个对象只被弱引用引用时,在下一次
垃圾回收(GC)时,即使内存空间足够,也会被回收
- 虚引用:形同虚设,虚引用并不会决定对象的生命周期,
作用是跟踪对象被垃圾回收的状态以及引用对象被放入与之关联的引用队列中,允许程序员在适当的时机进行一些额外的操作
。
finalize()方法?
垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法:
- 第一次被标记后,对象在在finalize()中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可
- 回收特殊渠道申请的内存,一般内存不需要亲自回收,但JNI(Java Native Interface)调用non-Java程序(C或C++)产生的无法被gc自动回收。
收集器种类
- Serial收集器: 新生代采用复制算法,老年代采用标记-整理算法。单线程,所以在发生GC时候需要暂停所有线程的工作(“Stop-The-World”)。
- Parallel Scavenge收集器:(相比Serial GC只是垃圾回收线程变多而已)适用于吞吐量比较高的场景,一些计算场景并不在意停顿时间的长短,还存在STW现象(“Stop-The-World”)
- ParNew收集器:(jdk8默认设置的垃圾收集器)新生代采用复制算法,老年代采用标记-整理算法。和Parallel 相似主要用于配合CMS收集器使用。
- CMS收集器:低延迟的并发型垃圾收集器。适用于希望减少暂停时间的应用。(用户和垃圾回收线程可以同时工作,当然还需要少量的STW用于清除浮动垃圾),采用“标记-清除”算法,是老年代的垃圾收集器,只能配合ParNew或Serial使用。没有整理过程,会导致内存碎片化。
- G1 GC:从JDK9开始,它作为默认的垃圾回收器。它将堆分为多个区域(Reigon)并发地标记、复制和清除这些区域。限制垃圾回收的暂停时间,并提供高吞吐量。
- ZGC:ZGC的目标是在任何堆大小下都能实现不到10毫秒的暂停时间,同时还能提供与其他垃圾回收器相似的吞吐量。
## 如何选择:
**响应时间要求**:如果应用对延迟非常敏感,那么选择如ZGC或CMS这样的暂停时间短的垃圾回收器会更合适。
**吞吐量要求**:高吞吐量的应用,如批处理作业或某些后端任务,可能更适合使用Parallel GC或G1 GC。
**内存资源**:如果内存资源有限,Serial GC可能是一个好选择。
监控垃圾回收
GC日志: JVM可以配置为输出GC日志,这些日志详细记录了垃圾回收的过程和结果。通过分析这些日志,开发者可以获取关于内存使用情况、垃圾收集的频率和持续时间等重要信息。
监控工具: 工具如JVisualVM和JConsole不仅可以实时显示JVM的性能指标,还提供了丰富的图形界面,帮助开发者直观地了解垃圾回收的行为。