概念

  1. 图中展示了7种不同分代的收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
  2. 而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:
    1. 新生代收集器:Serial、ParNew、Parallel Scavenge;
    2. 老年代收集器:Serial Old、Parallel Old、CMS;
    3. 整堆收集器:G1;
  3. 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  4. 其中Serial Old作为CMS出现“Concurrent Mode Failure”失败的后备预案。

并发和并行收集器:

  1. 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态; 如ParNew、Parallel Scavenge、Parallel Old;
  2. 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行); 用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如CMS、G1(也有并行);

Minor and Full GC:

  1. Minor GC:又称新生代GC,指发生在新生代的垃圾收集动作;因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
  2. Full GC:又称Major GC或老年代GC,指发生在老年代的GC;出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);Major GC速度一般比Minor GC慢10倍以上;

GC详细分类:

  1. 针对HotSpot VM的实现,它里面的GC其实准确分类只有两大类(Partial GC、Full GC)
  2. Partial GC:并不收集整个GC堆的模式
    1. Young GC:只收集young gen的GC
    2. Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式Mixed
    3. GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  3. Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式

Major GC通常是跟full GC是等价的,收集整个GC堆。

GC触发时机:

  1. 最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:

    1. Young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen, 所以young GC后old gen的占用量通常会有所升高。
    2. Full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen 剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump的GC,默认也是触发full GC。
  2. HotSpot VM里其它非并发GC的触发条件复杂一些,不过大致的原理与上面说的其实一样。当然也总有例外。Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。控制这个行为的VM参数是-XX:+ScavengeBeforeFullGC。
  3. 并发GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集。

safepoint(安全点)

  1. 什么是安全点:安全点是在程序执行期间的所有GC Root已知并且所有堆对象的内容一致的点。从全局的角度来看,所有线程必须在GC运行之前在安全点阻塞。也就是说当线程运行到安全点时,堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等。
  2. 安全点位置:
    1. safepoint:“A point in program where the state of execution is known by the VM”。不同JVM实现会选用不同的位置放置安全点。
    2. 以HotSpot VM为例,在解释器里每条字节码的边界都可以是一个安全点,因为HotSpot的解释器总是能很容易的找出完整的“state of execution”。
    3. 而在JIT编译的代码里,HotSpot会在所有方法的临返回之前,以及所有非counted loop的循环的回跳之前放置安全点。

Serial收集器:

  1. 特点:针对新生代;采用复制算法;单线程收集; “Stop The World”,即进行垃圾收集时,必须暂停所有工作线程,直到完成;
  2. 应用场景:
    1. 依然是HotSpot在Client模式下默认的新生代收集器;
    2. 优势:简单高效(与其他收集器的单线程相比); 对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
    3. 在用户的桌面应用场景中,可用内存一般不大(< 200M),可以在较短时间内完成垃圾收集,只要不频繁发生,这是可以接受的。
  3. 设置参数:“-XX:+UseSerialGC”:添加该参数来显式的使用串行垃圾收集器。

ParNew收集器:

  1. 特点:ParNew垃圾收集器是Serial收集器的多线程版本。除了多线程外,其余的行为、特点和Serial收集器一样,如:Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等;
  2. 应用场景:在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作。但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
  3. 设置参数:
    1. “-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;
    2. “-XX:+UseParNewGC”:强制指定使用ParNew;
    3. “-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同。
  4. 为什么只有Serial,ParNew能与CMS收集器配合?
    1. CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作。
    2. 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码。

