中国武术有句名言:“内练一口气,外练筋骨皮”,修炼内功可以让自己比变得更强!本文将全面的带领大家了解一下G1这款垃圾收集器,在正式了解G1之前,我们先来回顾一下垃圾回收的相关基础知识。

如何定位内存中不再使用的对象

各位小伙伴都知道Java是支持垃圾对象自动回收,不需要开发者手动去进行垃圾对象回收,用起来真的是舒服的不要不要的。那么问题了,Java是怎么做到识别内存中不再使用的垃圾对象呢?通常有两种方法:

  • 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值+1;当引用失效时,计数器值-1,任何时刻计数器值为0的对象就是不再被使用的对象。但是这种方法有一个缺点就是:不能解决循环引用的问题。

借助《深入理解Java虚拟机》书中的例子:

public class TestGC {public Object instance = null;private static final int _1MB = 1024 * 1024;private byte[] bigSize = new byte[2 * _1MB];public static void main(String[] args) {TestGC objA = new TestGC();TestGC objB = new TestGC();objA.instance = objB;objB.instance = objA;objA = null;objB = null;System.gc();}
}

对象objAobjB都有instance,赋值objA.instance = objBobjB.instance = objA,实际上这两个对象已经不可能再被访问,但是因为他们互相引用者对方,导致它们的引用计数都不为0,所以引用计数法无法通知GC回收它们。可以得出结论:Java虚拟机不是通过引用计数法来判断对象是否存活的。

  • 可达性分析法

在主流的语言程序中,都是通过可达性分析来判断对象是否存活的。基本思想就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则此对象是不可用的。

对象object5,object6,object7虽然互相有关联,但是它们的GC Roots是不可达的,所以基本被判断为是可回收的对象。

如何释放垃圾对象

在我们知道哪些对象需要被回收之后,接下来我们需要把这些垃圾对象进行回收,这个时候就不得不提一下垃圾回收算法了。

标记-清除(Mark-Sweep)

这是垃圾收集算法中最基础的,根据名字就可以知道,算法分为"标记""清除"两个阶段,首先标记处所有需要回收的对象,在标记完成之后统一回收被标记的对象。执行过程如图所示:

标记过程为:首先通过根节点,标记所有从根节点开始的不可达对象,这些被标记的就是未被引用的垃圾对象,然后在清除阶段清除所有被标记的对象。这种方法很简单,但是会有两个主要问题:

  1. 效率不高,标记和清除的效率都不高。
  2. 清除之后会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
标记-复制(Mark-Copying)

为了解决效率问题,复制算法可以将可用内存按容量划分相等的两部分,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上,然后把已使用的内存空间一次清理掉。这样子的做法不用考虑内存碎片的问题,但是也带来一个问题就是内存代价较高

tips:IBM公司的专门研究表明,新生代的对象98%是"照生夕死"的,所以不需要按照1:1的比例来划分内存。

于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为一块较大的Eden区(80%)和两块较小的S0(10%)和S1(10%),这样子只有10%的空间浪费。每次都会先使用Eden区和S0区,当垃圾回收时,将Eden和S0区中还存活的对象一次性复制到另外一块S1上去,然后清理掉刚才使用的内存空间。

标记-整理算法(Mark-Compact)

该算法是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了标记-复制算法的效率问题。它的不同之处就是在清除对象的时候先将可回收的对象移动到一端,然后清除掉这一端边界以外的对象,这样就不会产生内存碎片。

分代收集算法(Generational Collection)

根据对象的存活周期的不同将内存划分为几块,一般就分为新生代和老年代,根据各个年代的特点采用不同的收集算法。

  • 在新生代中每次垃圾回收都会有大批量的对象死去,只有少量存活,所以新生代通常使用"标记-复制算法",因为只要付出少量存活对象的复制成本就可以完成垃圾回收。
  • 而老年代中对象的存活率都比较高,没有额外的空间进行担保,所以通常使用"标记-整理算法"和"标记-清除算法"。

经典垃圾收集器

如果说垃圾回收算法是内存回收的理论,那么垃圾回收器就是垃圾回收算法的实践者。

上图展示了7种不同分代的收集器,如果收集器之间有连线,就证明可以搭配使用。

