8 垃圾回收

目录:

 

 

8.1 如何判断对象可以回收

8.1.1 引用计数法

只要一个对象被引用,那计数就+1,引用两次,那就+2。如果没被引用了,那就-1。到0之后那就回收。

但是引用计数存在一个大问题:循环引用的问题。

 

 

A引用B,B的引用计数是1。B引用A,A的引用计数也是1。这两个对象没有谁再引用他们。他们各自引用计数都是1,虽然这两个对象都不能被使用了,但是他们引用计数导致不能被回收,内存泄漏。

8.1.2可达性分析算法

java虚拟机使用的。

首先确定一系列根对象(不能被回收的)。回收之前,先对堆中所有对象进行一遍扫描。看对象是不是被根对象直接间接引用,如果是就不能被回收,不是的话就回收。

哪些对象可以作为GC Root?

  • 系统类 System.Class (Object啊,hashmap啊之类的)

  • Native Stack 本地方法

  • 被加锁的对象sychronized

  • 活动线程Thred

8.1.3 四种引用(面试高频)

请你回答java中的四种引用有哪些?

  • 强引用

  • 弱引用

  • 软引用

  • 虚引用

  • 终结器引用

我们平时用的都是强引用 String buffer = new StringBuffer(); buffer就是一个强引用,强引用我们new出来的对象。强引用引用的对象是不能被回收的。没有GC root强引用的时候就可以被垃圾回收。

软弱引用跟强引用的区别:只要A2,A3没有被直接强引用,那么都可能会被垃圾回收。GC回收时,当内存不足,认为软引用的对象不重要,直接被当垃圾回收掉。弱引用的话无论内存充不充足都会被当垃圾回收掉。

软弱引用还可以配合引用队列一起工作,当我的软引用的对象被回收了,软引用自身也是一个对象。他如果在创建时给他分配一个引用队列。当它所引用的对象被回收时,软引用就会进入这个队列,弱引用也是如此。

 

 

为什么会做这个处理呢?

  • 软弱引用本身也要占内存,如果想释放他们的时候,就去引用队列里面找把他们干掉。

虚引用和终结器引用和上面的软弱不同,软弱可以配合引用队列,也可以不配合引用队列来使用。而虚引用和终结器引用必须配合引用队列使用。

虚引用的作用:在我们所引用的虚引用的对象被垃圾回收时,虚引用对象自己进入引用队列,从而间接由一个线程来调用虚引用方法,然后调用unsafe.freeamemory方法区释放直接内存

 

 

终结器引用:所有的java对象都继承Object类,Object中都有一个finallize()终结方法。当我这个对象重写终结方法,并且没有强引用引用他时,那就可以被回收。那么这个终结方法什么时候被调用呢?我们希望他在GC进行垃圾回收的时候被调用,那么就是靠终结器来干这个事。

当对象没有强引用时,虚拟机会帮我们自动创建终结器引用。当这个对象被垃圾回收时,先把终结器引用对象假如引用队列,再由一个优先级很低的进程有空的时候去查看引用队列当中有没有终结器引用。如果有,那就通过他去找到要回收的对象,并且调用他的finallize()方法。等调用完了,下一次垃圾回收时,就可以真正的干掉这个对象占用的内存。

这种太随机了,不推荐使用finallize()去释放资源

总结:

 

 

8.1.3.1 软引用使用场景

在我们开发中,如果去网上找一张图片用强引用写进代码,当内存小时会报错,但是如果我们使用软引用,则不会报错。

使用强引用装进堆内存

 

 

使用弱引用

 

 

8.1.3.2 引用队列

清理无用的软引用。

 

 

 

8.1.3.3 弱引用

 

 

 

8.2 垃圾回收算法

8.2.1 标记清除

 

 

优点:速度快

缺点:容易产生内存碎片,虽然总的来看空闲内存多,但是因为是不连续的,如果我想要放一个大数组进来,还是放不下。

8.2.2 标记整理

避免标记清除时内存碎片问题。

找到空闲的内存之后,他把占用内存的对象移到一块,那我连续的空间就变多了。

 

 

优点:没有内存碎片

缺点:因为牵扯到了对象的移动,比较麻烦,速度慢。

8.3.3 复制算法

找到空闲的,标记。把使用的部分移到一块相同大小的内存中。然后清除左边的内存。最后再交换两块内存的位置,恢复原状。