Parallel Scavenge收集器-吞吐量(吞吐量收集器(Throughput Collector)):

  1. 吞吐量(Throughput):CPU用于运行用户代码的时间与CPU总消耗时间的比值;

    1. 即:吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间); 高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;
  2. 垃圾收集器期望的目标(关注点):
    1. 停顿时间:停顿时间越短就适合需要与用户交互的程序;良好的响应速度能提升用户体验;
    2. 吞吐量:高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;主要适合在后台计算而不需要太多交互的任务;
    3. 覆盖区(Footprint):在达到前面两个目标的情况下,尽量减少堆的内存空间;可以获得更好的空间局部性;
  3. 特点:有一些特点与ParNew收集器相似:新生代收集器;采用复制算法;多线程收集;
    1. 主要特点:它的关注点与其他收集器不同。CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间; 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
  4. 应用场景:高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
    1. 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互; 例如:那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
  5. GC参数设置:
    1. “-XX:MaxGCPauseMillis“:控制最大垃圾收集停顿时间,大于0的毫秒数;MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁;
    2. “-XX:GCTimeRatio”:设置垃圾收集时间占总时间的比率,0<n<100的整数; GCTimeRatio相当于设置吞吐量大小;垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n);例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%==1/(1+19);默认值是1%==1/(1+99),即:n=99。
    3. “-XX:+UseAdptiveSizePolicy”:开启这个参数后,就不用手工指定一些细节参数,如:新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(XX:PretenureSizeThreshold)等;JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量, 这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);
    4. 一种值得推荐的方式:
      1. 只需设置好内存数据大小(如:"-Xmx"设置最大堆);然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;那些具体细节参数的调节就由JVM自适应完成。
      2. 这也是Parallel Scavenge收集器与ParNew收集器一个重要区别。

老年代收集器-Serial-old

  1. 特点:针对老年代;采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);单线程收集;
  2. 应用场景:主要用于Client模式;而在Server模式有两大用途:
    1. 在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
    2. 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

Parallel Old收集器:Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本

  1. 特点:针对老年代;采用"标记-整理"算法; 多线程收集;
  2. 应用场景:JDK1.6及之后用来代替老年代的Serial Old收集器; 特别是在Server模式,多CPU的情况下;这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;
  3. 设置参数:"-XX:+UseParallelOldGC":指定使用Parallel Old收集器;

CMS收集器:并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;

  1. 特点:针对老年代;基于"标记-清除"算法(不进行压缩操作,产生内存碎片); 以获取最短回收停顿时间为目标;并发收集、低停顿;需要更多的内存(看后面的缺点);是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
  2. 应用场景:与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度; 以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用;
  3. 设置参数:"-XX:+UseConcMarkSweepGC":指定使用CMS收集器;
  4. CMS收集器运作过程
    1. 初始标记(CMS initial mark):仅标记一下GC Roots能直接关联到的对象; 速度很快;但需要"Stop The World";
    2. 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;应用程序也在运行;
    3. 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率;
    4. 并发清除(CMS concurrent sweep:回收所有的垃圾对象;整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作; 所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
  5. 明显的缺点
    1. 对CPU资源非常敏感:并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
    2. 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
      1. *浮动垃圾(Floating Garbage):*在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集; 也要可以认为CMS所需要的空间比其他垃圾收集器大;"-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间;
      2. "Concurrent Mode Failure"失败:如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败; 这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
    3. 产生大量内存碎片:由于CMS基于"标记-清除"算法,清除后不进行压缩操作;产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。解决方法:
      1. “-XX:+UseCMSCompactAtFullCollection”:使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程; 但合并整理过程无法并发,停顿时间会变长;默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
      2. “-XX:+CMSFullGCsBeforeCompaction”: 设置执行多少次不压缩的Full GC后,来一次压缩整理; 为减少合并整理过程的停顿时间;默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
  6. 由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大; 总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间; 但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;

G1收集器:JDK7-u4才推出商用的收集器

  1. 特点:

    1. 并行与并发:能充分利用多CPU、多核环境下的硬件优势; 可以并行来缩短"Stop The World"停顿时间; 也可以并发让垃圾收集与用户程序同时进行;
    2. 分代收集,收集范围包括新生代和老年代: 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配; 能够采用不同方式处理不同时期的对象;虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region);新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
    3. 结合多种垃圾收集算法,空间整合,不产生碎片从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法; 这是一种类似火车算法的实现;都不会产生内存碎片,有利于长时间运行;
    4. 可预测的停顿:低停顿的同时实现高吞吐量,G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;
  2. 应用场景: 面向服务端应用,针对具有大内存、多处理器的机器;最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案; 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:
    1. 超过50%的Java堆被活动数据占用;
    2. 对象分配频率或年代提升频率变化很大;
    3. GC停顿时间过长(长于0.5至1秒)。
  3. 设置参数
    1. “-XX:+UseG1GC”:指定使用G1收集器;
    2. “-XX:InitiatingHeapOccupancyPercent”:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
    3. " -XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;
    4. “-XX:G1HeapRegionSize”:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
  4. 为什么G1收集器可以实现可预测的停顿:
    1. 可以有计划地避免在Java堆的进行全区域的垃圾收集;
    2. G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
    3. 每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
    4. 这就保证了在有限的时间内可以获取尽可能高的收集效率;
  5. G1收集器运作过程:
    1. 初始标记(Initial Marking): 仅标记一下GC Roots能直接关联到的对象;且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;需要"Stop The World",但速度很快;
    2. 并发标记(Concurrent Marking): 进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;耗时较长,但应用程序也在运行;并不能保证可以标记出所有的存活对象;
    3. 最终标记(Final Marking): 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;上一阶段对象的变化记录在线程的Remembered Set Log;这里把Remembered Set Log合并到Remembered Set中;需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率;
    4. 筛选回收(Live Data Counting and Evacuation): 首先排序各个Region的回收价值和成本;然后根据用户期望的GC停顿时间来制定回收计划; 最后按计划回收一些价值高的Region中垃圾对象;回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量;