Serial收集器,复制算法

Serial收集器是最古老、最稳定的垃圾收集器。这个收集器是一个单线程的收集器,它会只是用一个CPU或一个线程去完成垃圾收集工作,在垃圾回收期间,会暂停其他所有的工作线程,直到垃圾回收完成。

这款收集器的缺点很明显,在垃圾回收期间会发生STW(Stop The World),也就是说在垃圾回收期间会停掉其他所有的工作线程,这一点就会造成系统停顿,也是很难让用户接受的。

虽然Serial收集器在垃圾回收过程成功会暂停其他说有的工作线程,但是Serial收集器的效率很高,尤其是对于限定单个CPU的环境来说,没有线程的开销,其实可以变得更加高效。Serial收集器是虚拟机在Client模式下的默认收集器。

ParNew收集器,复制算法

其实ParNew收集器就是Serial收集器的多线程版本,除了使用多个线程进行垃圾回收之外,其余的和Serial收集器保持一致。

Parallel Scavenge收集器,多线程复制算法

Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器。Parallel Scavenge收集器重点关注吞吐量,高吞吐量可以高效的利用CPU,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数精确控制吞吐量:

  • -XX:MaxGCPauseMillis:控制最大垃圾停顿时间,是一个大于0的毫秒数
  • -XX:GCTimeRatio:设置吞吐量的大小,是一个大于0且小于100的整数

Parallel Scavenge收集器还提供了一个参数-XX:UseAdaptiveSizePolicy。开启之后,就不需要手动设置新生代大小、晋升老年代等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,自动调节合适的停顿时间或者最大的吞吐量。自适应调节机制是ParallelScavenge收集器ParNew收集器的一个重要区别。

CMS收集器

CMS收集器是一种以获取最短回收停顿(STW)时间为目标的收集器。目前很大一部分的Java应用集中在B/S的架构上,这类应用尤其重视服务的响应速度,希望最短的停顿时间,从而给用户带来最好的体验。

  • 标记初始

标记GC Roots能直接关联到的对象,速度很快,但是仍然需要STW。

  • 并发标记

进行GC Roots Tracing的过程,整个过程耗时很长。

  • 重新标记

为了修正并发标记阶段因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个过程也会发生STW,但是一般会比初始标记阶段稍长一点,但是会比并发标记的时间短。

  • 并发清除

垃圾回收过程和用户线程是一起并发执行的。

当然CMS也是存在缺点的:

  • 比较依赖CPU
  • 无法处理浮动垃圾。由于CMS并发清理阶段用户线程还在运行,所以可能在清理之后产生新的垃圾,这些新产生的垃圾就没法回收掉它们,只好留在下一次GC时再次标记清除掉
  • 由于是基于标记-清除算法实现,所以会产生垃圾碎片

什么是G1

G1(Garbage First)是一款面向服务器的垃圾收集器,主要针对配置多核处理器以及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征,JDK 9开始默认使用G1垃圾收集器。

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
  • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

G1的特点

  • 并行与并发: G1能充分利用CPU,多核环境下的硬件优势,使用多个CPU来缩短STW时间,部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java线程继续执行。
  • 分代收集: 虽然G1可以不需要其他收集器配合就能独立管理整个堆,但还是保留了分代的概念。
  • 空间整合: 和CMS的标记-清除算法不同,G1从整体上看是标记-整理算法,但是从局部上来看是基于标记-复制的算法来实现的。
  • 可预测的停顿: 这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS的共同关注点,但是G1在追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为毫秒的时间片段 通过-XX:MaxGCPauseMillisk来指定内完成垃圾收集。

毫无疑问,可预测的停顿是G1收集器最强大的一个特点,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。

G1会根据回收价值和成本进行排序,同时把这个排序结果维护成一个优先级列表,每次根据允许的收集时间,优先选择回收价值最大的Region,比如一个Region花200ms能回收10M的垃圾,另外一个Region花50ms能回收20M的垃圾,那么在回收时间有限的情况下,优先选择后面那个Region。

这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以尽可能的提高收集效率

计算回收价值和成本的大概思路: 因为底层使用的是标记-复制算法,所以存活的对象越多,复制需要花费的时间就越长,存活的对象越短,复制需要花费的时间就越短,越有回收价值。

