对类进行计数,如果这个类被调用了,那么就给这个类加1权重
可是这种计数法,会有一个弊端,就是循环调用

循环调用后就会不停的+1,因此这种算法的弊端在java这种调用中是很大的
因此,Java并没有使用这种引用计数法
会将不能被当作垃圾的对象称之为根对象
在垃圾回收以前会对堆中的所有对象进行一个扫描,是否会被根对象直接引用,如果没有被引用,那么就判断可以被垃圾回收

可以发现,根对象也分为了几类,基本类、针对操作系统的类、线程类、锁类
这几种类型就是根对象的大致分类

蓝色直线:强引用
当两个根对象都调用了同一个对象,那么该对象就被说明是强引用
当两个根对象对它的调用都断掉了,那么就可以被垃圾回收了
只有所有GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
public static final int _4MB = 4 * 1024 * 1024;
/**
* 强引用
* 场景:当将多张图片放进一个list集合中时,就可能发生对内存溢出问题
* @throws IOException
*/
@Test
public void strong() throws IOException {
ArrayList<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}仅有软引用引用该对象时,在垃圾回收后,内存仍不足时才会再次发出垃圾回收信号,回收软引用对象 可以配合引用对象来释放软引用自身
/**
* 软引用
* 当新加入对象时,内存不够,那么进行回收
*/
@Test
public void soft(){
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结果:"+list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
[GC 5632K->1389K(19968K), 0.0011407 secs]
[GC 7021K->2299K(19968K), 0.0012354 secs]
[GC 7931K->3134K(19968K), 0.0011630 secs]
[GC 8766K->3901K(19968K), 0.0010361 secs]
[B@2eda0940
1
[B@3578436e
2
[B@706a04ae
3
[GC -- 17437K->17670K(19968K), 0.0012092 secs]
[Full GC 17670K->14596K(19968K), 0.0214907 secs]
[GC -- 14596K->14702K(19968K), 0.0010001 secs]
[Full GC 14702K->2121K(16896K), 0.0149012 secs]
[B@6eceb130
4
[B@10a035a0
5
循环结果:5
null
null
null
[B@6eceb130
[B@10a035a0可以看到最后的集合中,只存在后两个了
/**
* 软引用配合引用队列
*/
@Test
public void softQueue(){
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联了引用队列,当软引用所关联的byte[]被回收时,软应用自己会加入到queue中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 弹出最先进入的对象(队列中无用的软引用对象)
Reference<? extends byte[]> poll = queue.poll();
while(poll != null){
list.remove(poll);// 在集合中清除
poll = queue.poll();// 重新规定弹出对象(刷新一次避免重复循环)
}
System.out.println("=========================循环结果:"+list.size()+"=========================");
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
这里,当 软/弱 引用的对象都被回收后,其自身 软/弱 对象也是一个对象,这时这两个对象也都会分别被放进引用队列,被回收(因为这两个对象也会占用一定的内存) 软/弱 引用是可以选择是否被加进引用队列中回收的
/**
* 弱引用
*/
@Test
public void weak(){
// list > WeakReference > byte[]
ArrayList<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
// 关联了引用队列,当软引用所关联的byte[]被回收时,软应用自己会加入到queue中去
WeakReference<byte[]> ref = new WeakReference<byte[]>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w:list){
System.out.println(w.get()+"======");
}
System.out.println(list.size());
}
System.out.println("=========================循环结果:"+list.size()+"=========================");
}它删除自身,还是得配合引用队列来删除,具体代码跟软引用代码类似,也是在创建弱引用对象时,关联一个引用队列即可.
软/弱 引用可以选择是否加紧引用队列,而虚引用必须配合引用队列使用
最显著的就是 ByteBuffer 》直接内存
在创建虚引用时,就会将虚引用对象地址放进引用队列中被监控,之后队列会间接调用虚引用中的方法
当ByteBuffer被强引用使用完毕后,清除后,还存留有直接内存
强虚引用调用结束(被清除),虚引用对象进入引用队列,执行 Unsafe.freeMemory() 方法回收直接内存

必须配合引用队列使用,主要配合 ByteBuffer使用,被引用对象回首时,会将虚引用对象入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
终结器引用必须配合引用队列使用
当引用对象的不被调用(强引用结束),终结器引用就会被放进引用队列中等待回收
这时候引用对象并没有被删除,JVM 会给一个优先级很低的线程来监控终结器引用对象
这个线程会每隔一段时间就会检测一次,当检测到引用队列中存在终结器引用对象,那么就会顺着引用找到引用对象把它回收
工作效率低,由于是先将终结器引用放入队列,并且监控线程优先级还低,这时候就有可能造成引用对象长时间占用内存不被释放 不推荐

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象


清除速度快,因为只需要做一个标记清楚的处理,所以相对来说比较快。 但是在清除后有个小弊端,就是会产生清除碎片

不同于标记清除算法,在释放后会整理对象,清理内存碎片后,多余的空间就可以更高效的放入更多的对象
它的优点就是空间利用率更加高效,缺点就是速度偏慢:如果对象整理过程中,新出现了引用对象,那么引用地址就会发生改变,这时候又需要一些时间来处理



这种方法优点是不会有内存碎片,而缺点则是会在内存中占据双倍的空间
- 优点:速度较快
- 缺点:会造成内存碎片标记整理(Mark Compact)- 优点:没有内存碎片
- 缺点:速度慢复制算法(Copy)- 优点:不会有内存碎片
- 缺点:需要占据双倍的内存空间
在内存中,分为了两个内存区域,
这样就可以针对生命周期不同的对象做出不同的内存处理算法,提高效率

当有新的对象创建时,会将这个新对象存放到伊甸园内存空间中,
当伊甸园空间中逐步被占用,下一个对象再被创建发现没有足够内存存放了,就会首次触发一个GC回收
这第一次GC也被称为:Minor GC
Minor GC所做的,就是利用上面三种基本算法进行清理。
当选择使用复制算法后,会把未清理的对象存放进幸存区TO中,然后再进行更换位置,To与FROM更换位置。

当过一段时间又一次新生区(伊甸园)满了

触发第二次垃圾回收,第二次GC,不仅会看伊甸园中的对象,还会看幸存区中的对象
如果有对象已经死亡,那么就会被释放
若有对象还活着,那么就会转入幸存区From中,并且生命次数+1
然后 幸存区From 和 幸存区To 两个区域互相交换

在幸存区From中,如果对象超出了一个阈值。 会将该对象转移到老年代中

当老年代内的空间也不够多了,新生代空间也不够了
那么这时候会触发一次 Full GC

Full GC 会对新生代和老年代全部都做一次清理
最后,如果Full GC处理完后,如果新生代和老年代空间仍然不足!那么就会报内存溢出错误
含义 | 参数 |
|---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或(-XX:NewSize=size+ -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX: InitialSurvivoRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX: SurvivoRatio=ratio |
晋升阈值 | -XX: MaxTenuringThreshold=threshold |
晋升详情 | -XX: +PrintTenuringDisribution |
GC详情 | -XX: +PrintGCDetails -verbose:gc |
Full GC 前 Minor GC | -XX: +ScavengeBeforeFullGC |

当生成一个大对象时,如果新生代放不下,那么就会直接晋升到老年代区域
而当出现多个大对象或者一个巨大的对象,老年代也放不下。
那么这时候就会报错:OutOfMemoryError 内存溢出
而在报错前,JVM 会触发一次Full GC,尝试清理一次对象。
@Test
public void test1() throws InterruptedException {
new Thread(()->{
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}).start();
System.out.println("Sleep....");
Threa d.sleep(1000L);
}当GC在某个线程中报错后,并不会导致整个java的运行结束
-XX: +UseSerialGC = serial + serialOld 复制算法 标记整理算法

上图CPU就可以看作是多个线程。
当每个线程运行时,若某个线程需要进行GC,那么其余线程都会在一个安全节点停止运行,等待GC结束后,所有线程才会开始正常运行
-XX: +UseParallelGC ~ -XX: +UseParallelOldGC -XX: +UseAdaptiveSizePolicy # 动态调整新生代和伊甸园的空间比例 // 调整吞吐量的目标:1/1+ratio // GC工作的时间不能够超过总时间的100分之一 // 如果超过了这个时间,那么就会调整堆的大小 -XX: GCTimeRatio=ratio // 最大暂停毫秒数:200ms -XX: MaxGCPauseMillis=ms -XX: ParallelGCThreads=n #控制进行GC的线程数

不同于串行垃圾回收器,这种回收的方式会更加极端。
当某线程需要垃圾回收时,会调用全部线程都进行一次垃圾回收,这时候CPU的占用率是非常高的。(所谓做到尽快将垃圾清理完毕)
-XX: +UseConcMarkSweepGC ~ -XX: +UseParNewGC ~ SerialOld 标记清除 并发执行 / / 并发数线程设置,一般设置为1/4 -XX: ParallelGCThreads=n ~ -XX:ConcGCThreads=threads // 控制何时来处理垃圾回收的时间=内存占比(当达到阈值后就会进行一次垃圾回收 一般是6%) -XX: CMSInitiatingOccupancyFraction=percent // 在重新标记之前,做一次垃圾回收 (主要目的是减少重新标记的时间) -XX: +CMSScavengeBeforeRemark

该策略针对于老年代。
该策略对cpu占用率不高,但是用户线程的吞吐量会降低 因为当处理垃圾回收时,总有一个线程会占用一部分cpu使用率。而这部分cpu使用率被垃圾回收线程占用了,那么用户线程就会被减少占用。
当内存碎片过多的时候 ConcMarkSweepGC 就不工作了,这时候就会退化为 SerialOld 做一次串行的垃圾回收
定义:Garbage First
适用场景:
相关JVM参数
-XX: +UseG1GC -XX: G1HeapRegionSize=size // 设置G1在堆中Region的大小 例:1024,2048 -XX: MaxGCPauseMillis=time

当新生代区域满了后,会对新生代做一个并发标记,当并发标记做完,就会进行一次混合收集。混合收集结束,那么就会对新生代和老年代区域集体做一次垃圾回收
G1把整个堆分成了大小相同的 region,每个堆大约可以有 2048 个region,每个 region 大小为 1~32 MB (必须是 2 的次方)。如下图:

假设图中就是堆中的一块内存区域,绿色格子(E)就代表一个伊甸园区域。
当伊甸园区域逐渐被占满,这时候就会触发一次新生代的垃圾回收

触发垃圾回收后,会使用复制算法,将对象拷贝进蓝色格子(S)幸存区中

逐渐的,当幸存区中的对象也过多时,那么就会将幸存区符合要求的对象存放进橙色格子(O)老年代区域中。
同时会在堆中开辟出一块新的幸存区用于接受新的对象,老的幸存区会将不符合要求的对象存放进新幸存区中。

会对E、S、O进行全面垃圾回收
-XX: MaxGCPauseMillis=ms

在处理新生代时,伊甸园区域会存放进幸存区,一部分幸存区为符合标准的对象也会被转移至另一个幸存区,符合标准的对象才会晋升为老年代区域,其余幸存区将会进行回收
在处理老年代时, JVM会优先考虑垃圾最多的一块区域,然后使用复制算法拷贝将符合存活标准的对象拷贝进另一块老年代区域中,剩下的的老年代区域就会被清除。
主要目的就是为了减少时间消耗 并不是会清理所有的老年代区域,只是老年代中空间不够了的区域才会被回收
- 新生代内存不足发生的垃圾回收 -minor gc
- 老年代内存不足发生的垃圾回收 -full gcParallelGC- 新生代内存不足发生的垃圾回收 -minor gc
- 老年代内存不足发生的垃圾回收 -full gcCMS- 新生代内存不足发生的垃圾回收 -minorgc
- 老年代内存不足G1- 新生代内存不足发生的垃圾回收 -minor gc- 老年代内存不足CMS 和 G1 引入了并发标记过程,一般情况下只要业务吞吐量没有超过并发标记的数量就不会触发Full GC。 而如果吞吐量超过了并发标记数量,那么在CMS中就会进行一个串行垃圾回收,全部线程进行一次垃圾回收。当超出阈值后,内存依旧不足,那么就会进行一次Full GC 不同于CMS,G1在老年代区域中会使用复制策略进行垃圾回收。当超出阈值后,内存依旧不足,那么就会进行一次Full GC

在老年代中,对于一些对象会引用新生代中的对象,这些对象会被标记为脏卡。
而在寻找这些跨代调用问题时,就不会关注老年代中所有的对象,只会关注那些被标记为脏卡的对象。

当被调用的新生代对象失效时,会主动的更新脏卡队列。卡表的更新是很频繁的,当出现调用关系的取消时,脏卡队列就会进行一次更新。

在不停重复晋升灰、黑色集合中,留在白色集合中的对象必定是没有被调用的对象,那么最后进行回收步骤时就可以大方回收掉白色集合中的所有对象了
可以看到三色标记法有很多步骤,而这些步骤是和用户线程并发运行的,也就是说在标记过程中,用户还在创建新对象,或者抛弃老对象。
先讲创建新对象的情况:
这种情况下,X是白色的,而且按照三色标记法的规则,黑色的A是不会再次被标记的。如果不能把X变成灰色,那么它就会被垃圾回收掉,这个是是存在问题的。
因此,在标记开始之后,需要在对象引用更新的地方添加一个Pre-Write Barrier,用来将X直接标记为灰色。


C对象被放进队列中后,在垃圾回收时会再次进行一次检查,发现有写屏障并有调用关系,那么就会将该对象标记为黑色。这样就会避免被垃圾回收掉

-XX: +UseStringDeduplication
String s1 = new String("hello"); // ['h','e','l','l','o']
String s2 = new String("hello"); // ['h','e','l','l','o']所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX: +ClassUnloadingWithConcurrentMark // 默认启用

预备知识:
- CMS、G1- ParallelGC- Zing(几乎没有延迟时间)付费 允许Java应用程序利用他们需要执行的任何数量的内存,仅受系统中物理内存或虚拟机管理程序可识别的数量的限制。 由于- ZGC(执行效率超低延迟) ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点,使用 –XX:+UseZGC 可以启用 ZGC。内存多重映射和染色指针的引入,使 ZGC 的并发性能大幅度提升。
ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有 GC Roots,STW 时间 GC Roots 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。可见整个 ZGC 的 STW 时间几乎只跟 GC Roots 数量有关系,不会随着堆大小和对象数量的变化而变化。
最快的GC,其实时不发生GC。能不GC是最好的
查看FullGC前后的内存占用,考虑下面几个问题?
1. 对象图
2. 对象大小是否存在内存泄漏?- 强/弱- 第三方缓存实现新生代的特点
-Xmn
先说结论,不是。
当新生代空间越来越大,那么老年代的空间就会被压缩。同时一旦FullGC,会拉长FullGC的执行时间,并且运行曲线下降会越来越慢

这里Oracle官方给出的大小建议:新生代空间需要在老年代大小的25%~50%之间
如果幸存区空间太小,可能会导致将一些活跃对象提前晋升到老年代,那么这时候就需要等到老年代GC时,这些对象才会被清除,这无疑增加了这些对象的生命周期

以CMS为例
- 调整新生代或老年代的空间- 先查看CMS中哪个阶段耗时太长,再看看那个阶段是哪里耗时太长,并根据问题进行实际解决- 1.7中使用的是元空间,不如1.8的空间那么宽裕,所以只能扩展原空间的大小