JVM常用启动参数:

  1. -Xmx1024m,-Xmx1g 单位(g,m,k)
  2. -Xms512m
  3. -Xmn256m(相当于将新生代的初始、最小、最大值设置为同一个:-XX:NewSize = -XX:MaxNewSize)
  4. -Xss512k
  5. -XX:PretenureSizeThreshold,大于这个数量直接在老年代分配,缺省为0 表示不会直接分配在老年代;(注:如果在新生代分配失败且对象是一个不含任何对象引用的大数组,也可被直接分配到老年代。)
  6. -XX:-DisableExplicitGC,禁用显示GC,System.gc()
  7. -XX:+PrintGCDetails,打印GC详情
  8. -XX:+PrintGCTimeStamps : JVM启动到GC开始经历的时间
  9. -XX:+PrintGCDateStamps : GC发生的具体时间点
  10. -XX:+PrintCommandLineFlags: 让JVM打印出那些已经被用户或者JVM设置过的详细的XX参数的名称和值。
  11. -XX:NewRatio=2,老年代和新生代的内存比例为2:1
  12. -XX:SurvivorRatio=8,表示eden和1个survivor区的比例,survivor大小 = Xmn/(SurvivorRatio+2)
  13. -Dxxx=yyy,启动时配置系统属性,在java中通过System.getProperty(“xxx”)获取相应的值