从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率,而不追求一次把整个Java堆全部清理干净。这样应用在分配内存的同时在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。

这种新的设计思路从工程实现上看是从G1开始兴起的,所以说G1是收集器技术发展的一个里程碑

G1中的Region

传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址是连续的。如下图所示:

G1将整个堆划分为一个个大小相等的小块(每一块称为一个Region),每一块的内存是连续的。Region的大小可以通过参数-XX:G1HeapRegionSize指定,若未指定则设置默认值:最小Region为1M、最大为32M、默认Region个数为2048个。如下图所示:

在上图中,我们注意到还有一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征:

  • H-obj直接分配到了old gen,防止了反复拷贝移动。
  • H-obj在global concurrent marking阶段的cleanup 和 full GC阶段回收。
  • 在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。

为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。

RSet和Card Table

在进行Young GC的时候,Young区的对象可能还存在Old区的引用,这就是跨代引用的问题。

为了解决Young GC的时候,扫描整个老年代,G1引入了Card Table和Remember Set的概念,基本思想就是用空间换时间。

这两个数据结构是专门用来处理Old区到Young区的引用,相关Old区的对象将被存储在这两个数据结构中,用这两个数据结构空间,避免了Yong GC扫描整个老年代,这就是用空间换时间

这两个数据结构是专门用来处理Old区到Young区的引用。Young区到Old区的引用则不需要单独处理,因为Young区中的对象本身变化比较大,没必要浪费空间去记录下来。

RSet:全称Remembered Sets, 用来记录外部指向本Region的所有引用,每个Region维护一个RSet。
Card: JVM将内存划分成了固定大小的Card。这里可以类比物理内存上page的概念。
下图展示的是RSet与Card的关系。每个Region被分成了多个Card,其中绿色部分的Card表示该Card中有对象引用了其他Card中的对象,这种引用关系用蓝色实线表示。

RSet其实是一个HashTable,Key是Region的起始地址,Value是Card Table (卡表,字节数组),字节数组下标表示Card的空间地址,当该地址空间被引用的时候会被标记为dirty_card(脏卡)。而在Yong GC的过程中,垃圾收集器只会在脏卡中扫描老年代-新生代引用。

三色标记算法

提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。首先,我们将对象分成三种类型:

  • 黑色:根对象,或者该对象与它的子对象都已经被扫描。
  • 灰色:对象本身被扫描,但还没扫描完该对象中的子对象。
  • 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象。

接下来我们借助下面的图片来了解对象的扫描过程。首先根对象被置为黑色,子对象被置为灰色:

继续由灰色遍历,将已扫描了子对象的对象置为黑色:

遍历了所有可达的对象后,所有可达的对象都变成了黑色;不可达的对象即为白色,需要被清理:

这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题。

我们看下面一种情况,当垃圾收集器扫描到下面情况时:

这时候应用程序执行了以下操作:

A.c = C
B.c = null

这样,对象的状态图变成如下情形:

这时候垃圾收集器再标记扫描的时候就会成下图这样:

很显然,此时 C 是白色的,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC 标记的对象不丢失呢?有如下两种可行的方式:

  • 在插入的时候记录对象
  • 在删除的时候记录对象

刚好这对应 CMS 和 G1 的两种不同实现方式:

CMS 采用的是增量更新(Incremental Update),只要在写屏障(Write Barrier)里发现有一个白色对象的引用被赋值到一个黑色对象的字段里,那么就把这个白色对象变成灰色的。即插入的时候记录下来。

在 G1 中,使用的是 SATB(Snapshot-At-The-Beginning)的方式,删除的时候记录所有的对象,它有3个步骤:

  • Step-1:在初始标记的时候,生成一个快照图,用于标记存活对象。
  • Step-2:在并发标记的时候,所有被改变的对象入队(在 Write Barrier 里把所有旧的引用所指向的对象都变成非白的)。
  • Step-3:可能存在游离的垃圾,将在下次被收集。

这样,G1 到现在可以知道哪些老的分区可回收的垃圾最多。当全局并发标记完成后,在某个时刻就可以触发GC。

SATB

