导语
  既然是串行顾名思义就是使用单线程的方式进行执行,每次执行回收的时候,串行回收器只有一个工作线程,这样对于并行能力较弱的计算机,串行回收器更多的独占线程专一执行的方面有着良好的实现,也就是说在单线程的程序中效果较好,可以在新生代和老年代,根据不同的堆空间氛围新生代串行回收器和老年代串行回收器。下面就来分别谈谈这些收集器

文章目录

  • 串行回收器
    • 新生代串行回收器
    • 老年代串行回收器
  • 并行回收器
    • 新生代ParNew回收器
    • 新生代ParallelGC回收器
    • 老年代ParallelOldGC回收器
  • CMS 回收器
    • CMS 主要工作步骤
    • CMS主要的参数设置
  • CMS的日志分析
    • 关于Class的回收
  • 总结

串行回收器

新生代串行回收器

  串行收集器是所有的垃圾回收器中最为古老的而一种收集器,在也是JVM最为基本的垃圾回收器之一,它有两个特点

  • 使用单线程进行垃圾回收
  • 独占式的垃圾回收器

  在串行收集器进行垃圾回收的时候,Java应用程序中的线程都是需要暂停的,在之前的分享中也看到过这个现象。在串行回收器运行的时候,应用程序的停止被称为是Stop-The-World。这种停顿如果时间太长的话就会影响用户体验,在对实时性操作要求较高的应用中这种往往是不能使用的。

  如上图所示,串行收集器是一个经过长期生产环境的考验,验证为极为高效的垃圾收集器,在很多的场景中被使用。新生代串行处理器使用复制算法,实现相对比较简单、逻辑处理高效、没有线程之间切换的开销。在很多的单CPU处理器等硬件平台不是特别好的场景中,它的处理性能表现的比很多的并行处理器还要好。

  在之前的例子中使用了如下的参数

-XX:+UseSerialGC 这个参数可以指定使用新生代串行收集器和老年代串行收集器,当虚拟机在Client模式下进行运行的时候,它作为其默认的垃圾收集器。在之前的博客中有对应的输出实例这里就不在过多的展示了

  注意 在查看垃圾收集器的效果的时候可以使用-XX:+PrintGCDetails 进行查看。

老年代串行回收器

  老年代串行收集器使用的是标记压缩算法,与新生代的收集器一样,也是一个串行、独占式的垃圾回收器。由于老年代垃圾回收的时间要比新生代的回收时间长,所以在堆空间比较大的应用中,一旦老年代的垃圾回收启动之后,整个的应用程序可能会停顿很长的时间。影响用户体验。

  虽然是有很多的不好的地方,但是也是作为老牌的垃圾收集器,老年代串行回收器和很多的新生代的垃圾回收器配合使用还是比较高效的,当然它也是CMS回收器的备用回收器。

  如果要想启动老年代串行回收器,可以使用如下的一些参数

  • -XX:+UseSerialGC:新生代、老年代都使用串行回收器
  • -XX:+UseParNewGC: 新生代使用ParNew回收器,老年代使用串行收集器
  • -XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行收集器

并行回收器

  并行回收器是在串行回收器的基础上做了改进,可以使用多个线程同时进行垃圾回收看上去要不单线程收集的速度要快,但是如果底层的硬件不能有效的支持这个并行处理的能力也就不会体现出其垃圾回收的价值。不过在性能支持的场景下这种收集器可以有效的缩短垃圾回收的时间。