jvm垃圾回收之垃圾收集器相关推荐

  1. JVM垃圾回收——G1垃圾收集器

    目录 一.什么是G1垃圾收集器 二.G1垃圾收集器的内存划分 三.G1垃圾收集器的收集过程 四.G1收集器的优缺点 五.G1收集器的JVM参数配置 一.什么是G1垃圾收集器 Garbage First ...

  2. JVM垃圾回收——ZGC垃圾收集器

    目录 一.什么是ZGC垃圾收集器 二.ZGC的内存模型 三.收集过程 染色指针 多重映射 收集过程 四.优缺点 五.参数配置 一.什么是ZGC垃圾收集器 ZGC(Z Garbage Collector ...

  3. JVM面试(四)-垃圾回收、垃圾收集器、GC日志

    垃圾回收.垃圾收集器.GC日志 什么是垃圾?(垃圾的概念) 什么是垃圾回收?(垃圾回收的概念) 为什么要垃圾回收?(垃圾回收的原因) 如何定义垃圾? 引用计数算法 什么是循环引用 可达性分析算法 哪些 ...

  4. JVM(3)之垃圾回收(GC垃圾收集器+垃圾回收算法+安全点+记忆集与卡表+并发可达性分析......)

    <深入理解java虚拟机>+宋红康老师+阳哥大厂面试题2总结整理 一.堆的结构组成 堆位于运行时数据区中是线程共享的.一个进程对应一个jvm实例.一个jvm实例对应一个运行时数据区.一个运 ...

  5. JVM垃圾回收器-G1垃圾收集器

    Java8的G1垃圾回收器官方文档参考:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#ga ...

  6. 深入理解java虚拟机(五)GC垃圾回收-经典垃圾收集器

    文章目录 前言 一.Serial收集器(标记-复制算法) 二.ParNew收集器(标记-复制算法) 三.Parallel Scavenge收集器(标记-复制算法) 四.Serial Old收集器(标记 ...

  7. jvm垃圾回收——诊断垃圾收集问题

    http://www.oracle.com/technetwork/java/example-141412.html 转载请注明出处,多谢. 这个附录包含说明垃圾收集问题的日志的例子.如不指定,使用默 ...

  8. 深入理解Java虚拟机——JVM垃圾回收机制和垃圾收集器详解

    一:概述 说起垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它和Java联系起来.在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,顾名思义,垃圾回收就是释 ...

  9. 看完这篇 JVM 垃圾回收,和面试官扯皮没问题了

    本文来源:码海 前言 Java 相比 C/C++ 最显著的特点便是引入了自动垃圾回收 (下文统一用 GC 指代自动垃圾回收),它解决了 C/C++ 最令人头疼的内存管理问题,让程序员专注于程序本身,不 ...

最新文章

  1. 鱼眼相机标定_鱼眼相机模型(二)
  2. 8086汇编复习3 - 标志寄存器 - 使用emu8086
  3. Three.js中使用材质覆盖属性
  4. Leetcode--75. 颜色分类
  5. 【连载】Django入门到实战(一)
  6. 万字长文!剖析美团外卖持续交付的前世今生
  7. 欧姆龙cp1h指令讲解_欧姆龙CP1H-XA40DT-D手册CP1H-XA40DT-D参考手册通信指令 - 广州凌控...
  8. 小程序Table样式
  9. CactiEZ中文版的安装使用方法
  10. Matlab中FracLab计算分形维数方法
  11. Windows禁止某个软件联网保姆级教程
  12. 音乐计算机在哪里买,[计算器自带音乐在哪里]语音计算器上的音乐有什
  13. 数控编程也是c语言吗,学数控编程需要使用个人电脑吗
  14. 小米tts语音引擎下载_在手机和 AIoT 双战场打拼的小爱同学,会把语音助手带向何方?...
  15. 同一个机器 安装多个版本Chrome浏览器的方法
  16. 如何理解DevOps
  17. http authorization 基本认证
  18. 黑马程序员_毕向东_JavaScript视频教程--学习笔记
  19. 项目进度跟踪的最佳实践:每日站立会议
  20. matlab绘制风场图(矢量图、箭头图)

热门文章

  1. 现代软件工程讲义 2 工程师的能力评估和发展
  2. node-red端口争用问题
  3. Ubuntu20.4下打开WPS提示缺失字体“Symbol、Wingdings、Wingdings 2...“的解决办法
  4. 用计算机弹钢琴谱,flash用键盘弹钢琴,电脑钢琴谱-走啦网
  5. 对幕布的几条改进建议
  6. linux命令part,技术|十个鲜为人知的 Linux 命令-Part 3
  7. ipad发布会ipad_如何在iPad上调试网站
  8. 连续被爆押金退款难  共享汽车会否走向和共享单车一样的宿命?
  9. python 如何安装软件包故障_安装 Python 软件包遇错误,怎么办?
  10. modern cmake的概念剖析