目录
一、什么是垃圾
没有任何引用指向的一个或多个对象;多个对象是指循环引用的对象。例如在内存中A引用B,B引用C,C引用A,三者虽然在相互引用,但是没有栈中的引用指向他们,所以也是垃圾。

二、JVM如何定位垃圾
两种算法
- .引用计数算法(reference count)
在每个对象当中添加一个int变量count,每有一个引用指向该对象,那么count+1,如果为0则为垃圾。
但是这种方式无法处理循环引用垃圾。因为他们相互引用,count永远不可能为0。所以JVM不使用这种算法。
(2).根可达算法/根搜索算法(Root Searching)
涉及到一个面试经典问题:什么是根对象?
根对象:说白了就是系统一定会使用的对象。一定是根对象的对象(来源于JVM规范):
- 线程栈中的对象(JVM Stack):就是我们在栈中new出来的对象
- 本地方法栈中的对象(native method stack)
- 常量池中的对象(runtime constant pool)
- 静态对象(static reference in method area)
- 从Class类中load到内存的对象(Clazz)
- 以上五种对象中成员对象引用的对象
除了这6种,其他的对象都算垃圾。

右下角的两个为垃圾
三、常见的垃圾回收算法
(1)标记清除算法(Mark-Sweep)
在内存中找到垃圾并添加标记,GC直接将其释放。

存在问题:位置不连续,会产生碎片。
(2)拷贝算法(Copying)
将内存划分为两半,有一半不存放数据,另一半存放数据。当进行内存回收时,将使用的一半内存中的需要保留的数据拷贝到另一份内存中,同时释放所有刚才存放了数据的内存空间。

不会产生碎片,而且空间连续,对大进程友好,只是线性地址的一个拷贝的过程,所以效率非常高。但是严重浪费空间,10G的内存只能用5G
(3)标记压缩算法(Mark-Compact)
将空间上的零散的垃圾区域做标记,把后面不是垃圾的区域直接拷贝过来。这样,既释放了内存,又整理了内存空间,形成了一大片的可用区域,方便对后续的划分,由于操作时内存移动,多线程需要线程同步,效率较低。而使用单线程的话,效率本来就很低。

最好的算法,也是JVM在使用的。
四、JVM分代算法
部分垃圾回收器使用的模型。
一般分为:(1)新生代(new) (2)老年代(old) (3)永久代(jdk1.7)/元数据区(Meta Space jdk1.8)
永久代和元数据区都是装Class的,就是我们new出来的Class;永久代必须指定大小限制,在使用动态代理的时候会出现内存溢出。而元数据区可以设置也可以不设置,也就是可以无限大。直到物理内存溢出。

