深入理解JVM 一GC(上)

对于GC我们首先会思考的问题是:
1.哪些内存要回收?哪些不用?
2.如何回收?算法
3.何时回收?触发

GC回收哪些内存?

在上一篇文章中,详细说了jvm内存的模型。
深入理解JVM 一内存

因为program counter register、stack(native method stack、VM stack)是随着jvm中线程的产生而产生,线程的湮灭而消失。这个几个区域,基本是在运行之前就已确定的,所以gc不作用于这部分内存。
gc主要作用于 Heap、Method Area ,这些区域基本都是运行时才可知,才创建的。这部分内存的分配、回收都是动态的。

GC如何回收?

很多人会说GC回收使用引用计数器算法:就是用一个计数器判断对象是否被引用,被引用一次,计数器+1,释放引用-1。当计数器为0,则回收这个对象。但是主流gc中都没有使用这个算法,主要是因为这个算法无法解决类之间相互引用的问题。比如:

public class ReferenceCountingGC {
    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[2 * _1MB];// 占用空间

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();

        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        // 假设发生GC, objA、objB是否被回收。objA、objB相互引用中。
        System.gc();

    }

    public static void main(String[] args) {
        testGC();
    }
//[GC [PSYoungGen: 6717K->680K(76288K)] 6717K->680K(249856K), 0.0023914 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//[Full GC [PSYoungGen: 680K->0K(76288K)] [ParOldGen: 0K->587K(173568K)] 680K->587K(249856K) [PSPermGen: 2569K->2568K(21504K)], 0.0316210 secs] [Times: user=0.09 sys=0.00, real=0.03 secs] 
//                Heap
//                 PSYoungGen      total 76288K, used 1966K [0x00000007ab300000, 0x00000007b0800000, 0x0000000800000000)
//                  eden space 65536K, 3% used [0x00000007ab300000,0x00000007ab4eb920,0x00000007af300000)
//                  from space 10752K, 0% used [0x00000007af300000,0x00000007af300000,0x00000007afd80000)
//                  to   space 10752K, 0% used [0x00000007afd80000,0x00000007afd80000,0x00000007b0800000)
//                 ParOldGen       total 173568K, used 587K [0x0000000701a00000, 0x000000070c380000, 0x00000007ab300000)
//                  object space 173568K, 0% used [0x0000000701a00000,0x0000000701a92d40,0x000000070c380000)
//                 PSPermGen       total 21504K, used 2575K [0x00000006fc800000, 0x00000006fdd00000, 0x0000000701a00000)
//                  object space 21504K, 11% used [0x00000006fc800000,0x00000006fca83ca8,0x00000006fdd00000)

对比删除对象实例运行后的GC日志,可以确认,上边的代码发生了GC,两个相互引用的对象,(很明显引用计数器不会为0),也被回收了。可以看出jvm并非使用简单的引用计数器回收算法。

实际上hotspotJVM使用了可达性分析算法,通过一些称为 GC Roots的对象作为起点,向下扫描,查看是否有个一条路径(称为引用链)到达某个对象,如果可以到达,则这个对象依旧存活(可达)。否则清除不可达的对象。
这里写图片描述

可以作为GC Roots 对象有哪些?
GC Roots 最为可达性搜索的起点,首先保证这些对象是要可达的。
static 全局变量、常量;
栈帧中的本地列表;
(JNI)native方法引用的对象。

http://it.deepinmind.com/gc/2014/05/13/debugging-to-understand-finalizer.html

再谈引用

简单来说,什么是引用:
一个reference类型的数据存储的是另一个对象内存地址,那么我们就称这个reference是这个对象的引用。
JDK2之后,引用类型:
**强引用:**例如:String strongReference = new String(),这个strongReference就是个强引用,只要这个强引用还存在,还指向这个对象,那么这个对象就不会被GC回收。
通常我们的引用都是强引用。
软引用: 描述有用,但是非必须的对象。在内存将要溢出时,会对所有的软引用做二次回收,如果依旧没有足够内存,抛出OOM。

SoftReference<String> softR = new SoftReference<String>(new String("soft Reference"));

弱引用: 更弱的引用,在下一次GC时被回收掉。

WeakReference<String> weakReference = new WeakReference(new String("Weak reference"));

虚引用: 对被指向的对象不构成任何影响,也无法通过虚引用获取对象。只是在虚引用指向的对象被回收时,系统会收到一个通知。PhantomReference

finalize()与FinalizerQueue

这里写图片描述

GC in Pergen

对于方法区的GC,主要说两点:
1.常量池中废弃的常量被回收;
2.无用的class 对象被卸载。对于大量使用反射、动态代理、cglib、bytecode等技术的框架中,会频繁需要卸载无用的类。

