1、垃圾判断算法
- 引用记数算法:对当前对象的引用进行记数,有引用+1,没有引用-1,当引用记数为0时,回收对象所占的内存;又一个缺陷是,不能回收循环引用的对象
- 可达性分析算法
2、内存分配算法
- 指针碰撞(适用于堆内存完整的情况)
上面这个图片表示的是当前正在进行内存分配的一整块内存块,其中红色区域表示的是已使用的内存,白色区域表示未使用内存,绿色区域表示正在分配的内存。那到底什么是指针碰撞呢?
(1)按照对象的大小,b指针直接偏移到符合对象大小的地址;
(2)判断当前的b指针时候超过了c指针的地址,如果没有超过,那a就移到b的位置,对象完成分配;
- 空闲列表(适用于堆内存不完整的情况)
3、垃圾收集算法
- 标记-清除算法:内存块比较零散
(1)面向整个堆;
(2)会产生碎片; - 标记-整理算法:存活的对象往一端移动
(1)内存碎片整理算法
(2)老年代使用这个算法
(3)能合并内存,比较耗cpu
(4)合并碎片的时候,需要暂停所有线程 - 分代+复制算法:复制算法-内存55分;分代:根据对象不同的生命周期,使用不同的垃圾收集算法。解决标记-整理算法合并碎片消耗性能过高、gc停止用户线程过长的问题
补充:分代-复制算法的数据移动、指针移动的底层原理?
4、堆空间
5、让我们先搞清楚下面几个概念
- STW:(stop the world)暂停所有用户线程
垃圾回收器的三种运行模式
- 串行:用户线程STW,一个gc线程运行
- 并行:用户线程STW,多个gc线程运行
- 并发:不需要stw,用户线程、gc线程并发运行
6、现在主流的垃圾收集器都支持并发标记。什么是并发标记呢?就是标记的时候不暂停或少暂停用户线程,一起运行。那么标记是标记的什么呢,标记其实标记的是根据对象是否被访问,标记出不同的颜色,让我们一起来看下吧
- 白色:尚未访问过
- 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了
- 灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色
7、标记分为这三个阶段
-
初始标记,只标记GC Roots直接关联的对象,需要stw
-
并发标记,不需要stw,因为耗时很久,将GC Roots直接关联的对象的所有引用链全部跑一遍
-
重新标记、最终标记
8、下面让我们来看个简单的标记过程(A是GCRoots,A引用B,B引用C)
- 刚开始对象都是白色的
- 开始初始标记,首先将A标记为灰色
- 紧接着通过A找到了B,这个时候把B标记成灰色
- 因为B已经是灰色,紧接着把A标记成黑色
- 顺着B往下走,把C标记成灰色,回退到B,把B标记成黑色
- 以此类推…
- 标记阶段完成后,清除程序,会清除白色的对象
9、上面标记的过程可能会带来三个问题:多标、少标、漏标。
- 多标:当标记完B时,A对B的引用断了,这个时候B已经被标记成灰色,躲过了一次gc
- 少标:标记程序在运行的过程中,用户线程依然会创建对象,新创建的对象默认都是黑色
- 漏标:标记程序运行过程中引用链发生改变。从下面的图中可以看出来,当B标记成灰色以后,引用链发生了变化,B可能引用了其它对象或者将对C的引用赋为null,现在A又引用了当前的C,由于A已经是黑色,不会再遍历A的子对象,这种情况下,C就不会变为灰色,这次gc的时候,就会把C对象回收,这样的话,就和我们的程序逻辑不符合了。
注:漏标产生的条件:(1)灰色对象 断开了白色对象的引用;即灰色对象原来成员变量的引用发生了变化。
(2)黑色对象重新引用了 该白色对象;即黑色对象成员变量增加了新的引用。
10、如果出现漏标的话,程序就会出问题,基于上面的漏标产生的条件,我们又如下思路,其实就是,怎么做才能破坏上面的任意一个条件,就能避免出现漏标的情况
- 增量更新(incremental update):破坏上面漏标的第二个条件,让C可以被扫描到。通过写屏障将这个变动记录下来,也就是将上图总的C加入扫描的集合中,等待扫描,这样的话,C就会标记
- 原始快照(SATB):这种方式解决的是条件一,带来的结果是依然能够标记到C,具体做法如下:对象B的引用关系变动的时候,即给B对象中的某个属性赋值时,将之前的引用关系记录下来。标记的时候,扫描旧的对象图,这个旧的对象图即原始快照
11、垃圾回收器
- Serial收集器:串行垃圾收集器,即GC线程与用户线程先后运行,即GC时需要STW(暂停所有用户线程),直至GC结束才恢复用户线程的运行。专注于收集年轻代,底层是复制算法,相关参数:-XX:+UseSerialGC
- ParNew收集器:Serial收集器的多线程版本。唯一能与CMS收集器搭配使用的新生代收集器。相关参数:-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器-XX:+UseParNewGC:强制指定使用ParNew-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同
- Parallel收集器:关注吞吐量的收集器
吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
相关参数: -XX:MaxGCPauseMillis:是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。 -XX:GCTimeRatio:一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间。 -XX:+UseAdaptiveSizePolicy:一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics) - Serial Old收集器:Serial收集器的老年代版本。基于标记-整理算法实现,有两个用途:1、与Serial收集器、Parallel收集器搭配使用;2、作为CMS收集器的后备方案
- Parallel Old收集器:Parallel收集器的老年代版本。基于标记-整理算法实现。
- CMS收集器:聚焦低延迟。基于标记-清除算法实现。
由于CMS收集器是并发收集器,即在运行阶段用户线程依然在运行,会产生对象,所以CMS收集器不能等到老年代满了才触发,而是要提前触发,这个阈值是92%。这个阈值可以通过参数-XX:CMSInitiatingOccupancyFraction设置
相关参数:
-XX:+UseConcMarkSweepGC:手动开启CMS收集器
-XX:+CMSIncrementalMode:设置为增量模式
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
12、CMS收集器工作的步骤 - 初始标记:会STW。只标记GC Roots直接关联的对象。
- 并发标记:不会STW。GC线程与用户线程并发运行。会沿着GC Roots直接关联的对象链遍历整个对象图。可想而知需要的时间较长,但因为是与用户线程并发运行的,除了能感知到CPU飙升,不会出现卡顿现象
- 重新标记:会STW。CMS垃圾收集器通过写屏障+增量更新记录了并发标记阶段新建立的引用关系,重新标记就是去遍历这个记录。
- 并发清除:GC线程与用户线程并发运行,清理未被标记到的对象
默认启动的回收线程数 = (处理器核心数 + 3) / 4
显然CMS收集器依然不是完美的,不然后面就不会出现G1、ZGC等。那有哪些缺点呢?
1、运行期间会与用户线程抢夺CPU资源。当然,这是所有并发收集器的缺点
2、无法处理浮动垃圾(标记结束后创建的对象)
3、内存碎片
13、G1收集器
G1收集器与之前的所有收集器都不一样,它将堆分成了一个一个Region,这些Region用的时候才被赋予角色:Eden、from、to、humongous。一个region只能是一个角色,不存在一个region既是Eden又是from。
每个region的大小可通过参数-XX:G1HeapRegionSize设置,取值范围是2-32M。
一个对象的大小超过region的一半则被认定为大对象,会用N个连续的region来存储。
回收某个region的价值大小 = 回收获得的空间大小 + 回收所需时间
G1收集器会维护一个优先级列表,每个region按价值大小排序存放在这个优先级列表中。收集时优先收集价值更大的region,这就是G1名字的由来。
G1收集器的运行步骤
- 初始标记:会STW。
做了两件事:
1、修改TAMS的值,TAMS以上的值为新创建的对象,默认标记为存活对象,即多标
2、标记GC Roots能直接关联到的对象 - 并发标记:耗时较长。GC线程与用户线程并发运行。
从GC roots能直接关联到的对象开始遍历整个对象图 - 最终标记:遍历写屏障+SATB记录下的旧的引用对象图
- 筛选回收:更新region的统计数据,对各个region的回收价值进行计算并排序,然后根据用户设置的期望暂停时间的期望值生成回收集。
然后开始执行清除操作。将旧的region中的存活对象移动到新的Region中,清理这个旧的region。这个阶段需要STW。
相关参数:
-XX:G1HeapRegionSize:设置region的大小
-XX:MaxGCPauseMillis:设置GC回收时允许的最大停顿时间(默认200ms)
-XX:+UseG1GC:开启g1
-XX:ConcGCThreads:设置并发标记、并发整理的gc线程数
-XX:ParallelGCThreads:STW期间并行执行的gc线程数
缺点:
1、需要10%-20%的内存来存储G1收集器运行需要的数据,如不cset、rset、卡表等
2、运行期间会与用户线程抢夺CPU资源。当然,这是所有并发收集器的缺点