Java GC 知识小结
Java GC相关知识小结,欢迎交流,指正错误。
1. 什么是垃圾?怎么寻找垃圾?
1.1. 什么是垃圾?
不再被引用的对象称为垃圾。
1.2 . 垃圾寻找方法(垃圾标记方法):
1.2.1. 引用计数算法(Reference Counting):
介绍:给对象添加一个引用计数器,每当一个地方引用它时,计数器加1;当引用失效时,计数器减1;计数器为0的即可被回收。
优点:实现简单,判断效率高。 缺点:存在循环引用(objA.instance = objB; objB.instance = objA)的问题,所以Java语言并没有选用引用计数法管理内存。
1.2.2. 根搜索算法(GC Root Searching)
Java和C#都是使用根搜索算法来判断对象是否存活。通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。
不存在循环引用问题。
在Java中哪些对象可以成为GC Root?
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用对象
- 本地方法栈中JNI(即Native方法)的引用对象
2. 常见的垃圾收集算法有哪些?
2.1 标记清除(Mark-sweep)
这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记那些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:
- 效率不高,标记和清除的效率都很低;
- 会产生大量不连续的内存碎片,导致以后程序在分配交大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
2.2. 复制算法(Copying)
为了解决效率问题,复制算法将可用内存按容量划分相等的两部分,然后每次只使用其中的一块,当第一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一块内存,在将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一块内存。 于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大的那份内存叫Eden区,其余两块较小的内存叫Survior区。每次都会先使用Eden区,若Eden区满了,就将对象赋值到Survivor区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制赋值到老年代中。(Java堆又分为新生代和老年代)。
2.3. 标记-整理算法(Mark-Compact)
该算法是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收的对象移动一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片。
2.4. 分代收集算法(Generational Collection)
根据对象的存活周期的不同将内存划分为几块,一般就分为新生代和老年代,根据各个年代的特点采用不同的收集算法。新生代(少量存活)用”复制算法“;老年代(对象存活率高)“标记-清除算法”。
3. JVM运行时数据区
JVM运行时数据区如下图所示:

3.1. 堆
- 堆是Java虚拟机所管理的内存最大一块。堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一的目的就是存放对象实例。所有的对象实例都在这里分配内存。 堆内存分为年轻代(Young Generation)、老年代(Old Generation),年轻代又分为Eden区和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
- Java堆是垃圾收集器管理的主要区域。
- 从内存回收的角度来看,由于现在的垃圾收集器采用的是分代收集算法。所以,Java堆又分为新生代和老年代。
- 从内存分配的角度来说,线程共享的Java堆中可能划分出多个线程私有的fenp缓冲区(Thread Local Allocation Buffer)。
- 可以通过 -Xms、-Xmx分别控制堆初始化是最小堆内存和最大堆内存大小
3.2. 虚拟机栈
- 与程序计数器一样,Java虚拟机栈也是线程私有的,他的生命周期与线程相同。
- 虚拟机栈描述的是Java方法的执行的内存模型:每个方法在执行的同时会创建一个栈桢(stack frame)用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着栈桢在虚拟机栈中入栈到出栈的过程。
- 在虚拟机规范中,对这个区域规定了两种异常情况:StackOverflowError 和 OutOfMemorryError
3.3. 本地方法栈
- 与虚拟机栈发挥的作用非常类似,他们之间的区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。与虚拟机栈一样,本地方法区栈也会抛出StackOverflowError,OutOfMemorryError异常。
3.4. 方法区(1.8后该区域被废弃)
- 方法区与Java堆一样,是各个线程所共享的,它用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
- 方法区是jvm提出的规范,而永久代就是方法区的具体实现。
- Java虚拟机对方法区的限制非常宽松,可以像堆一样不需要连续的内存可可选择的固定大小外,还可以选择不识闲垃圾收集,相对而言,垃圾收集行为在这边区域是比较少出现的。
- 在方法区会报出 永久代内存溢出的错误。而Java1.8为了解决这个问题,就提出了meta space(元空间)的概念,就是为了解决永久代内存溢出的情况,一般来说,在不指定 meta space大小的情况下,虚拟机方法区内存大小就是宿主主机的内存大小.
3.5. 程序计数器
- 程序计数器是一块较小的内存空间,他可以看做是当前线程所执行字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选择下一条将要执行的字节码指令。
- 由于JAVA虚拟机的多线程是通过多线程流转切换并分配处理器执行时间的方式来实现的。在任一一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程的计数器之间互不影响,独立存储,我们称该类内存区域为线程私有。
- 如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
3.6. 运行时常量池
- 运行时常量池是方法区的一部分。Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池。
- 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性。Java语言并不要求常量一定只有在编译器才能产生,依旧是并非预置入Class文件中的常量池的内容才能进入方法区运行时常量池。
4. Java GC的过程。

1、先判断创建的对象是否可以放入栈中,若可以的话,直接放入栈中,对象使用完后,直接从栈中pop即可,不涉及GC。 2、若不能放入栈中,判断对象所占内存是否太大,若对象太大,则直接放入堆中的老年代,最后通过FGC(Full GC)进行回收。 3、对象所占内存不算太大,经过TLAB(Thread Local Allocation Buffer)后放入Eden区。 4、在Eden区经过GC后,放入Survior1。 5、在Survior1中经历GC时,判断年龄是否太大,太大则将对象放入老年区。 6、年龄不大,则进入Survior2。 7、在Survior2中经历GC时,判断年龄是否太大,太大则将对象放入老年区。 8、年龄不大,则进入Survior1。 9、重复 5~8 。
5. 垃圾回收所有算法。
除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型 G1是逻辑分代,物理不分代 除此之外不仅逻辑分代,而且物理分代

上图是10种常见的垃圾回收算法。画弧线部分表示可以组合使用,图中黄线部分是最常见的3种组合。(STW : Stop The World 非常严重的问题)
5.1. Serial。
在GC时,由单线程进行“清理”,其他线程(业务线程等)全部停止(STW)。

5.2. Parallel Scaven。
在GC时由多线程进行“清理”, 其他线程(业务线程等)全部停止(STW)。

5.3. Serial Old。
用于老年代收集垃圾的单线程GC。

5.4. ParNew。
与Parallel Scaven类似,但它可以与CMS配合使用。