GC算法

Mark-Sweep(标记清除算法):就是用可达性分析先标记可回收的内存,然后清除掉这些被标记内存的占用。标记——>清除

缺陷:
1.效率低下,每次回收都要把整个内存空间从roots分析一遍;
2.产生内存碎片,如果大对象调用内存,无法获取连续的空间,会触发full-GC;而且要维护一份free-list,无法通过直接移动指针分配内存。

**Copying **(复制算法):将整个内存分为两块:A,B,JVM只在A上申请内存,当A需要清理时,清理A,然后把活下来的对象整齐的复制到B中,彻底清空A。

相比于Mark-Sweep算法,优势是保证了内存的连续性(A中每次都会清空,B中是整齐连续的)。另外适用于“对象朝生夕死”的新生代,只需要复制少量的存活对象,然后彻底清空,效率高。

缺点:
1.只能使用总内存的一部分
2.需要内存担保(Handler Promotion)
(如上例子中,A可以使用,B不能被直接使用),当程序中有过多大对象的使用,比如A总共大小为5M,现在有个对象10M怎么够用?这就需要“担保内存”,当自身不够时,可以直接使用“担保”(Handle Promotion)去分配内存。

另外,如果A:B=9:1,那么可以大大提高使用的内存,但是如果清楚A时意外发现100%的对象都存活,需要复制到B中,明显B内存不够,即使够用,复制100%的对象效率也是低下的。这时也同样需要“担保内存”。

其实,在hotspotJVM中,A就是eden+S0,B是S1,担保内存是oldGen。当需要将A中存活的对象复制到B上但B不够的时候,会直接使用oldGen(担保)去。

mark-sweep-compact(标记整理算法):
标记已经不存活的对象,然后让所有存活的对象移动到内存的一端,然后清理掉不存活的对象空间。标记——>移动(整理)——>清除。

解决了内存空间不连续的问题。

这里写图片描述

分代收集:
就是针对内存的不同区域,使用不同的GC算法。比如,youngGen 使用 copying算法;oldGen使用 mark-sweep 或者mark-sweep-compact.
这里写图片描述
连线的收集器都可以组合使用。

Serial收集器
-XX:+UseSerialGC
最基本的的收集器,单线程,同时收集时会暂停所有其他线程的执行(stop-the-world)。用于 youngGen。客户端默认使用。

youngGen中的Serial使用了copying收集算法。

“Serial” is a stop-the-world, copying collector which uses a single GC thread.

Parallel New收集器 (-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC默认的youngGen收集器)
仅适用于新生代。
serial的多线程版本,使用的gc算法与serial一致。但是还是会有stop-the-world。

UseParNewGC is “ParNew” + “Serial Old”
UseConcMarkSweepGC is “ParNew” + “CMS” + “Serial Old”. “CMS” is used most of the time to collect the tenured generation. “Serial Old” is used when a concurrent mode failure occurs.

Parallel Scavenge 吞吐量收集器(-XX:UseParallelGC)

“Parallel Scavenge” is a stop-the-world, copying collector which uses multiple GC threads.
parallel scavenge 类似 parallel New,同样是作用于年轻代,使用copying回收算法。但它更关注程序的吞吐量。

吞吐量 vs gc停顿
http://ifeve.com/useful-jvm-flags-part-6-throughput-collector/
JVM在专门的线程(GC threads)中执行GC。 只要GC线程是活动的,它们将与应用程序线程(application threads)争用当前可用CPU的时钟周期。 简单点来说,吞吐量是指应用程序线程用时占程序总用时的比例。例如,吞吐量99/100意味着100秒的程序执行时间应用程序线程运行了99秒, 而在这一时间段内GC线程只运行了1秒。

然而吞吐量的提高总会带来暂停时间的增长:比如我想提高系统吞吐量,那么我就不能让GC执行太过频繁,才能减少上下文切换;但是如果GC不够频繁,那么单次GC执行的时间肯定会增长。
所以,我们要根据实际情况来找准调优目标,如果是后台运算,我们追求吞吐量,让计算能力更强。如果是图像化界面,与用户交互,我们则要缩短暂停时间,以减少卡顿提高体验。

几个调优参数:
-XX:MaxGCPauseMillis=(以毫秒为单位)。 通过设置这个值,让gc尽可能的保证gc最大的 停顿不要超过这个值,非一定。