新生代(new)的划分:
新生代被划分为三块,eden区和两个survivor区(后面用s0 s1
来简称这两个区)。划分比例为8:1:1垃圾回收算法为拷贝算法。垃圾回收被称为YGC。YGC效率很高。
新生代的运行机制:
- 当我们new出一个对象之后,首先,会在栈上分配,如果栈上空间足够,则对象会直接待在栈中。如果栈空间不足,会进入eden区,在eden区中有一个TLAB(TLAB在总结中详解)的分配。JVM确信:在第一轮循环之后,这些new出的新对象90%都会被回收。
- 所以YGC会来到eden区进行回收,并把有用的对象放进以高速的拷贝算法放进s0。eden区被清空。
- 第二轮循环eden又会收纳很多新对象,之后继续让YGC进行定位回收。同时s0区中的对象也会被YGC检索。回收掉s0区中的垃圾。之后将eden区和s0区的所有有用的对象再次以高速的拷贝算法放进s1区。清空eden和s0.
- 之后的循环以此类推重复这些操作。当出现eden区的对象被装进s区的时候,s区已经满了,那么这些多出的对象会被直接装进老年代。对于s区中的对象,如果存活年龄已经达标,则进入老年代。具体达标年龄因不同的GC而异,例如ps+parallelOld是15,CMS是6,G1是15,ZGC没有,因为ZGC不分代。
老年代的划分:
老年代用于存放刚才还经常使用的对象,内部不细分,并且老年代一般不进行垃圾回收,垃圾回收算法为标记压缩算法和标记清除算法混合。没有特定垃圾回收名称,但作为区分我们称为OGC。新生代与老年代的划分比例为1:3
老年代的执行:
老年代不存放新对象,只存放从新生代中达标过来的对象。当老年代满了之后,会进行FGC而不是单纯的OGC。
FGC:Full GC
同时对新生代和老年代进行垃圾回收,内部涉及到压缩以及标记等复杂的工序,再加上整个内存都要被清理,所以效率极其低下。会导致明显的停顿。对于FGC要让它尽量少发生。这也是GC调优(JVM Tuning)
对于一些概念的澄清:MinorGC = YGC MajorGC = FGC
五、垃圾回收器
直到jdk14为止,JVM一共产生了十种垃圾回收器。
对于以上十种GC,直接背过。现在线上编程一般都用左半边的六种。也是需要重点掌握的。
左边六个为分代模型。GC从分代算法演化到了不分带算法。其中的连线表示这些GC可以相互配合使用。
GC浅谈
主要用于新生代的:
- Serial:Java应用程序的其他所有线程都被挂起
(Stop-The-World,简称STW),也就是程序停止运行,用单线程来进行垃圾回收。适用于几十MB的程序
- PS(Parallel Scavenge):停止运行的程序,用多线程进行
回收。适用于几GB的程序
- ParNew:原理与PS相同,但它可以配合CMS的多线程
回收。
主要用于老年代的:
- SerialOld:就是把Serial放到老年代里,原理与Serial相同。
- ParallelOld:把PS放进老年代里,原理与PS相同。
- CMS(ConcurrentMarkSweep):一个极其复杂的GC,但是做到了高效。程序运行和垃圾回收是并发的,也就是同时运行,降低了STW的时间(大概200ms)。对于前五种,直接一两个小时程序一动不动都有可能。虽然高效,但是内部问题也很多也极其复杂。适用于几十GB的程序
不分新生代和老年代都可以使用的,并且都继于CMS后诞生:
- G1:一个极其复杂的GC,STW甚至可以降低至10ms,适用于100GB以上的程序
- ZGC:一个极其复杂的GC,STW甚至可以降至1ms,也是jdk更倾向于下一步所使用的,目前还在测试完善阶段。
- Shenandoah(shan nen dou wa):与ZGC相同。适用1T以上的程序。
- Eplison:JDK做Debug的时候用的,实际上是一个空的GC,主要作用是调试JDK,看看程序的垃圾产生的过程;或者程序根本不需要使用GC。则使用Eplison。所以一般也不用。
GC组合:
在最早的时候 GC组合为:Serial+SerialOld
而在GC显著变化的一个时期JDK1.8:PS+ParallelOld
正由于1.8默认回收器如此,所以我们对于GC调优的主要对象为Serial、SerialOld、PS、ParallelOld。
六、JVM调优第一步,了解生产环境下的垃圾回收器组合
涉及到JVM命令行。
JVM参数分类
- 标准参数 -开头,所有的HotSpot(HotSpot是JDK的一种,JDK分好多种类)都支持
eg: java -version获取jdk等版本信息,这就是一条标准命令
2. 非标准参数 -X开头,特定版本的HotSpot支持特定命令
eg: java -X 获取所有非标准参数,这是一条非标准命令
3. 不稳定参数 -XX开头,下个版本有可能取消。
一些比较重要的命令
- java -XX:+PrintCommandLineFlags 获取程序运行时所有生效的命令行参数,这句话很关键,用它可以观察生产环境中的环境配置,比如GC用了什么。
![]()
如上图片中所展示的就是环境配置。参数解释:
起始堆大小、最大堆大小、压缩指针、对对象进行压缩、默认GC为PrallelGC(PS+PrallelOld)
- java -XX:+PrintFlagsInitial 获取所有参数的默认值
- java -XX:+PrintFlagsFinal 获取所有参数现在的设置值(最终生效的值)
- Java -XX:+PrintFlagsWithComments 获取所有-XX参数以及其解释,但只能在Debug环境下使用。所以我们一般采用2来获取所有的-XX参数
七、总结及补充
TLAB详解
TLAB(Thread Local Allocation Buffer) 线程本地分配缓冲区
在Eden中,每个分配线程来了之后,都会争着要为自己的对象分配空间,如果不进行线程同步,就会出现多个线程争抢一个eden地址的问题。但是线程同步又极其低效。所以引入了TLAB。
TLAB是eden区的一部分,它本身属于eden区。它的责任在于为每个线程划分一份极小的特有的空间。每个线程进来之后会先去它专属的那个TLAB中,由于空间是特有的,所以不会出现线程同步问题。从而提高了效率。由于TLAB空间极小,面对稍大一点的对象还是在eden上直接存放。但是TLAB只是一个缓冲作用,实际上所有的对象都还是被分配在了eden区中。
CMS详解
CMS工作流程图