全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
  • 灰:对象被标记了,但是它的field还没有被标记或标记完。
  • 黑:对象被标记了,且它的所有field也被标记完了。

由于并发阶段的存在,Mutator和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况,这种情况发生的前提是: * Mutator赋予一个黑对象该白对象的引用。 * Mutator删除了所有从灰对象到该白对象的直接或者间接引用。

对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误,所以SATB破坏了第二个条件。也就是说,一个对象的引用被替换时,可以通过write barrier 将旧引用记录下来。

G1中的三种GC方式

  • YoungGC

YoungGC 并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收预计需要多少,如果回收时间远远小于预期停顿时间 -XX:MaxGCPauseMillis指定,那么不会立刻触发Young GC,而是会增加年轻代的Region,继续给新对象存放,一直到下一次Eden区放满,G1计算回收Eden区花费的时间和指定的时间相近,那么会执行YoungGC。

举个例子:默认情况下年轻代占整个堆内存的5%,设定的预期停顿时间默认是200ms,当Eden区满了,G1会计算Eden区如果回收完需要花费多少时间,假设是50ms,远远小于200ms,那么G1不会立刻触发YoungGC,而是会增加年轻代的Region,比如增加个100个给新对象继续存放,然后等到这100个被塞满,再次执行YoungGC会再次判断预计花费时间,假设这时候已经接近200ms,那么G1会执行YoungGC

  • MixedGC

不是Full GC,老年代的堆占有率超过可用参数-XX:InitiatingHeapOccupancyPercent设定的值触发MixedGC,回收所有的Young和部分Old根据期望的暂停时间确定old区的优先级顺序,以及大对象区,正常情况下,G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个Region中存活的对象,拷贝到别的Region里去,拷贝过程中如果发现没有足够的空region去承载拷贝对象,那么就会发生一次Full GC,过程就如下图所示:

  • Full GC

停止系统程序,然后采用单线程进行标记、清理和压缩整理,有点类似于Serial Old,空闲出来的一批Region来供下一次Mixed GC来使用,这个过程是非常耗时的Shenandoah优化成多线程收集了。

G1最佳实践

开发人员仅仅需要声明以下参数即可:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中,-XX:+UseG1GC用于开启 G1 垃圾收集器,-Xmx32g用于设置堆内存的最大内存为 32G,-XX:MaxGCPauseMillis=200用于设置 GC 的最大暂停时间为 200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。

  • -XX:G1HeapRegionSize=n

设置 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。

  • -XX:ParallelGCThreads=n

设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。

如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。

  • -XX:ConcGCThreads=n

设置并发标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

  • -XX:InitiatingHeapOccupancyPercent=45

设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

总结

Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。怎么选择垃圾收集器?

  • 优先调整堆的大小让JVM自适应完成。
  • 如果内存小于100M,使用串行收集器
  • 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  • 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
  • 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
  • 官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

最后需要明确一个观点:

  • 没有最好的收集器,更没有万能的收集
  • 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

参考文章

  • https://tech.meituan.com/2016/09/23/g1.html
  • https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html
  • https://www.oracle.com/cn/technical-resources/articles/java/g1gc.html

