参考资料:
[1]. 浅析JAVA的垃圾回收机制(GC)
[2]. JVM 七种垃圾回收器
[3]. JVM(六)为什么新生代有两个Survivor分区?
[4]. 自己画的垃圾回收框图
垃圾回收主要做两件事:
1.找出垃圾,2.回收垃圾
引用计数法(Reference Counting Collector)
引用随着引用和引用失效的进行加减,变为0的变量就是垃圾。
优点:判断效率高,不用专门停下来检测垃圾,因为引用计数法是交织在程序运行过程中的。
缺点:引用计数器增加了程序运行的开销,而且不能检测出循环引用,假设一堆垃圾互相引用形成一个圈,这样引用计数法并不能检测出来。
根搜索算法(Tracing Collector)
根集:Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。
这种算法的基本思路:
(1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
(2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
(3)重复(2)。
(4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
![]()
- GC根对象包括:
(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中的常量引用的对象;
(3)方法区中的类静态属性引用的对象;
(4)本地方法栈中JNI(Native方法)的引用对象。
(5)活跃线程。
关于标记阶段有几个关键点是值得注意的:
(1)开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便JVM可以尽情地收拾家务的这种情况又被称之为安全点(Safe Point),这会触发一次Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。
(2)暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
(3)在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:
1.如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于OC中的dealloc,Swift中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
2.如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
(4)实际上GC判断对象是否可达看的是强引用。
Tracing算法(Tracing Collector) 或 标记—清除算法
标记处垃圾,然后回收垃圾,回收的时候只是标记


-
优点
不用移动对象 -
缺点
效率低下,需要一个空闲列表,管理它的时候效率低下。
产生内存碎片
Compacting算法(Compacting Collector) 或 标记—整理算法
先标记然后将还有用的变量进行移动


-
优点
新对象分配起来简单,因为整理完内存是连续的。
没有内存碎片的问题 -
缺点
GC时间变长,移动对象耗时
Copying算法(Copying Collector)
将内存分为两块大小相同的内存,其中一块用完了,就把还存回的对象复制到另一块内存。比较适合新生代,即新对象存活数量比较少。


-
优点
标记和复制可以同时进行
分配内存简单
没有内存碎片 -
缺点
每次可用内存只剩下一半
Adaptive算法(Adaptive Collector)
在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。
Java的堆内存
Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法(4.1-4.3)进行垃圾回收(GC),以便提高回收效率。

年轻代
Eden:Survivor0:Survivor1=8:1:1
分配内存在Eden,每次gc放到Survivor1,清空其他两个,然后交换Survivor0和Survivor1指针。
每次GC对象的年龄+1,满15岁也会放到老年区。
Survivor1不足的时候把对象放到老年区。
老年代满了会触发Full GC——新生代和老年代都进行回收。
- 复制清除算法为啥需要分为三个分区?
如果只有两个分区,1:1的话分配起来内存利用率不够,8:2的话轮流起来不均匀,如果轮到2存放的新对象,很快就满了。
三个分区可以保证最大的分区Eden区每次都是空闲的,而其他两个轮流存放旧的对象,同时Eden最大是因为新的对象会有很大,存活下来会很少。
老年代
老年代的内存比例一般是新生代的2倍。
老年代的对象存活时间都比较久。
老年代满了触发full gc。
永久代
用于存放静态文件(class类、方法)和常量等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
永久代空间在Java SE8特性中已经被移除。取而代之的是元空间(MetaSpace)。因此不会再出现“java.lang.OutOfMemoryError: PermGen error”错误。
堆内存分配策略明确以下三点:
(1)对象优先在Eden分配。
(2)大对象直接进入老年代。
(3)长期存活的对象将进入老年代。
各个区回收策略
新生代——复制算法
老年代——标记—清除算法/标记—整理
新生代GC(Minor GC/Scavenge GC):发生在新生代的垃圾收集动作。因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁(不一定等Eden区满了才触发),一般回收速度也比较快。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
老年代GC(Major GC/Full GC):发生在老年代的垃圾回收动作。Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。
按执行机制划分Java有四种类型的垃圾回收器:
(1)串行垃圾回收器(Serial Garbage Collector)
(2)并行垃圾回收器(Parallel Garbage Collector)
(3)并发标记扫描垃圾回收器(CMS Garbage Collector)
(4)G1垃圾回收器(G1 Garbage Collector)

1、串行垃圾回收器
串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序(单CPU、新生代空间较小及对暂停时间要求不是非常高的应用)。是client级别默认的GC方式。
通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
2、并行垃圾回收器
并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,当执行垃圾回收的时候它也会冻结所有的应用程序线程。
适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式。可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
3、并发标记扫描垃圾回收器
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
(1)当标记的引用对象在Tenured区域;
(2)在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
-
并发标记扫描垃圾回收器 Concurrent Mark-Sweep GC
并发说的是用户程序跟GC并发。
它管理新生代的方式与Parallel收集器和Serial收集器相同,而在老年代则是尽可能得并发执行,每个垃圾收集器周期只有2次短停顿。
CMS的初衷和目的:为了消除Throught收集器和Serial收集器在Full GC周期中的长时间停顿。
初始标记(CMS initial mark):仅仅标记一下 GC Roots 能关联到的对象,速度很快。
并发标记(CMS concurrent mark):GC Roots Tracing 过程。
重新标记(CMS remark):修正并发标记期间引用变化那一部分对象
并发清除(CMS concurrent sweep)
-
优势:
并发收集、低停顿。 -
缺陷:
对 CPU 资源敏感。多线程导致占用一部分 CPU 资源而导致应用程序变慢。
无法处理浮动垃圾。并发清理过程中用户线程还在运行,会产生新的垃圾,CMS 无法在当次收集中处理它们,只好等待下一次 GC 时再清理掉。这一部分垃圾称为浮动垃圾。
CMS 采取的标记清除算法会产生大量空间碎片。往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 -
G1收集器
G1的出发点跟操作系统的内存管理的分页里面是一样的,离散地分配内存,还有连续虚拟地址和离散虚拟地址的概念。
跟CMS有点像,只是最后回收的阶段是不合用户程序并发执行的。
-
G1收集器在收集某个region的时候如何避免收集到那些被其他region引用的对象?
JVM在对Reference对象进行修改的时候,也为每个region维护一个Remembered Set,标记这个region对象被其他region引用的信息,当收集某个region的时候,GC root枚举范围加上该region的Remembered Set。
网友评论