当图中的数值变成0时,这个时候使用引用计数算法就可以判定它是垃圾了,但是引用计数法不能解决一个问题,就是当对象是循环引用的时候,计数器值都不为0,这个时候引用计数器无法通知GC收集器来回收他们,如下图所示:
这个时候就需要使用到我们的根可达算法
2. 根可达算法
根可达算法的意思是说从根上开始搜索,当一个程序启动后,马上需要的那些个对象就叫做根对象,所谓的根可达算法就是首先找到根对象,然后跟着这根线一直往外找到那些有用的,例如我们Java程序 main() 方法运行,一个main() 方法会启动一个线程。
线程栈变量: 线程里面会有线程栈和main栈帧,从这个main() 里面开始的这些对象都是我们的根对象。
静态变量: 一个class 它有一个静态的变量,load到内存之后马上就得对静态变量进行初始化,所以静态变量到的对象这个叫做根对象。
常量池: 如果你这个class会用到其他的class的那些个类的对象,这些就是根对象。
JNI: 如果我们调用了 C和C++ 写的那些本地方法所用到的那些个类或者对象
图中的 object5 和object6 虽然他们之间互相引用了,但是从根找不到它,所以就是垃圾,而object8没有任何引用自然而然也是垃圾,其他的Object对象都有可以从根找到的,所以是有用的,不会被垃圾回收掉。
3. 区别
| 算法 | 思想 | 优点 | 缺点 |
| — | — | — | — |
| 引用计数法 | 给对象添加一个引用计数器,每当一个地方引用这个对象的时候,计数器值就+1;当引用失效时,计数器值-1 | 判定效率高 | 不能解决对象之间相互引用的情况,开销比较大,频繁且大量的引用变化,带来大量的额外运算 |
| 可达性分析 | 通过一系列称为 “GC Roots” 的对象作为起始点,从这些节点向下搜索,当GC Roots到某个对象不可达时,这个对象就是可回收的 | 更加精确和严谨,可以分析出循环数据结构相互引用的情况 | 实现比较复杂,需要分析大量的数据,消耗大量时间 |
我们找到对应的垃圾之后,我们如果去清理垃圾呢?GC常用的算法有三种:
-
Mark-Sweep(标记清除)
-
Copying(拷贝)
-
Mark-Compact(标记压缩)
1. 标记 - 清除算法
就和它的名字一样 ,算法分为 “标记” 和 “清除” 两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,这个是最基础的收集算法,为什么说它是最基础的,因为后续的收集器都是基础这种思路并对其不足进行改进而得到的。
标记清除算法它有自己的小问题,大家可以看到上面这张图,我们从GC的根找到那些不可回收的,绿色是不可回收的,紫色是可以回收的,我们把它回收之后就变成空闲的了,这种算法相对比较简单,在存活对象比较多的情况下效率比较高,它需要经历两次扫描,第一遍扫描是找到那些有用的,第二遍扫描是把那些没用的找出来清理掉,这里会有两个问题:一个是效率问题,标记和清除两个过程的效率都不高,另一个是空间问题,标记清除之后会产生大量不连续的空间碎片,如果空间碎片太多会导致以后的程序在运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2. 复制算法
为了解决效率的问题,所以有了复制(Copying)算法的出现,它将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象赋值到另一块上面,然后再把已使用过的内存空间一次清理掉,这样使得每次都对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只需要移动堆顶的指针,这种适用于存活对象较少的情况,所以比较适合eden区,只扫描一次,效率提高了没有碎片,但是会造成空间的浪费,将内存缩小为原来的一半,未免太高了一点,而且移动复制对象,需要调整对象的引用
3. 标记 压缩算法
标记压缩就是把所有的东西整理的过程,清理的过程同时压缩到头上去。回收之前,有用的往前面走,将剩下的清理出来,但是标记压缩算法依然有它的问题,我们都是通过GC Roots 找到那些不可回收的对象,然后把不可回收的往前挪,这个时候我们需要扫描两次而且需要移动对象,第一遍扫描出有用的对象,第二遍进行移动,而且移动如果是多线程还需要进行同步,所以这个效率会低很多,但是它不会产生碎片,分配对象也不会产生内存减半。
4. 总结
-
Mark-Sweep(标记清除): 标记为垃圾之后就给清理掉,别的空间还是固定的,效率还可以,就是容易产生碎片
-
Copying(拷贝): 将内存一分为二,只使用一半,如果垃圾太多了,拷贝有用的到另外一边,剩下的清理就直接整个内存进行清理,效率比较高
-
Mark-Compact(标记压缩): 将所有的对象凑在一起,把垃圾全部清理走,接下来剩下的这个空间还是连续的,在里面分配任何内容的时候直接往里面分配就行了
JVM中的Hot Spot 用的是分代算法
新生代分为:eden、survivor
eden(伊甸): 默认比例8:是我们刚刚新 new出来对象之后往里扔的那块区域
survivor: 默认比例是1:是回收一次之后跑到这个区域,这里面由于装的对象不同,所以采取的算法也不同
由于新生代存活对象特别少,死去对象特别多所以使用的算法是 复制算法
old 老年代:tenured(终身)
老年代活着的对象特别多适用于:标记清除和标记压缩算法
一个对象产生之后首先进行栈上分配,栈上如果分配不下会进入伊甸区,伊甸区经过一次垃圾回收之后进入surivivor区,survivor区在经过一次垃圾回收之后又进入另外一个survivor,与此同时伊甸区的某些对象也跟着进入另外一个survivot,什么时候年龄够了就会进入old区,这是整个对象的一个逻辑上的移动过程。
那什么时候会在栈上分配,什么时候会在伊甸区分配?
1 栈上分配
栈上分配:
-
线程私有小对象:小对象、线程私有的
-
无逃逸:就在某一段代码中使用,除了这段代码就没有人认识它了
-
支持标量替换:意思是用普通的属性、把普通的类型代替对象就叫标量替换
栈上分配会比在堆上分配快一点,如果在栈上分配不下,会优先进行本地分配,也就是 线程本地分配TLAB(Thread local Allocation Buffer): 在伊甸区很多线程都会往里面分配对象,但是分配对象的时候我们一定会进行空间的征用,谁抢到算谁的,多线程的同步,效率就会降低,所以设计了TLAB机制
-
占用eden,默认为1%,在伊甸区取用百分之一的空间,这块空间叫做线程独有,分配对象的时候首先往线程独有的这块空间进行分配
-
多线程的时候不用竞争eden就可以申请空间,提高效率
2 老年代
对象什么时候进入老年代?
回收了多少次进入老年代?
- 超过
XX:MaxTenuringThreshold
指定次数(YGC)
-
Parallel Scavenge 15次进入老年代
-
CMS 6次进入老年代
-
G1 15次进入老年代
网上有说可以次数往上调大,这个是不可能的
动态年龄判断
为了能够适用不同程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Surivivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
两个Survivor之间拷贝来拷贝去只要超过百分之50的时候把年龄最大的直接放入到old区,也就是不一定非得到15岁。
在s1里面有这么多对象拷贝到了s2里面超过百分之50的话,s1里面在加上伊甸区里面,整个一个对象一下子拷贝到s2里面,经过一次垃圾回收,过去之后,这个时候整个加起来对象已经超过s2的一半了,这里面年龄最大的一些对象直接进入老年区,这个就叫做动态年轻判断
大对象直接进入老年代 ,所谓的大对象是指,需要连续大量内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组,经常出现大对象容易导致内存还有不少空间的时候就提前触发了垃圾收集来获得足够的连续内存空间
start 先是new一个对象,然后在栈上进行分配,如果在栈上能够分配,就分配到栈上,栈直接弹出,弹出结束,如果在栈上分配不下,判断对象是否为大对象,如果是大对象,直接进入老年代,FGC后结束如果不是,进入线程本地分配(TLAB),不管怎么样都会到伊甸区进行GC清除,如果清除完毕,直接结束,如果没有清除完毕,进入S1,S1继续GC清除,如果年龄到了进入old区,如果年龄不够进入S2,然后S2再继续GC的清除,要么年龄到了,要么动态年龄达到
MinorGC/YGC: 年轻代空间耗尽时触发
MajorGC/FullGC: 在老年代无法继续分配空间时触发,新生代老年代同时进行回收
新生代收集器: Serial、ParNew、Parallel Scavenge
老年代收集器: Serial Old、CMS、Parallel Old
新生代和老年代收集器: G1、ZGC、Shenandoah
每种垃圾回收器之间不是独立操作的,下图表示垃圾回收器之间有连线表示,可以协作使用:
1. Serial收集器
Serial 收集器是最基础、历史最悠久的收集器,是一个单线程工作的收集器,它的“单线程”的意义不是说他只会使用一个处理器或者一条收集线程去完成垃圾收集的工作,更重要的是强调在它进行垃圾收集的时候,会暂停其他所有工作线程,直到它收集结束
根据上图中我们可以知道,当Serial收集器运行的时候,会暂停所有线程,“Stop The World” ,等到GC完成后,应用线程继续执行,就类似于 你有三个女朋友,他们同时让你陪他们去逛街,你只能陪完其中一个才能去陪另外一个,陪其中一个的时候,其他女朋友就要等待,但是垃圾收集这项工作要比这种情况要复杂的多!
优势: 因为使用的是单线程的方式,所以对于单个CPU来说,是其他类型收集器中效率最高的一个
缺点: 在用户不可知、不可控的情况下,暂停所有线程,风险性和体验感不好,让人比较难接受
使用命令:可以开启Serial 作为新生代收集器
-XX:+UserSerialGC #选择Serial作为新生代垃圾收集器
2. ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集器之外,其余的比如Serial收集器可用的控制参数、收集算法、Stop The Wrold 、对象分配规则等等都和Serial收集器完全一样,在多核机器上,默认开启的手机线程数和CPU数量一样,但是可以通过参数进行修改
-XX:ParallelGCThreads #设置JVM垃圾收集的线程数
ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它 却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集 器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS 收集器配合工作。
优点:随着CPU的有效利用,对于GC时系统资源的有效利用有好处
缺点:同Serial一样的毛病
使用场景:ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为CMS只能与Serial 或者 ParNew 配合使用,在如今的多核环境下,首选的是多线程并行的ParNew,ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)
的默认新生代收集器,也可以使用-XX:+/-UseParNewGC
选项来强制指定或者禁用它
3. Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代的收集器,它同样是基于标记-复制算法那实现的收集器,也是能够并行收集器的多线程收集器,Parallel Scavenge收集器关注点与其他收集器的不用处在于,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是一个可控制的吞吐量,所谓的吞吐量就是处理器用于运行用户代码的时间与处理器总消耗的比值,如下图所示:
如果说虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那么吞吐量就是99%。停顿时间越短就越适合需要与用户交互或者需要保证服务响应质量的程序,良好的响应速度能提升用户体验。
垃圾收集器每100秒收集一次,每次停顿10秒,和垃圾收集器每50秒收集一次,每次停顿时间7秒,虽然后者停顿时间变短了,但是总体吞吐量变低了,CPU总体利用率变低了。
| 收集频率 | 每次停顿时间 | 吞吐量 |
| — | — | — |
| 100秒收集一次 | 10秒 | 91% |
| 每50秒收集一次 | 7秒 | 88% |
可以通过 -XX:MaxGCPauseMillis
来设置收集器尽可能在多长时间内完成内存回收,可以通过 -XX:GCTimeRatio
来精确控制吞吐量。
如下是 Parallel 收集器和 Parallel Old 收集器结合进行垃圾收集的示意图,在新生代,当用户线程都执行到安全点时,所有线程暂停执行,ParNew 收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行;在老年代,当用户线程都执行到安全点时,所有线程暂停执行,Parallel Old 收集器以多线程,采用标记整理算法进行垃圾收集工作。
1. Serial Old 收集器
Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法,这个收集器的主要意义也是供客户端模式下HotSpot虚拟机使用。如果在服务端一种是与Parallel Scavenge收集器搭配使用,另外一种是作为CMS 收集器发生失败时的后备预案。
Serial收集器与Serial Old收集器的运行示意图:
适用场景: Client模式;单核服务器;与Parallel Scavenge收集器搭配;作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用
2. Parallel Old收集器
Parallel Old 是 Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现,可以充分利用多核CPU的计算能力,虑Parallel Scavenge/Parallel Old收集器运行示意图:
2. CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清楚算法实现的,这个收集器的运作过程比前面的几个收集器更复杂一点,整个过程分为四个步骤:
1) 初始标记(CMS initial mark): 只是标记 GC Roots能够直接关联到的对象,速度很快
2) 并发标记(CMS concurrent mark): 从GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以和垃圾收集线程一起并发运行
3) 重新标记(CMS remark): 修正并发标记期间,因用户程序继续运作导致标记产生对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长一些
4) 并发清除(CMS concurrent sweep): 清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,这个阶段也是可以与用户线程同时并发的。
其中 初始标记、重新标记这两个步骤仍然需要 “Stop The World” 暂停所有用户线程,由于在整个过程中耗时最长的并发标记和并发清理阶段中,垃圾收集器线程都可以与用户线程一起工作,总体来说,CMS收集器的内存回收过程是和用户线程一起并发执行的,如下图所示:
优点: CMS收集器是一款优秀的收集器,它主要体现为:并发收集、低停顿。
缺点:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
还有Java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板可以领取+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书+2021年最新大厂面试题。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
还有Java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板可以领取+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书+2021年最新大厂面试题。
[外链图片转存中…(img-sC8YZIfj-1713550766991)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!