G1这么强,你确定不了解一下?相关推荐

  1. 玩游戏不拖沓,续航也很强,QCY G1真无线电竞耳机体验

    现在大家没事都喜欢打一局手游,很多MOBA.吃鸡类的游戏都很流行,半小时左右的一局游戏虽然不短,但玩过之后确实也非常爽快.在玩手游的时候,耳机也是很重要的装备,不仅能够让我们获得更具沉浸感的操作体验, ...

  2. JVM学习笔记之-垃圾回收相关概念 System.gc()的理解 内存溢出与内存泄漏 STW 垃圾回收的并行与并发 安全点与安全区域 再谈引用:强引用 软引用 弱引用 虚引用 终结器引用

    System.gc()的理解 在默认情况下,通过System.gc()或者Runtime. getRuntime ( ).gc ()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试 ...

  3. 垃圾收集器Serial 、Parallel、CMS、G1

    http://blog.sina.com.cn/s/blog_3f12afd00101r8w9.html http://www.zicheng.net/article/55.htm http://si ...

  4. 【重难点】【JVM 03】CMS、G1、ZGC

    [重难点][JVM 03]CMS.G1.ZGC 文章目录 [重难点][JVM 03]CMS.G1.ZGC 一.CMS 1.介绍 2.优点 3.缺点 二.G1 1.介绍 2.优势 3.应用场景 4.Re ...

  5. JVM学习-G1回收器

    目录 1.简介 2.G1垃圾回收阶段 2.1.Young Collection 2.2.Young Collection + CM 2.3.Mixed Collection 2.4.FullGC 2. ...

  6. 《推荐系统笔记(三)》Adaboost算法 —— 弱分类器组合成强分类器的方法

    前言 我们将介绍将弱分类器组合成强分类器的算法,Adaboost算法,以及该算法有效的证明. 对于这种提升方法,我们有 每次迭代加大误分类点的权重,这样下次生成的弱分类器能够更可能将该误分类点分类正确 ...

  7. 图神经网络将成AI下一拐点!MIT斯坦福一文综述GNN到底有多强

    深度学习在图像分类,机器翻译等领域都展示了其强大的能力,但是在因果推理方面,深度学习依然是短板,图神经网络在因果推理方面有巨大的潜力,有望成为AI的下一个拐点.DeepMind 公司最近开源了其Gra ...

  8. 详解 JVM Garbage First(G1) 垃圾收集器

    前言 Garbage First(G1)是垃圾收集领域的最新成果,同时也是HotSpot在JVM上力推的垃圾收集器,并赋予取代CMS的使命.如果使用Java 8/9,那么有很大可能希望对G1收集器进行 ...

  9. 2019强网杯crypto writeup

    本次write包含以下题目 copperstudy randomstudy 强网先锋-辅助 copperstudy 题目描述 nc 119.3.245.36 12345 连上去返回 [+]proof: ...

  10. 【面试题001】最强java八股文

    一.基础篇 网络基础 TCP三次握手 1.OSI与TCP/IP 模型 2.常见网络服务分层 3.TCP与UDP区别及场景 4.TCP滑动窗口,拥塞控制 5.TCP粘包原因和解决方法 6.TCP.UDP ...

最新文章

  1. Oracle 11g 客户端使用
  2. 如何去除My97 DatePicker控件上右键弹出官网的链接 - 如何debug混淆过的代码
  3. matlab 用fplot和plot作出函数图像
  4. “由于/bin 不在PATH 环境变量中,故无法找到该命令”
  5. 不借助第三方工具查看映像路径(系统进程路径).
  6. serverlet filter
  7. 前端如何接收 websocket 发送过来的实时数据
  8. 设计模式-创建型-抽象工厂
  9. android 消息循环滚动条,Android ViewPager实现循环滚动
  10. poj 3348(求凸包面积)
  11. HTML的<span>标签【杂记】
  12. leetcode第一刷_Combinations
  13. CAN波形解析实例(1)
  14. mysql问题定位_十、MySQL的SQL优化之定位SQL的问题 - 系统的撸一遍MySQL
  15. 长安渝北工厂机器人_探秘长安UNI-T生产基地 智造工厂机器人24小时不休
  16. 【做题记录】区间排序—线段树
  17. java windows so文件_windows下编译使用NDK,调用SO文件 | 学步园
  18. 申请https协议总结
  19. 洛谷 P2525 Uim的情人节礼物·其之壱
  20. 最适合写python程序的软件

热门文章

  1. 不同编程语言语言的适用场景
  2. freebsd上运行hpool
  3. 某些型号的Comba和D-Link路由器存在管理员密码泄露漏洞
  4. java jpg转换tif_JAVA 实现jpg/tif/bmp 等图片之间格式得互相转换
  5. android 外文翻译,Android外文文献翻译.doc
  6. Git初学(5)--关联远程库
  7. chrome应用程序无法正常启动0x0000005
  8. switch语句的ns图怎么画_ns结构流程图是什么?ns流程图怎么画?
  9. 王道书 P150 T18(在中序线索二叉树里找指定节点在后序的前驱节点)+ 拓展(在中序线索二叉树里找指定节点在先序的后继节点)
  10. 从零开始学习深度学习,推荐几本书单,建议按照先后顺序排名进行学习