清除前:

 

 

清除中:

 

 

最后:

 

 

优点:没有内存碎片

缺点:会占用双倍的内存空间

 

8.3.4 分代垃圾回收定义

实际JVM不会只采用其中一种回收算法,他会综合来使用。综合完之后叫做分代垃圾回收。

他把我们堆内存划分成两大块

  • 新生代(伊甸园,幸存区From , 幸存区To)

  • 老年代

思考为什么要做这样划分?

  • java中有些对象需要长期使用,有些就用一会。那么我分区来放的话,也可以设置垃圾回收的频率和不同的算法。新生代的频繁一点。老年代的就回收频率低一点。

8.3.5 分代回收工作原理

对象创建的时候,分配到伊甸园里面去。伊甸园满了之后触发一次垃圾回收(新生代的我们叫Minor GC),采用复制算法。把使用的内存放到幸存区To里面去,然后交换位置变为幸存区From。并且把幸存的对象寿命标记一个1。第二次GC除了标记伊甸园里面的,还会去看你幸存区的哪些不用了。不用了就干掉,还用的那就把寿命变为2。

 

 

假如一个对象在幸存区里的寿命超过我设置的阈值15(4bit 1111转换为10进制就是15,不同垃圾回收器阈值不一样。),那就把它移到老年代里面去养老。

 

 

如果新生代和老年代都没有空间了,那就触发一次Full GC,大扫除。

注意minor GC的时候会引发 stop the world。其它线程别忙了,我要收垃圾了,等我收完你们再干事。因为我们垃圾回收过程中,对象的地址会改变,如果其它线程还允许,那就乱套了。

8.3.6 相关VM参数

 

 

8.3.7 案例演示

 

 

大对象如果在新生代放不下,但是老年代有空间的话会直接晋升。就不会触发GC。

注意一个线程内的out of memory 并不会导致其它线程也挂。

 

 

 

8.3 垃圾回收器

分为3种:

  • 串行回收器(楼层小低一个保洁员解决)

    • 单线程

    • 堆内存较小,适合个人电脑

  • 吞吐量优先回收器(楼层多高找多个保洁员)

    • 多线程

    • 堆内存较大,多核cpu(多把扫把)

    • 适合服务器上

    • 单位时间内,stw的时间最短 0.2 0.2 = 0.4

  • 响应时间优先

    • 多线程

    • 堆内存较大,多核cpu

    • 适合服务器上

    • 尽可能让单次stw的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

8.3.1 串行

开关:

 

 

8.3.2 吞吐量优先

在jdk1.8中默认就是使用这个。

开关:

 

 

你只要开启一个,另外一个就会自动开启

 

 

线程数可以用参数来控制

 

 

动态自动调整伊甸园幸存区大小

 

 

最大暂停时间和吞吐量设置

 

 

ratio一般设置为19 ,也就是100秒内,我允许5秒暂停时间。

 

8.3.3 响应时间优先

开关:

 

 

concurrent 并发 mark 标记 sweep 清除

注意并发和并行是不一样的。并发是指垃圾回收线程在工作的同时,其它用户线程也能够同时工作。用户线程和垃圾回收线程并发执行,都要去抢占cpu。并行是指垃圾回收器工作期间,其它用户线程就好好待在。cpu是他一个人的,很霸道。

那么这个并发的话就减少了stop the world的时间。

如果并发失效的话,那就降级为串行。

工作流程:

 

 

并行线程数和并发线程数的设置,一般把并发的设置为并行的四分之一

 

 

也就是一个线程占用cpu去进行垃圾回收。剩余3个线程留给用户线程去工作。对整个进程的吞吐量有影响。

设置什么时候开始进行响应时间优先垃圾回收

 

 

设置一个百分比,比如我老年代占了百分之80空间了,那就进行垃圾回收。预留出来的百分之20空间,因为我是并发的清理,那我在清理过程中,其它的用户线程也会产生垃圾,所以要预留一点空间来装这些新产生的浮动垃圾。

 

在你做重新标记之前,我先对新生代进行垃圾回收。减轻我重新标记时的压力

 

 

响应时间优先垃圾回收器存在的问题:当我并发失败之后,会退化为串行回收器,那时间会大大增加,对用户体验极其不友好。

posted on 2021-10-12 10:41  Love&Share  阅读(42)  评论(0编辑  收藏  举报

导航