are you ready~~
知识点太多,肝一篇长文~,let us go ~~

先来看张图:

上图可以看到随着java版本的迭代,不断有新的垃圾收集器面世,新的GC Collector也日渐强大~~

JDK 1.3 发布串行Serial GC、ParNew(多线程的Serial)-> JDK 1.4 发布 并行Parallel Scavenge -> JDK 1.5 发布并发CMS -> JDK 1.6 发布Parallel Old ->JDK 1.7 发布G1 -> JDK 9 默认G1,标记CMS过时 - >JDK 10 增强G1->JDK 11 引入Epsilon、ZGC->JDK 12 发布 增强G1,Shenandoah GC ->JDK 13 发布 增强ZGC -> JDK 14 删除CMS,拓展ZCC

先来简单说下jdk已经提供的几种垃圾收集、如下图:

Young Generation - 年轻代:

  • Serial-串行 :

    • 单线程 - 只有一个GC线程在做GC工作
    • 串行 - STW(stop the world)-暂停所有用户线程
    • 复制算法
  • ParNew - 并行:
    • Serial 的多线程版本 - 多个GC线程并行做GC工作,缩短了GC时间
    • 其它和Serial一毛一样
  • Parallel Scavenge -并行
    • 多线程
    • STW
    • 复制算法
    • 目标是提升吞吐量(吞吐量 = t用户/(t用户 + tGC))
  • G1 - 并行 +并发

Tenured Generation - 老年代:

  • CMS-Concurrent Mark Sweep

    • 并发收集 - 用户线程与GC线程交替工作 - 可能产生浮动垃圾
    • 标记-清除算法 - 会产生内存碎片
  • Serial Old
    • Serial 的老年代版
    • 标记-整理算法
    • STW
  • Parallel Old
    • Parallel Scavange 的老年代版
    • 标记-整理算法
  • G1

note:

并行 - parallelism: 只是说多个GC线程并行进行垃圾收集动作,还是要STW,暂停所有用户线程的,如下图

  • Parallel 和ParNew 都是并行GC Collector,都会STW

  • Parallel 和ParNew 都是都是标记-整理算法,Parallel 更关注高Throughput-吞吐量,高效利用CPU,也称吞吐量优先收集器
    吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)

  • Parallel Scavenge 提供了两个参数控制吞吐量:

-XX:MaxGCPauseMillis  - GC最大暂停时间(毫秒)、JVM没有指定默认值
-XX:GCTimeRatio =99 - GC时间占比  , 只能取0到100的整数,默认99,即  1/(1+99)  =  1%
  • Parallel Scavenge 还提供了1个参数 - 很牛:
-XX:+UseAdaptiveSizePolicy  - 启动GC自适应的调节策略、jdk8默认启用
-XX:-UseAdaptiveSizePolicy - 关闭GC自适应的调节策略

自适应嘛,顾名思义,JVM自己动态调整来提供最合适的停顿时间或最大的吞吐量,
启动之后,以下参数不用自己配啦,根据运行情况动态调整。

-Xmn: - 年轻代大小、一般为1/4 (-Xmx - 最大堆内存大小)
-XX:NewRatio:2(默认)- 新生代与老年代的比例,即Young = 1/3 (Heap)、Old = 2/3 (Heap)
-XX:SurvivorRatio:8(默认) - 新生代中Eden:Survivor0:Survivor1 = 8:1:1
  • 既然Parallel Scavenge 这么厉害还要ParNew干啥呢?ParNew也很重要,它可以和CMS搭配使用,Parallel Scavenge不可以和CMS搭配使用,因为HotSpot VM 有一个分布式GC框架,Serial、Serial Old、ParNew、CMS都在这个框架内,共享了很多代码,可以任意搭配,“mix-and-match”,Parallel Scavenge 和G1采用了新框架,不兼容原本分布式GC框架。,所以ParNew+CMS 一度非常受欢迎