CMS工作流程
- 当程序在运行的过程中,CMS首先会做一次初始标记,期间会造成STW,但是这个时间极短。CMS会拿一个单线程去找到所有的根引用。也就是标记所有根对象。所以会非常快。
- 并发标记:拿到根对象之后,CMS会顺着根对象中引用的对象去找其他对象,这个过程是与程序并发进行的。程序一边运行,CMS一边去找其他对象,是垃圾就初次标记。
- 正是因为做垃圾初次标记时,程序也在同时运行。所以,很容易出现两种情况:(1)CMS刚刚将一个对象标记为垃圾,下一秒,程序就把这个被标记的对象拿来引用了。该问题被称为标记失误。(2)CMS刚刚看这个对象不是垃圾,下一秒,这个对象的引用被程序释放了。产生浮动垃圾。浮动垃圾是比较小的问题,只要CMS再次进行垃圾清理的时候,浮动垃圾也难逃被清理的命运。但是标记失误这个问题绝对不能发生。假设程序正在运行,由于标记失误,一个正在被引用的对象突然在运行过程中被GC回收了。程序会出现严重的问题。所以为了避免标记失误,CMS采用了重新标记。
- 重新标记:CMS会在并发标记对垃圾进行初次标记之后,让程序STW,之后对所有刚才标记的对象进行二次确认。如果它还是垃圾,则再次标记,该对象正式成为垃圾。如果发现它被引用了,则撤销标记。该过程仅对被清理单位进行标记而不是清理,所以效率也很高。
- 并发清理:CMS对标记单位的操作完成之后结束STW,采用多线程对刚才所标记的垃圾进行清理。此时程序和CMS并发进行。
注:
CMS在对垃圾初次标记时,为了不影响程序正常运行,被标记的对象仍然可以被正常引用。而被二次标记的对象则无法再次被引用。这是CMS为了保护程序所采用的二次标记方案。
对于CMS还有一个致命的问题:
从GC板块的图片可以看出,CMS除了和官宣的ParNew可以配合使用以外,它还可以和Serial相配合使用。所以在CMS中如果稍不注意设置。CMS很有可能会在老年代中使用SerialOld进行FGC。SerialOld早就被淘汰的GC用在几十GB的内存中做清理工作。芜湖~~起飞~~这效率可太酸爽了。
G1详解
G1(Garbage First) 垃圾优先
G1在逻辑上分代,但是在内存上不分代,而且首次开创了分区技术,将整个内存分为一个一个的小块。每一块中存放的对象可能是old区的,也可能是eden区的,也可能是s区的。只是在逻辑上为他们定义了分代。每次清理的时候,不对整个内存清理,优先清理垃圾最多的小块。

面试经典问题:
- G1中是否存在STW:看似没有,但是存在。
- G1中是否有FGC:看似没有,但是存在。
ZGC和Shenadoah
两者逻辑差不多,都起源于一个叫C4的GC。ZGC是Oracle制作的,Shenadoah不是。所以在未来JDK更倾向于使用ZGC。而且调优也变得极为简单,只有几个参数,所有垃圾回收全部由C4智能管理。
本文详细介绍了Java垃圾回收的基本概念,包括什么是垃圾、JVM如何定位垃圾,以及常用的垃圾回收算法。重点讲解了JVM的分代算法,新生代和老年代的管理,并探讨了各种垃圾回收器,如Serial、ParallelScavenge、CMS和G1。此外,还讨论了JVM调优的第一步,即理解生产环境下的垃圾回收器组合,并提供了JVM命令行参数的相关知识。

被折叠的 条评论
为什么被折叠?