通过-XX:GCTimeRatio=我们告诉JVM吞吐量要达到的目标值。 更准确地说,-XX:GCTimeRatio=N指定目标应用程序线程的执行时间(与总的程序执行时间)达到N/(N+1)的目标比值。 例如,通过-XX:GCTimeRatio=8我们要求应用程序线程在整个执行时间中至少8/9是活动的(因此,GC线程占用其余1/9)。 基于运行时的测量,JVM将会尝试修改堆和GC设置以期达到目标吞吐量。 -XX:GCTimeRatio的默认值是99,也就是说,应用程序线程应该运行至少99%的总执行时间。
如果-XX:GCTimeRatio=与-XX:MaxGCPauseMillis=同时使用,优先达到停顿时间目标。

Serial Old收集器
Serial收集器的老年代版本

“Serial Old” is a stop-the-world, mark-sweep-compact collector that uses a single GC thread.

Parallel Old收集器(-XX:+UseParallelOldGC)
parallel scavenge的老年代版本,多线程的标记整理算法。
通常在注重吞吐量,以及cpu敏感的场合使用Parallel Scavenge
+Parallel Old的组合!

CMS(concurrent mark sweep)收集器
–XX:+UseConcMarkSweepGC
cms收集器是一种以获取最短GC Pause为目标的收集器。非常适合重视响应时间的B/S系统中。特点是:并发收集(基本与用户线程同时进行)、短停顿。
主要有四个步骤:
1.CMS initial mark 初始标记:
标记GC Roots能直接关联到的对象,Stop-the-world方式。
2.CMS concurrent mark 并发标记:
这里的"并发"指,GC 线程与用户线程同时并行执行。concurrent mark是不使用stop-the-world的方式,进行可达性分析,并标记。
3.CMS remark 重新标记:
因为并发标记过程中,用户线程也在执行,存在引用变更,也可能会产生新的垃圾,所以需要对误差重新标记。
4.CMS concurrent Sweep 并发清除。
Concurrent Sweep,不使用stop-the-world方式,与用户线程同时运行,执行清理工作,会产生内存碎片。
注意:第四步中,把清理出来的空间地址放入 free list,以便后续使用。另外,存活的对象没有移动。

由于2,4两步占整个CMS的绝大部分时间,所以,我们认为cms的gc是与用户线程同时运行的,不存在stop-the-world pause。

缺点:
1.对cpu资源敏感。它虽然不会导致用户线程卡顿,但是因为在并发标记、并发清理时(与用户线程同时)占用cpu资源(或者说占用线程),所以会导致用户应用程序变慢(上线文切换导致的),最终吞吐量下降。

在并发标记、并发清理时,默认产生的gc threads个数是 (cpu个数+3)/4,比如双核cpu启动,会启动1个gc线程,那么对cpu的占用可能会达到50%;5核心cpu,会启动2个gc线程,对cpu的占用可能达到40%;9核心cpu,启动3个gc线程,对cpu的占用可能达到33%…随着cpu核心数的增加,对cpu的负担比例会逐渐下降,但不会低于25%。

2.当CMS收集器无法处理“浮动垃圾”时,会产生Concurrent Mode Failure而失败,导致使用Serial-Old替代,重新进行gc回收。

在第四步,因为gc线程与用户线程并行执行,所以在gc线程执行清理工作的同时,用户线程的运行极可能产生其他的垃圾,这些垃圾无法在本次GC中标记、清理。我们称这些垃圾为浮动垃圾,这些浮动垃圾需要下次清理。

因为有浮动垃圾的存在,所以每次执行CMS-GC时,就需要为这些浮动垃圾预留空间,肯定不能等到old-Gen满了再清理。目前默认的CMS阈值是92%,老年代被使用92%就开始执行清理工作。

3.因为CMS基于标记-清理的算法实现的,所以会导致old-Gen内存碎片。当内存碎片过多或者CMS未完成时oldGen已经满了,则会产生concurrent mode failure 然后切换为SerialOld(mark-sweep-compact 带整理内存碎片)。

The message “concurrent mode failure” signifies that the concurrent
collection of the tenured generation did not finish before the
tenured generation became full. In other words, the new generation is
filling up too fast, it is overflowing to tenured generation but the
CMS could not clear out the tenured generation in the background.

就是年轻代晋升到老年代的对象太多,导致CMS未完成之前,老年代已经被占满了。(也就是担保内存不够了,无法存放浮动的垃圾,可以减少-XX:CMSInitiatingOccupancyFraction指标)

CMS

G1收集器:
http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

http://darktea.github.io/notes/2013/09/08/java-gc.html

https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/

另外附上一个非常有用的配置,用来显示所有的JVM 参数值(包括没有设置的):

-XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version

详见:
https://www.javaworld.com/article/2073676/hotspot-jvm-options-displayed---xx--printflagsinitial-and--xx--printflagsfinal.html

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值