并发 - concurrency: GC线程可以和用户线程交替获得CPU时间片

  • CMS是G1之前真正实现并发GC的Collector,GC的同时,用户线程也在并发进行,用户无感知的情况下就完成了GC(当然它也有STW的阶段,G!的mixed GC 同CMS类似,后面详细介绍)
    宏观上来看就是这样的、如下图:

  • CMS使用的是Mark-Sweep (标记-清除)算法,会产生很多内存碎片、如下图

    7种垃圾收集器详解 - 知识补充请找这位老哥 - 传送门

CMS使老年代垃圾收集器前进了一大步,缺点也很明显,HotSpot VM 团队历时6年,终于在jdk 7 发布时、G1垃圾收集器强势来袭,赋予了取代CMS的使命

G1 - Garbage First 垃圾收集器

G1- Garbage First,见名思议,垃圾优先,先回收垃圾多的region

G1 特性 - 牛在哪里:

  • 可预测的停顿时间模型 - soft real-time (软实时) - 短暂停时间且可控
  • 高吞吐量
  • 并发+并行
  • 引入Region-分区思想 - GC以Region为单位、设计了基于部分内存回收的Young GC和Mixed GC
  • 引入Concurrence Refinement Thread - 并发优化线程

我们都知道java的 一大NB之处是它的自动内存管理,即GC,GC虽然叫Garbage Collection - 垃圾收集,它有两个职能,第一是内存的分配管理JVM-Java Virtual Machine,java虚拟机,是运行在操作系统上的虚拟计算机,只能跟OS打交道,对内存的管理也是先通过OS的系统调用申请一块内存,然后通过不同的GC算法进行管理),第二是垃圾回收,每种垃圾回收策略都和内存分配策略息息相关。

G1之前内存划分 - 分代 - 如下图 -

  • 内存划分为年轻代和老年代
  • 年轻代和老年代内存空间物理连续
  • Permanent - 永久代 在jdk 8 被干掉了
  • 年轻代收集器都采用复制算法,通过s0、s1空间复制实现

    G1将内存重新划分,引入了Region(分区)的概念、先上图:

  • G1将heap划分为2048个大小相等的Region

  • 每个Region逻辑连续,物理不连续

  • GC是以Region为单位进行的

  • Region有5种类型

    • Eden - 伊甸园,新生代的eden区
    • Survivor - 存活区,新生的s0、s1区
    • Old - 老年代
    • Humongous - 巨无霸对象区,简单说一下吧,就是一个对象大小>1/2 eden,那它就是巨大对象,直接放到H区,H区可能占几个连续的Region
    • free - 空闲分区
  • 每个Region类型不是固定的,一个Region被GC释放后就是free分区,下次被分配为Eden、Survivor、Old Region,any type is possible ~

我们逐个来看一下,大牛们是怎么想的~~
Region 类型 - 分区类型在这定义的 - heapRegionType.hpp

  // 0000 0 [ 0] Free//// 0001 0      Young Mask// 0001 0 [ 2] Eden// 0001 1 [ 3] Survivor//// 0010 0      Humongous Mask// 0010 0 [ 4] Humongous Starts// 0010 1 [ 5] Humongous Continues//// 01000 [ 8] Oldtypedef enum {FreeTag       = 0, //空闲分区YoungMask     = 2, //年轻代EdenTag       = YoungMask, //年轻代中Eden-伊甸园 区SurvTag       = YoungMask + 1,//年轻代中Survivor - 存活 区HumMask       = 4, //巨无霸对象、占一片连续RegionHumStartsTag  = HumMask, //巨无霸对象起始位置HumContTag    = HumMask + 1, //巨无霸对象连续RegionOldTag        = 8 //老年代} Tag;

还提供了一些判断方法:

public:// Queriesbool is_free() const { return get() == FreeTag; }bool is_young()    const { return (get() & YoungMask) != 0; }bool is_eden()     const { return get() == EdenTag;  }bool is_survivor() const { return get() == SurvTag;  }bool is_humongous()           const { return (get() & HumMask) != 0; }bool is_starts_humongous()    const { return get() == HumStartsTag;  }bool is_continues_humongous() const { return get() == HumContTag;    }bool is_old() const { return get() == OldTag; }

Region大小上限、下限 - heapRegionBounds.hpp

HR(Heap Region)的大小影响分配和回收的效率,HR太大回收时间长,HR太小Eden频繁占满、频繁进行分配,开销大,也不合理,所以JVM设置了上限和下限,源码如下:
panda保留了关键注释,进行了蹩脚翻译~ ,毕竟咱也六级选手呢,<{=....(嘎嘎~)

private:// Minimum region size; we won't go lower than that.//最小堆大小,我们不会再小了~static const size_t MIN_REGION_SIZE = 1024 * 1024; //Region最小1MB// reason for having an upper bound. We don't want regions to get too// large, otherwise cleanup's effectiveness would decrease as there//设置上限的原因是,我们不想把Region设置太大,因为回收效率会因此降低static const size_t MAX_REGION_SIZE = 32 * 1024 * 1024;// Region 最大 32MB// The automatic region size calculation will try to have around this//自动region大小计算会调整这个值 ,就是可变、不固定static const size_t TARGET_REGION_NUMBER = 2048; //默认2048个Region

Region大小设置 - HEAPREGION.CPP
JVM 给了Region大小范围,我们可以通过-XX:G1HeapRegionSize参数来设置每个Region的大小,如果你没设置怎么办呢,JVM根据实际情况动态决定,关键源码如下:

void HeapRegion::setup_heap_region_size(size_t initial_heap_size, size_t max_heap_size) {uintx region_size = G1HeapRegionSize;if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;region_size = MAX2(average_heap_size / HeapRegionBounds::target_number(),(uintx) HeapRegionBounds::min_size());}int region_size_log = log2_long((jlong) region_size);// Recalculate the region size to make sure it's a power of// 2. This means that region_size is the largest power of 2 that's// <= what we've calculated so far.region_size = ((uintx)1 << region_size_log);// Now make sure that we don't go over or under our limits.if (region_size < HeapRegionBounds::min_size()) {region_size = HeapRegionBounds::min_size();} else if (region_size > HeapRegionBounds::max_size()) {region_size = HeapRegionBounds::max_size();}// And recalculate the log.region_size_log = log2_long((jlong) region_size);
}

不要慌,我们逐步来看,
类名 - HeapRegion
俩参数 :

  • initial_heap_size - 初始堆大小,就是我们熟悉的Xms 设置的最小堆内存 ,我们假设给个之值2048 (2MB)
  • max_heap_size - 最大堆大小,是Xmx 设置的值,我们假设给个之值6144(大概6MB)

方法体:

  1. uintx region_size = G1HeapRegionSize; 将我们通过参数-XX:G1HeapRegionSize设置的值作为HR size
 if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;region_size = MAX2(average_heap_size / HeapRegionBounds::target_number(),(uintx) HeapRegionBounds::min_size());  }

这步是重点,标星,嘎嘎~~,我们再来拆解一下

  • if (FLAG_IS_DEFAULT(G1HeapRegionSize))是否是默认标志,就是没设置嘛
  • size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;//取我们传的这俩参数的平均值,就是说你不是设置了堆内存范围吗【2048,6144】(panda
    给的示例值啊,不是jvm的默认值,嘎嘎~),那我就当你设置了(2048+6144)/ 2 = 4096(4MB)
    这么大,既然你说随便,那我给你个平均值呗,无功无过的,嘎嘎~
  • region_size = MAX2(average_heap_size / HeapRegionBounds::target_number(), (uintx) HeapRegionBounds::min_size()); }
    MAX 就是要取最大值嘛,取谁和谁的最大值呢,
    (uintx) HeapRegionBounds::min_size()这个上面说了,jvm给的HR界限里的最小值1MB,
    average_heap_size / HeapRegionBounds::target_number()这个就是刚才算完堆大小用平均值 average_heap_size除以Region目标个数2048就是每个Region的大小啦,
    计算的大小与最小1MB取最大值,因为不能低于1MB呀~
  • 这就算完啦~~~
  1. 后面几步就是判断是否是2的次幂、是否在界限范围内