新生代ParNew回收器

  ParNew 回收器作为一个工作在新生代的垃圾收集器,只能简单地将串行回收器进行多线程处理,它的回收策略、算法以及参数和新生代串行回收器一样。

  如上图ParNew回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,所以在并发能力较强的CPU上,所产生的停顿时间要短与串行回收器,在单CPU或者并发能力不是太好的系统中,并行回收器就不会比串行收集器好,由于多线程的压力,可能性能上比单线程所表现出的效果还要差。

  开启ParNew回收器可以使用如下的一些参数

  • -XX:+UseParNewGC: 新生代使用ParNew回收器,老年代使用串行回收器
  • -XX:+UseConcMarkSweepGC:新生代使用ParNew回收器,老年代使用CMS

  ParNew回收器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定。在一般情况下最好是与CPU的数量相同,避免线程过多的线程数,从而影响到整个的垃圾收集的性能,在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量,当CPU数量大于8个的时候,ParallelGCThreads 的值等于3+((5*CPU_Count)/8)

-Xmx1g -Xms1g -Xmn900m -XX:+UseParNewGC -Xloggc:gc.log -XX:+PrintGCDetails

第一次 ParNew回收器的日志输入信息

[GC (Allocation Failure) [ParNew: 737280K->28149K(829440K), 0.0276526 secs] 737280K->28149K(956416K), 0.0276925 secs] [Times: user=0.31 sys=0.02, real=0.03 secs]

  可以看到,整个输出的结果和新生代串行收集器是一样的,只有回收器标识符不同。

新生代ParallelGC回收器

  新生代ParallelGC回收器也是使用复制算法的收集器,从表面上来看,和ParNew回收器是一样的,都是多线程独占式的收集器。ParallelGC回收器有一个重要的特点就是关注与系统的吞吐量。

  新生代ParallelGC回收器可以使用一下参数启动

  • -XX:+UseParallelGC: 新生代使用ParallelGC回收器,老年代使用串行回收器
  • -XX:+UseParallelGCOldGC:新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。

ParallelGC回收器提供了两个重要参数用于控制系统的吞吐量。

  • -XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间,它的值是一个大于0 的整数。

  在ParallelGC工作的时候,会调整Java堆大小或者是其他的一些参数,尽可能的把停顿时间控制在MaxGCPauseMillis以内。但是如果使用者希望可以减少停顿的时间,而把这个值变得很小,为了达到预期的停顿时间,虚拟机可能会使用一个较小的堆,而这个将导致垃圾回收变得频繁,从而增加垃圾回收的总时间。

  • -XX:GCTimeRatio : 设置吞吐量大小,它的值是一个0到100 之间的整数。

  如果GCTimeRatio 的值设为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。

  ParallelGC回收器与ParNew回收器的另一个不同之处在于它还支持一种自适应的GC调节策略。使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。在这种模式下,新生代的大小、eden和survivor 的比例、晋升老年代的对象年龄等参数都会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优困难的场景下可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优。

-Xmx1g -Xms1g -Xmn900m -XX:+UseParallelGC -Xloggc:gc.log -XX:+PrintGCDetails

ParallelGC 回收器的工作日志如下

14.341: [GC (Allocation Failure) --[PSYoungGen: 691200K->691200K(806400K)] 691200K->818176K(933376K), 0.7543450 secs] [Times: user=1.15 sys=5.70, real=0.75 secs]

  显示ParallelGC 回收器的工作成果,也就是回收前的内存大小和回收后的内存大小以及花费的时间。

老年代ParallelOldGC回收器

  老年代ParallelOldGC 回收器也是一种多线程并发收集器。与新生代ParallelGC回收器一样,也是一个关注吞吐量的收集器,它是一种老年代的垃圾回收器,并且可以与新生代的ParallelGC进行搭配使用。

  ParallelOldGC 回收器使用标记压缩算法,在JDK1.6 的时候才开始使用。

  使用如下的参数进行操作

  • -XX:+UseParallelOldGC 可以在新生代使用ParallelGC 回收器,老年代使用 ParallelOldGC回收器。在对吞吐量敏感的系统中可以考虑使用
  • -XX:ParallelGCThreads 也可以用于设置垃圾回收时的线程数量