先理解到这个维度吧,逐步递进:

G1的内存分配策略就让人哇塞~~,大内存就是任性啊~,

G1 就是把堆内存给切豆腐了,不同的Region不同的策略,分而治之,一块一块的管理,

G1把内存分配策略搞介个样子,是出于GC效率考虑的,其它收集器进行GC都要整堆扫描,硬件发展这么迅速,内存已经很大了,JVM可以申请的内存也很大了,还整堆扫描,STW的时间太长了,用户忍不了啊,所以就有问题解决问题啊,切豆腐~
分区管理,每次GC部分收集(当然只有老年代是部分收集,年轻代还是all Young Region
收集的,嘎嘎~~),提高收集效率,响应用户也快,内存管理也高效,double win ~~

总体还是分代思想,分为新生代和老年代,

GC分为Young GC 和 Mixed GC ,

也是以Serial Old 的Full GC 做担保机制

行话~

先来储备几个行话~~ 行走江湖不会说几句黑化怎么行~~嘎嘎~

STW - stop the world :世界静止了。。JVM停止一切用户线程

safepoint - 安全点: JVM并不能为所欲为地STW,设计安全点,当用户线程到达安全点时,主动停止

Mutator : Java应用线程,即用户线程

RSet - Remember Set : 记忆集,记录代际间引用关系,不用头大,先混个脸熟吧,嘎嘎~
Panda 白话 - G1垃圾收集器 之 RSet(Remembed Set)源码解读 - 传送门-灰常通俗白话

Refine - 优化 : 处理RSet的优化线程
Panda白话 - G1垃圾收集器 之 Refine线程 - 传送门

Evac-Evacuation - 转移:将存活对象复制到Survivor Region或Old Region过程

Reclaim - 回收 : Region中存活对象已经完成Evac,分区可以释放了

GC Root: GC的根节点,这不用说了,GC算法可达性分析法的可达就是GC Root可达

Root Set: 根集合

FGC - Fu’ll GC : 整堆回收,串行,STW,G1用Serial Old做担保机制做Full GC

Remark - 再标记

TLAB - Thread Local Allocation Buffer:线程本地分配缓冲区

我们来认真看一下TLAB,见名思议,每个线程一个本地缓冲区,这个解释听君一席话如听一席话,嘎嘎~
情况是这样的:
JVM中堆内存是线程共享的(这都不了解的话你就完犊了~~)
从堆内存分配对象需锁整堆,为不受其它线程中断和影响
那这肯定不行啊,创建个对象堆就被锁住了,创建个对象堆就被锁住了,其它线程还怎么干活了。。
所以JVM团队就设计了TLAB,
每个线程分配一块内存(Eden区的内存),每个线程创建的对象都在自己的内存分配,且不用上锁
所有TLAB内存对所有线程都是可见的,因为堆是共享的嘛,划分几块给各个线程,那它还是共享的啊,线程共享TLAB还怎么各管各的,还无锁分配对象呢?
每个线程有个数据结构,存放TLAB可用内存的start和end地址,新对象只能在这段区间分配,就做到了无锁分配,快速分配、看图说话:
T1线程用完两块TLAB,分配第三块TLAB了,第三块用完一半了,还是top 到end那么点空间可分配
T2和T4线程同理
一个Region可以有多个TLAB,一个TLAB不能占多个Region
可以通过参数-XX:TLABWasteTargetPercent: 1 设置TLAB占Eden空间百分比,默认1%

堆分配TLAB空间时还是要上锁的(G1使用CAS并行分配),同理分配对象,堆说我管你是谁呢,我是共享的,从我这分配空间就得上锁,得保证并发情况数据一致性啊

G1 - 参数设置 :

-XX:+UseG1GC - 开启G1垃圾收集器 、JDK9之后默认开启-XX:G1HeapRegionSize   -  指定每个region的大小 ,2的n次幂,1MB - 32MB  (1MB、2MB、4MB、8MB、16MB、32MB)-XX:G1NewSizePercent - 新生代占总堆最小百分比、默认值5%-XX:G1MaxNewSizePercent - 新生代占总堆最大百分比、默认值60%-XX:MaxGCPauseMillis - GC暂停时间,默认200ms
-XX:GCTimeRatio - GC与应用耗费时间比,G1默认是9,即GC占10%,CMS默认是99,即GC耗时占1%,-XX:NewRatio - 年轻代占比,默认值 2,即1/3 年轻代,2/3 老年代

-XX:NewRatio 参数 和 -XX:G1NewSizePercent、-XX:G1MaxNewSizePercent 两个参数我们来说下吧,都是设置年轻代占比,那搞这么多干啥,当然各有各的用啦,

  • -XX:NewRatio 是固定了年轻代大小,值为2,则年轻代占总堆的1/3
  • -XX:G1NewSizePercent、-XX:G1MaxNewSizePercent 是不固定年轻代大小,指定了范围,年轻代在总堆大小的5% 到 60% 动态调整
  • 我们可以通过参数-XX:MaxGCPauseMillis设置希望GC停顿最大时间,JVM会根据停顿预测模型来动态调整新生代大小,如果只设置了-XX:NewRatio就会将动态调整大小false,可能就不能满足停顿预期时间了
  • 如何同时设置了-XX:NewRatio 参数 和 -XX:G1NewSizePercent、-XX:G1MaxNewSizePercent 两个参数,JVM则自动忽略-XX:NewRatio ,源码如下:
  if (FLAG_IS_CMDLINE(NewRatio)) {if (FLAG_IS_CMDLINE(NewSize) || FLAG_IS_CMDLINE(MaxNewSize)) {warning("-XX:NewSize and -XX:MaxNewSize override -XX:NewRatio");} else {_sizer_kind = SizerNewRatio;_adaptive_size = false;return;}}

Young GC - 年轻代垃圾收集

  • 触发时机:

    • 新的对象创建会放入Eden区
    • Eden区满、G1会根据停顿预测模型-计算当前Eden区GC大概耗时多久
    • 如果回收时间远 < -XX:MaxGCPauseMills,则分配空闲分区加入Eden 区存放新创建的o,
    • 如果回收时间接近-XX:MaxGCPauseMills,则触发一次Young GC
    • 年轻代初始占总堆5%,随着空闲分区加入而增加,最多不超过60%
  • Young GC 会回收all新生代分区 - Eden区和Survivor 区

  • Young GC 会STW(stop the world),暂停所有用户线程

  • GC后重新调整新生代Region数目,每次GC回收Region数目不固定

  • 回收过程:

    • phase one - 扫描根
    • phase two - 更新RSet
    • phase three - 处理RSet
    • phase four - 复制对象
    • phase five - 处理引用
  • 图解:

画图太慢了,借用大佬几张图这可能是最清晰易懂的 G1 GC 资料