15.007: [Full GC (Ergonomics) [PSYoungGen: 691200K->459974K(806400K)] [ParOldGen: 126976K->126676K(126976K)] 818176K->586650K(933376K), [Metaspace: 3146K->3146K(1056768K)], 0.4226251 secs] [Times: user=4.57 sys=0.11, real=0.42 secs]

  从日志中可以看到它显示了新生代、老年代以及元数据区的使用情况。以及Full GC所消耗的时间。

CMS 回收器

  与之前的垃圾回收器不同,CMS回收器主要关注与系统停顿的时间。CMS是使用Concurrent Mark Sweep 的缩写。也就是并发标记清除,从名称上就可以看到。使用标记清除算法,同时使用并发多线程垃圾回收器。

CMS 主要工作步骤

  CMS回收器的工作过程与其他垃圾收集器相比较,略显复杂。在CMS工作时,主要的步骤有如下几个:初始标记、并发标记、预清理、重新标记、并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而预清理、并发标记、并发清除和并发重置是可以和用户线程一起执行。所以,从整体上,CSM收集不是独占式的,它可以在应用程序运行过程中进行垃圾回收。CMS流程如下:

  根据标记清除算法,初始标记、并发标记和重新标记都是为了标记出需要回收的对象。并发清理则是在标记完成后,正式回收垃圾对象。并发重置是指在垃圾回收完成后,重新初始化CMS数据结构和数据,为下一次垃圾回收做好准备。并发标记、并发清理等并发重置都是可以和应用程序线程一起执行。

  在整个的CMS的执行回收过程中,默认在并发标记之后,会有一个预清理的操作(-XX:-CMSPrecleaningEnable)。预清理是并发的,除了为正式清理做准备和检查之外,预清理还会尝试控制一次停顿的时间。由于重新标记是独占CPU的,如果新生代GC发生后,立即触发一次重新标记。这次停顿的时间可能会很长,所以在预处理的时候,会等待到下一次的新生代GC的发生,然后根据历史性能数据预测下一次新生代GC可能发生的时间,然后在当前时间与预测时间的中间时刻,进行重新的标记,这样可以避免了新生代GC和重新标记重合,尽可能减少停顿时间。

CMS主要的参数设置

  • -XX:+UseConcMarkSweepGC :表示启动对应的CMS垃圾收集器

  CMS默认的启动并发线程数(ParallelGCThreads+3)/4),ParallelGCThreads 表示 GC并行时使用的线程数量,如果新生代使用ParNew,那么ParallelGCThreads 也就是新生代GC的线程数量也就是是4个,之后1个并发线程,而且两个并发线程是有5~8 个ParallelGCThreads线程数。

  并发线程数量也可以通过如下的两个参数进行设定

  • -XX:ConcGCThreads
  • -XX:ParallelCMSThreads

  当CPU资源资源不足的时候,CMS回收线程会导致应用系统性能下降。

注意
  并发是指收集器和应用程序交替执行,并行是指应用程序停止,同时由多个线程一起执行GC。因此并行收集器并不是并发的。因为并行回收器执行时,应用程序完全挂起,不存在交替执行的步骤。

  由于CMS回收器不是独占式的回收器,在CMS执行的过程中还会有应用程序仍然在工作,又会产生新的垃圾。这些新的垃圾在CMS回收过程中是无法清除的,同时应用程序没有中断,所以在CMS执行过程中,还应该确保应用程序有足够的内存可用。所以,CMS回收器不会等地堆内存饱和的时候才进行回收操作,而是当内存使用率达到某个阈值的时候开始进行回收,这样可以保证CMS在工作过程中依然有足够的空间支持应用程序运行。

  之前提到的回收阈值可以通过 -XX:CMSInitiatingOccupancyFraction 来进行指定,默认是68。也就是说当老年代空间使用率达到68%,会执行一次CMS回收。如果应用程序的内存使用率增长很快,在CMS在执行过程中,已经出现了内存不足就会导致CMS回收就会失败,虚拟机将启动老年代串行收集器进行垃圾回收,如果这样应用程序将完全中断,直到垃圾回收完成,这时候应用程序会停顿很长的时间。

调优
  根据应用程序的特点,可以对 -XX:CMSInitiatingOccupancyFraction 进行调优。如果内存增长缓慢,则可以设置一个较大的值,大的阈值可以有效降低CMS的触发频率。

  CMS 是一个基于标记清除算法的回收器。导致内存大量的碎片,在这种情况下即使堆内存仍然有较大的剩余空间,也可能就会被迫进行一次垃圾回收,以换取一块可以使用的连续内存。为了解决这个问题CMS提供了几个压缩整理参数。

  • -XX:+UseCMSCompactAtFullCollection :表示可以在CMS进行垃圾回收之后进行一次内存碎片整理,内存碎片的整理并不是并发执行的
  • -XX:CMSFullGCsBeforeCompaction 参数可以用于设定进行多少次CMS回收之后,进行一次内存压缩。

CMS的日志分析

-Xmx1g -Xms1g -Xmn900m  -XX:+UseConcMarkSweepGC -Xloggc:gc.log -XX:+PrintGCDetails

GC日志结果

14.979: [GC (Allocation Failure) 14.979: [ParNew: 737280K->43307K(829440K), 0.0238308 secs] 737280K->43307K(956416K), 0.0239861 secs] [Times: user=0.26 sys=0.04, real=0.02 secs]
32.211: [GC (Allocation Failure) 32.211: [ParNew: 780587K->92160K(829440K), 0.1395256 secs] 780587K->190880K(956416K), 0.1396500 secs] [Times: user=0.48 sys=0.29, real=0.14 secs]
## CMS 标记初始化
32.351: [GC (CMS Initial Mark) [1 CMS-initial-mark: 98720K(126976K)] 209792K(956416K), 0.0094776 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] ## 并发标记开始
32.361: [CMS-concurrent-mark-start]
32.370: [CMS-concurrent-mark: 0.009/0.009 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]## 并发预清理开始
32.370: [CMS-concurrent-preclean-start]
## 预清理
32.370: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] ## 并发终止预清理
32.370: [CMS-concurrent-abortable-preclean-start]
32.370: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] ## CMS 最终标记
32.370: [GC (CMS Final Remark) [YG occupancy: 125817 K (829440 K)]32.370: [Rescan (parallel) , 0.0104235 secs]32.381: [weak refs processing, 0.0000152 secs]32.381: [class unloading, 0.0003597 secs]32.381: [scrub symbol table, 0.0003096 secs]32.382: [scrub string table, 0.0001029 secs][1 CMS-remark: 98720K(126976K)] 224538K(956416K), 0.0113683 secs] [Times: user=0.13 sys=0.01, real=0.01 secs]
## 并行扫描
32.382: [CMS-concurrent-sweep-start]
32.395: [CMS-concurrent-sweep: 0.013/0.013 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
## 并行重置
32.395: [CMS-concurrent-reset-start]
32.395: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

  通过上面的日志可以看出完全按照之前的几个步骤来进行执行的,只不过具体的堆内存大小分析还需要读者自己查看
  通过上面的输入结果可以看到,整个的GC操作日志结构与之前都有所不同。这里需要测试一个参数,就是-XX:CMSInitiatingOccupancyFraction参数。

-Xmx1g -Xms1g -Xmn900m  -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=30 -Xloggc:gc.log -XX:+PrintGCDetails
15.010: [GC (Allocation Failure) 15.010: [ParNew: 737280K->43258K(829440K), 0.0278249 secs] 737280K->43258K(956416K), 0.0279809 secs] [Times: user=0.27 sys=0.05, real=0.03 secs]
32.130: [GC (Allocation Failure) 32.130: [ParNew: 780538K->92160K(829440K), 0.1360428 secs] 780538K->190882K(956416K), 0.1361398 secs] [Times: user=0.48 sys=0.30, real=0.14 secs]
32.266: [GC (CMS Initial Mark) [1 CMS-initial-mark: 98722K(126976K)] 209794K(956416K), 0.0093485 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
32.276: [CMS-concurrent-mark-start]
32.283: [CMS-concurrent-mark: 0.008/0.008 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
32.283: [CMS-concurrent-preclean-start]
32.284: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
32.284: [CMS-concurrent-abortable-preclean-start]
32.284: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
32.284: [GC (CMS Final Remark) [YG occupancy: 125817 K (829440 K)]32.284: [Rescan (parallel) , 0.0088066 secs]32.293: [weak refs processing, 0.0000120 secs]32.293: [class unloading, 0.0002377 secs]32.293: [scrub symbol table, 0.0002658 secs]32.293: [scrub string table, 0.0000923 secs][1 CMS-remark: 98722K(126976K)] 224539K(956416K), 0.0095009 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
32.293: [CMS-concurrent-sweep-start]
32.310: [CMS-concurrent-sweep: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
32.310: [CMS-concurrent-reset-start]
32.310: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

 如果下GC日志里发现并发收集失败,这可能就是由于应用程序在运行过程中老年代的空间不足导致,如果在CMS工作中出现非常频繁的并发模式失败,就应该考虑调整,就可以通过上面这个参数来降低CMS触发的阈值,使得CMS在执行过程中,仍然会有较大的老年代空间可以使用

-Xmx1g -Xms1g -Xmn900m  -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=90 -Xloggc:gc.log -XX:+PrintGCDetails
002651 secs]42.969: [scrub symbol table, 0.0003033 secs]42.969: [scrub string table, 0.0001144 secs][1 CMS-remark: 98725K(126976K)] 648111K(956416K), 0.0155712 secs] [Times: user=0.19 s
ys=0.00, real=0.02 secs]
42.969: [CMS-concurrent-sweep-start]
43.012: [CMS-concurrent-sweep: 0.043/0.043 secs] [Times: user=0.05 sys=0.00, real=0.04 secs]
43.013: [CMS-concurrent-reset-start]
43.013: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
45.017: [GC (CMS Initial Mark) [1 CMS-initial-mark: 3503K(126976K)] 637603K(956416K), 0.0165192 secs] [Times: user=0.20 sys=0.01, real=0.01 secs]
45.034: [CMS-concurrent-mark-start]
45.034: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
45.034: [CMS-concurrent-preclean-start]
45.035: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
45.035: [CMS-concurrent-abortable-preclean-start]
49.579: [GC (Allocation Failure) 49.579: [ParNew (promotion failed): 829440K->829440K(829440K), 0.3785727 secs]49.958: [CMS49.997: [CMS-concurrent-abortable-preclean: 2.310/4.963 secs]
[Times: user=3.65 sys=0.22, real=4.97 secs] (concurrent mode failure): 126140K->126976K(126976K), 0.3357808 secs] 832943K->321892K(956416K), [Metaspace: 3171K->3171K(1056768K)], 0.7145068 secs] [Times: user=1.53 sys=0.16, real=0.71 secs]
52.293: [GC (CMS Initial Mark) [1 CMS-initial-mark: 126976K(126976K)] 424421K(956416K), 0.0436691 secs] [Times: user=0.07 sys=0.00, real=0.05 secs]

关于Class的回收

  在使用CMS回收器时,如果需要回收Perm区,那么就会触发一次FullGC,但是如果希望使用CMS回收Perm区,必须要进行-XX:+CMSClassUnloadingEnabled 进行设置使用了这个参数,如果条件允许,系统会使用CMS的回收Perm区Class数据。

总结

  到这里传统的在1.7之前的垃圾回收器都说完了,在后面的分享中会提到一个新的垃圾回收器-G1,敬请期待吧!

JVM优化系列-JVM垃圾收集器介绍相关推荐

  1. JVM优化系列-JVM G1 垃圾收集器

    导语   G1回收器是在JDK1.7中正式使用的一种全新的垃圾回收器,它的目标是为了取代CMS回收器.G1回收器拥有独特的垃圾回收策略,和之前的任意的一种垃圾回收器都有所不同,但是从分代策略上来说依然 ...

  2. JVM优化系列-JVM内存溢出的原因

    导语   内存溢出(OutOfMemory)OOM,通常情况下出现在某一块内存空间快要消耗完的时候.在Java程序中,导致内存溢出的原因有很多,下面就来分享关于内存溢出的一些问题.其中包括堆内存.直接 ...

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

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

  4. JVM的7种垃圾收集器

    原文地址:Java虚拟机垃圾回收(三) 7种垃圾收集器 Java虚拟机垃圾回收(三) 7种垃圾收集器 主要特点 应用场景 设置参数 基本运行原理 在<Java虚拟机垃圾回收(一) 基础>中 ...

  5. JVM(2)垃圾收集器

    1.对象存活 内存回收与分配重点关注的是堆内存和方法区内存(程序计数器占用小,虚拟机栈和本地方法栈随线程有相同的生命周期). 1.1.引用计数算法 给对象中添加一个引用计数,每当有一个地方引用它时,计 ...

  6. Java8默认垃圾收集器介绍

    如何确认自己的JDK使用的哪个垃圾收集器? 在可以用jvm参数,打印自己的jdk版本和收集器信息 -XX:+PrintCommandLineFlags -version 测试代码-指定参数启动 /** ...

  7. JVM 常见垃圾收集器介绍

    垃圾收集器如何演化的? 垃圾收集器的发展路线,简单来说是随着内存越来越大而发生变化. 从分代算法逐渐演化为不分代算法. 从serial的几十兆,逐渐演化到parallel的几个G,再到CMS的几十个G ...

  8. java 查看垃圾收集器_JVM系列:查看JVM使用的什么垃圾收集器

    一.方法一 打印虚拟机所有参数 [root@localhost ~]# java -XX:+PrintFlagsFinal -version | grep : uintx InitialHeapSiz ...

  9. JVM垃圾回收系列之垃圾收集器一

    序 "黑发不知勤学早,白发方悔读书迟" 引言 本文将介绍HotSpot中常见的几种垃圾收集器,如:Serial.ParNew.Parallel Scavenge.CMS等 参考书籍 ...

最新文章

  1. 利用jsoncpp将json字符串转换为Vector
  2. hosts和resolv.conf区别
  3. 类加载、类加载器、反射
  4. jQuery的显示和隐藏
  5. java反序列化漏洞 tomcat_CVE-2020-9484 Apache Tomcat反序列化漏洞浅析
  6. Visual Studio 2015 单元测试(c++初试)
  7. asp.net中FCKeditor的调用(31)
  8. matlab中S函数的概念及使用
  9. 蛮牛教育Unity Shader从入门到精通:第十节课Shader问题
  10. JTextField文本框的使用
  11. labelImg打标签教程
  12. html 文件常用格式
  13. 打包 压缩 解压缩命令
  14. java 画立体图形
  15. 汤小小账号变现课第2期,今日头条、小红书、公众号,1000粉也可以接广告变现
  16. Revit二次开发之族库管理系统
  17. 3dmax动画学习阶段总结
  18. 开源 安卓项目汇总
  19. 机械革命无法使用U盘启动linux,机械革命bios设置,详细教您机械革命bios怎么设置u盘启动...
  20. 以json格式输出 bro(zeek)日志

热门文章

  1. centos8 用u盘安装失败_CentOS8 的安装过程
  2. 适合0基础的web开发系列教程-canvas
  3. Java JUC学习 - ConcurrentLinkedDeque 详解
  4. while/for 嵌套expect 批量免密码传文件
  5. ASP.NET中Url重写后,打不开真正的Html页面
  6. MySql关键字-保留字
  7. Android(Fragment和Activity之间通信)
  8. HttpApplication的认识与加深理解
  9. 利用权限禁止QQ的自动升级(QQUpdateCenter)
  10. 解决sourceTree的git clone 报SSH密钥认证失败的问题