-

  • 初始状态: D -> B 、 C -> F 、 E -> C

    • 年轻代 - Region A - 存放了对象 : A、 B、C
    • 年轻代 - Region B - 存放了对象 :E
    • 年轻代 - Region C - 存放了对象 F
    • 老年代 - Region D - 存放了对象 :D
  • 可以看到E->C, C -> F ,E、C、F都在新生代分区中,RSet不会记录他们的引用关系

  • D -> B ,老年代引用新生代,所以在B所在的Region A的RSet中会记录这个代间引用关系

  • 第一步:选择CSet(收集集合)- YGC收集所有新生代分区,根据停顿预测模型选择最大新生代分区数

  • 第二步: GCRoot Scan(根集合扫描)

    • 从GCRoot出发能直达的对象都是存活对象
    • 存活对象复制到新的Survivor分区
    • 存活对象的field入栈,留着后面深度递归用 panda.kongfu = new Kongfu(),konfu属性就是panda对象的field,引用了对象Kongfu,通过kongfu存放的值可以找到Kongfu对象所在堆内存地址
  • 示例图分析:

    • 根可达的对象有A、D、E,D对象在老年代,不在YGC范围,所以将A、E存活对象复制到新的空闲Survivor分区Region M
    • E->C ->F ,所以E的field c,C的field f 入栈
    • 对象A、E复制到了新Region M,原Region中的对象A、E的对象头有点变化
      • 对象头里面的指针指向新的对象(引用对象可以根据指针找到新对象)
      • 对象头中锁标志位会被设置为 11 - 即可以GC
  • 步骤三 : RSet Scan(扫描RSet):

    • 更新RSet,即上面讲的Refine线程处理DCQS中DCQ记录的引用关系更新RSet,使RSet为最新完整信息
    • 将RSet 作为GC Root进行根扫描,同步骤二

    • 示例图分析: Region A 中RSet记录了对象D到对象B的引用,B为直接可达对象复制到Region M,因为D在老年代,不处理,所以D的field 不入栈
  • 步骤四: Evacuation(复制):

    • 将上面步骤放入栈中的field(存放的是引用对象的地址),将引用对象复制到Region M
    • 示例图解:
    • 栈中c、f指向的对象C、F复制到Region M
    • 原对象C、F的对象头修改
  • 步骤五:重构RSet、释放空间

    • 更新RSet (Rset 记录引用对象地址和卡表信息,对象都重新分配了,它的地址,卡表肯定变了) Panda 白话 - G1垃圾收集器 之 RSet(Remembed Set)源码解读
    • 清理卡表 - 释放空间
    • 示例图解:
      • CSet中Region A、B、D被回收了
      • 回收后的Region作为Free Region ,下次被分配为哪种Region使用不确定

Mixed GC - 混合垃圾收集

未完待续。。。

Panda白话 - G1垃圾收集器相关推荐

  1. Panda白话 - G1垃圾收集器 之 Refine线程

    先上一张Panda总结图,欢迎大佬们指正: DCQ - Dirty Catd Queue DCQS - Dirty Catd Queue Set Mutator - 用户线程 RSet - Remem ...

  2. 转:深入理解Java G1垃圾收集器

    本文首先简单介绍了垃圾收集的常见方式,然后再分析了G1收集器的收集原理,相比其他垃圾收集器的优势,最后给出了一些调优实践. 一,什么是垃圾回收 首先,在了解G1之前,我们需要清楚的知道,垃圾回收是什么 ...

  3. 深入理解 Java G1 垃圾收集器--转

    原文地址:http://blog.jobbole.com/109170/?utm_source=hao.jobbole.com&utm_medium=relatedArticle 本文首先简单 ...

  4. 深入理解 Java G1 垃圾收集器

    本文首先简单介绍了垃圾收集的常见方式,然后再分析了G1收集器的收集原理,相比其他垃圾收集器的优势,最后给出了一些调优实践. 一,什么是垃圾回收 首先,在了解G1之前,我们需要清楚的知道,垃圾回收是什么 ...

  5. G1 垃圾收集器原理详解

    一.G1 垃圾收集器的开发背景: 1.CMS 垃圾收集器的缺陷: JVM 团队设计出 G1 收集器的目的就是取代 CMS 收集器,因为 CMS 收集器在很多场景下存在诸多问题,缺陷暴露无遗,具体如下: ...

  6. GC之G1垃圾收集器

    GC之G1垃圾收集器 目录 以前收集器的特点 G1是什么 G1特点 G1底层原理 G1回收步骤 和CMS相比的优势 小总结 1. 以前收集器的特点 年轻代和老年代是各自独立且连续的内存块 年轻代收集必 ...

  7. JVM性能调优实践:G1 垃圾收集器介绍篇

    前言 前面两篇主要整理了性能测试的主要观察指标信息:性能测试篇,以及JVM性能调优的工具:JVM篇.这一篇先简单总结一下GC的种类,然后侧重总结下G1(Garbage-First)垃圾收集器的分代,结 ...

  8. 深入理解 Java G1 垃圾收集器GC调优

    本文首先简单介绍了垃圾收集的常见方式,然后再分析了G1收集器的收集原理,相比其他垃圾收集器的优势,最后给出了一些调优实践. 一,什么是垃圾回收 首先,在了解G1之前,我们需要清楚的知道,垃圾回收是什么 ...

  9. G1垃圾收集器设计目标与改良手段【纯理论】

    在之前已经详细对CMS垃圾回收器进行了学习,今天准备要学习另一个全新的垃圾收集器---G1(Garbage First Collector 垃圾优先的收集器),说是一种全新的,其实G1垃圾收集器已经出 ...

  10. G1垃圾收集器深度剖析

    G1垃圾收集器深度剖析 一.G1垃圾收集器概述 1.1 思考 开始学习前,抛出两个常见面试问题:1.G1的回收原理是什么?为什么G1比传统的GC回收性能好?2.为什么G1如此完美仍然会有ZGC?简单的 ...

最新文章

  1. 直接插入排序、希尔排序
  2. Python 技术篇-如何打印一段文字,用友云霸气控制台颜文字打印
  3. 论文辅助笔记(代码实现):Bayesian Probabilistic Matrix Factorizationusing Markov Chain Monte Carlo
  4. Android复习12【广播接收者-BroadcastReceiver(简单案例-发送广播、静态注册、动态注册、本地广播、代码示例(别处登陆踢用户下线)、常用系统广播总结、音乐播放器)】
  5. 客户端页面不更新CSS样式或JS脚本的方法 (2018-08-17 17:33)
  6. [XSY] 选举(线段树优化dp)
  7. 简单介绍Java中Comparable和Comparator
  8. 对esp和ebp分析来了解函数的调用过程
  9. java编译命令带参数_java编译命令基础知识点
  10. 虚拟专题:联邦学习 | 联邦学习隐私保护研究进展
  11. C++智能指针shared_ptr使用实例
  12. Hibernate简易BaseDao演示单例
  13. Facade(装饰模式)
  14. 锂电池的充电原理以及快充原理
  15. 桌面应用程序脚本录制
  16. iframe子页面调用父页面javascript函数的方法
  17. 对话框的动画弹出和动画消隐
  18. BCV和BEV是个什么鬼?
  19. 制造业要用什么项目管理软件?
  20. 第二十章 Caché 命令大全 QUIT 命令

热门文章

  1. 怎么修改docker镜像的名字_docker镜像如何重命名
  2. 78 网站点击流数据分析案例(网站流量分析过程)
  3. m基于BP神经网络的障碍物避障和路线规划matlab仿真
  4. Qt进程-QProcess使用总结
  5. Meta-learning algorithms for Few-Shot Computer Vision论文解读(三)
  6. 一份来自28岁老程序员的自白
  7. linux修改mac地址_如何(以及为什么)在Windows,Linux和Mac上更改您的MAC地址
  8. (DINO) Emerging Properties in Self-Supervised Vision Transformers——自监督ViT的新属性和几点思考
  9. 问题解决:Ubuntu18.04显示器分辨率不正常
  10. java.lang.null_java.lang.nullpointerexception 怎么解决