Java真的是一门编译型的语言吗——即时编译器JIT

如有错误请大佬指正

JIT是什么

JIT(Just-in-Time,实时编译)一直是Java语言的灵魂特性之一。

让我们回忆一下Java程序是如何运行的

我们知道编程语言根据编译及运行过程,主要分为两大阵营:编译型语言 和 **解释型语言。**前者在运行前需要先通过编译器编译成目标产物(通常来说是机器码),然后才可以运行,一旦代码改动就需要重新编译生产新的产物,代表c/c++,而后者则不需要进行编译,由解释器直接接收用户编写的源代码,逐行逐块地解释执行,即便是在运行过程中也可以动态地修改代码行为,代表JavaScript。

Java语言通常被归属为编译型语言,但其与C/C++、Go这些传统意义上的编译型语言又有所不同,Java代码经由编译器编译后得到的产物并不是机器码,而是 字节码 这种“中间语言”,需要交给JVM来运行,而JVM在运行字节码时,其实是 先解释执行,运行达到一定的频率之后再编译成机器码运行。因此Java语言同时具有 编译型语言 和解释型语言的特点:既能保证运行速度够快,同时又具有一定的运行时灵活性,也被称为“半编译半解释型”语言。

这里就不得不提到JVM的JIT(Just-in-Time)编译器,它的运行原理如下图所示:

以方法(或代码块)为单位,当任意一个方法被调用的时候,JVM会先判断这个方法是否已经被编译过,如果没有被编译就会以解释的方式进行运行,而当这个方法执行的次数达到一定阈值的时候,就会被认定为是“热点代码”,并触发JIT编译器的编译过程,将其编译为本地机器码,存储到CodeCache中。后续JVM再运行这个方法的时候,就会直接从CodeCache中以本地机器码的方式运行,提高运行效率。

HotSpot JVM中集成了两种JIT编译器,Client Compiler和Server Compiler,它们的作用也不同。Client Compiler注重启动速度和局部的优化,Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢。两种编译器有着不同的应用场景,在虚拟机中同时发挥作用。而随着时间的发展,不论是Client Compiler还是Server Compiler都发展出了各具特色的实现,如 C1、C2、Graal Compiler等,你可以在JVM启动参数中选择自己所需的JIT编译器实现。

通俗理解

java不是完全意义上的编译语言。

java会根据不同环境的jdk生成相应的字节码文件。

字节码文件去解释运行程序,

当一部分代码被反复执行,会把这部分代码标记成热点代码,然后用jit编译成本地汇编代码,提高运行效率。

对于字节码文件的解释执行

打个不太合适的比喻。比如我所在的部门要申请办公用品,笔记本中性笔什么的,需要先给行政部门写申请,然后行政审批通过以后,我再去取物资,但是由于我们部门这些东西消耗得快,三天两头我就得重新申请,行政部门也审批烦了,加之我已经混熟了,行政的小姐姐一看到我的脸就直接把物资拿给我,省的总是审批提高效率。

JIT的优点

  • 可以根据当前硬件情况实时编译生成最优机器指令,或是根据当前程序的运行情况生成最优的机器指令序列,因此理论峰值性能会更高一些
  • 拥有一定的动态性能力,可以运行时动态地对代码行为进行一定的干预
  • 可以根据进程中内存的实际情况调整代码,使内存能够更充分的利用

JIT的缺点

  • 由于进程刚开始执行的时候是解释执行的,因此启动时性能较差,并且启动速度也较慢
  • 程序需要将JIT编译器和VM一起打包,因此完整的可运行产物占用体积较大
  • JIT编译器在运行期间进行编译,需要占用额外的内存和CPU,可能会导致程序运行卡顿

JIT在主流虚拟机中的运用

目前主流的两款商用Java虚拟机(HotSpot、OpenJ9)里,Java程序最初都是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。

提出问题并在学习中回答Q&A

Q1.为何HotSpot虚拟机要使用解释器与即时编译器并存的架构?
Q2·为何HotSpot虚拟机要实现两个(或三个)不同的即时编译器?
Q3·程序何时使用解释器执行?何时使用编译器执行?
Q4·哪些程序代码会被编译为本地代码?如何编译本地代码?
Q5·如何从外部观察到即时编译器的编译过程和编译结果?

A1解释器与编译器两者各有优势:解释器还会给编译器兜底。
当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。当程序
启动后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少
解释器的中间损耗,获得更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释执行
节约内存(如部分嵌入式系统中和大部分的JavaCard应用中就只有解释器的存在),反之可以使用编
译执行来提升效率。同时,解释器还可以作为编译器激进优化时后备的“逃生门”(如果情况允许,
HotSpot虚拟机中也会采用不进行激进优化的客户端编译器充当“逃生门”的角色),让编译器根据概率选择一些不能保证所有情况都正确,但大多数时候都能提升运行速度的优化手段,**当激进优化的假设不成立,如加载了新类以后,类型继承结构出现变化、出现“罕见陷阱”(Uncommon Trap)时可以通过逆优化(Deoptimization)退回到解释状态继续执行,**因此在整个Java虚拟机执行架构里,解释器与编译器经常是相辅相成地配合工作,其交互关系如图11-1所示。

A2.JIT有两种即时编译器,分别是客户端和服务端,客户端的注重启动速度性能,服务端的注重峰值性能。

HotSpot虚拟机中内置了两个(或三个)即时编译器,其中有两个编译器存在已久,分别被称
为“客户端编译器”(Client Compiler)和“服务端编译器”(Server Compiler),或者简称为C1编译器和C2编译器(部分资料和JDK源码中C2也叫Opto编译器),第三个是在JDK 10时才出现的、长期目标是代替C2的Graal编译器。Graal编译器目前还处于实验状态,下一篇文章将介绍在本节里,我们将重点关注传统的C1、C2编译器的工作过程。

混合模式编译

A3.在分层编译(Tiered Compilation)的工作模式出现以前,HotSpot虚拟机通常是采用解释器与其中
一个编译器直接搭配的方式工作,程序使用哪个编译器,只取决于虚拟机运行的模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在客户端模式还是服务端模式。

  • 使用 “-client” 强制虚拟机运行于 Client 模式。
  • 使用 “-server” 强制虚拟机运行于 Server 模式。
  • 使用 “-Xint” 强制虚拟机只使用解释器执行程序,编译器不工作。
  • 使用 “-Xcomp” 强制虚拟机只使用编译器执行程序,解释器作为编译器的“逃生门”。
  • 使用 “-XX:+TieredCompilation” 开启分层编译。虚拟机 Server 模式下默认开启。

解释器与编译器搭配使用的方式在虚拟机
中被称为“混合模式”(Mixed Mode),用户也可以使用参数“-Xint”强制虚拟机运行于“解释模
式”(Interpreted Mode),这时候编译器完全不介入工作,全部代码都使用解释方式执行。另外,也可以使用参数“-Xcomp”强制虚拟机运行于“编译模式”(Compiled Mode),这时候将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。可以通过虚拟机的“-
version”命令的输出结果显示出这三种模式。

分层编译

HotSpot虚拟机在编译子系统中加入了分层编译的功能

分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:
·第0层。程序纯解释执行,并且解释器不开启性能监控功能(Profiling)。
·第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启
性能监控功能。
·第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
·第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如
分支跳转、虚方法调用版本等全部的统计信息。
·第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启
用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。
以上层次并不是固定不变的,根据不同的运行参数和版本,虚拟机可以调整分层的数量。各层次
编译之间的交互、转换关系如图11-2所示。

实施分层编译后,解释器、客户端编译器和服务端编译器就会同时工作,热点代码都可能会被多
次编译
,用客户端编译器获取更高的编译速度,用服务端编译器来获取更好的编译质量,在解释执行
的时候也无须额外承担收集性能监控信息的任务,而在服务端编译器采用高复杂度的优化算法时,客
户端编译器可先采用简单优化来为它争取更多的编译时间。

热点代码

A4.即被多次调用的方法。被多次执行的循环体。

对于这两种情况,编译的目标对象都是整个方法体,

要知道某段代码是不是热点代码,是不是需要触发即时编译,这个行为称为“热点探测”(Hot
Spot Code Detection),其实进行热点探测并不一定要知道方法具体被调用了多少次,目前主流的热点探测判定方式有两种[2],分别是:
基于采样的热点探测(Sample Based Hot Spot Code Detection)。采用这种方法的虚拟机会周期性
地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。基于采样的热点探测的好处是实现简单高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
基于计数器的热点探测(Counter Based Hot Spot Code Detection)。采用这种方法的虚拟机会为
每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为
它是“热点方法”。这种统计方法实现起来要麻烦一些,需要为每个方法建立并维护计数器,而且不能
直接获取到方法的调用关系。但是它的统计结果相对来说更加精确严谨。

这两种探测手段在商用Java虚拟机中都有使用到,譬如J9用过第一种采样热点探测,而在HotSpot
虚拟机中使用的是第二种基于计数器的热点探测方法,为了实现热点计数,HotSpot为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter,“回边”的意思就是指在循环边界往回跳转)。当虚拟机运行参数确定的前提下,这两个计数器都有一个明确的阈值,计数器阈值一旦溢出,就会触发即时编译。

  • 注意,机器在热机状态可承受的负载要大于冷机状态(刚启动),如果热机状态时的流量进行切流,可能导致冷机状态的服务器因为无法承载流量假死,生产环境中要以分批的形式进行发布,根据机器数量划分多个批次,每个批次占集群总数的1/8

实战:查看及分析即时编译结果

A5.手动编译尝试
https://github.com/13884566853/UniversalToolBox/tree/main/UniversalToolBox/src/main/java/com/wwt/example/jvm/jit

使用

将Test类主方法com.wwt.example.jvm.jit.JitCompilationTest#main注释去掉

添加vm option参数

-XX:+PrintCompilation -verbose:gc

运行结果1:

213  292 %     3       com.wwt.example.jvm.jit.Test::doubleValue @ 2 (18 bytes)   made not entrant
213  299       4       com.wwt.example.jvm.jit.Test::doubleValue (18 bytes)
214  296       3       com.wwt.example.jvm.jit.Test::doubleValue (18 bytes)   made not entrant
214  300       3       com.wwt.example.jvm.jit.Test::calcSum (26 bytes)
214  301 %     4       com.wwt.example.jvm.jit.Test::calcSum @ 4 (26 bytes)
217  302       4       com.wwt.example.jvm.jit.Test::calcSum (26 bytes)
218  300       3       com.wwt.example.jvm.jit.Test::calcSum (26 bytes)   made not entrant

结果输出含有%说明是有回边计数器触发的OSR编译

从运行结果1中可以确认,main()、calcSum()和doubleValue()方法已经被编译,我们

加上参数要求虚拟机输出方法内联信息

-XX:+UnlockDiagnosticVMOptions
-XX:+PrintCompilation
-XX:+PrintInlining
-verbose:gc`

运行结果2:

    214  282 %     3       com.wwt.example.jvm.jit.JitCompilationTest::doubleValue @ 2 (18 bytes)   made not entrant214  289       4       com.wwt.example.jvm.jit.JitCompilationTest::doubleValue (18 bytes)215  284       3       com.wwt.example.jvm.jit.JitCompilationTest::doubleValue (18 bytes)   made not entrant215  290       3       com.wwt.example.jvm.jit.JitCompilationTest::calcSum (26 bytes)@ 12   com.wwt.example.jvm.jit.JitCompilationTest::doubleValue (18 bytes)   inlining prohibited by policy215  291 %     4       com.wwt.example.jvm.jit.JitCompilationTest::calcSum @ 4 (26 bytes)@ 12   com.wwt.example.jvm.jit.JitCompilationTest::doubleValue (18 bytes)   inline (hot)218  292       4       com.wwt.example.jvm.jit.JitCompilationTest::calcSum (26 bytes)219  290       3       com.wwt.example.jvm.jit.JitCompilationTest::calcSum (26 bytes)   made not entrant@ 12   com.wwt.example.jvm.jit.JitCompilationTest::doubleValue (18 bytes)   inline (hot)

从运行结果2可以看到,doubleValue()方法已被内联编译到calcSum()方法中,所以虚拟机再次执行calcSum()时doubleValue()方法是不会再被实际调用的,没有任何方法
分派的开销,它们的代码逻辑都被直接内联到calcSum()方法里面了。

加上参数要求虚拟机打印汇编结果

-XX:+UnlockDiagnosticVMOptions
-XX:+PrintCompilation
-XX:+PrintInlining
-XX:+PrintAssembly
-verbose:gc`

运行结果3:

这里是编译log
ImmutableOopMap{[184]=Oop [192]=Oop rax=Oop }pc offsets: 2521 2554     231  272       3       com.wwt.example.jvm.jit.JitCompilationTest::calcSum (26 bytes)   made not entrant@ 12   com.wwt.example.jvm.jit.JitCompilationTest::doubleValue (18 bytes)   inline (hot)
Compiled method (c2)     231  275       4       com.wwt.example.jvm.jit.JitCompilationTest::calcSum (26 bytes)total in heap  [0x0000019707aeb310,0x0000019707aeb690] = 896relocation     [0x0000019707aeb470,0x0000019707aeb478] = 8main code      [0x0000019707aeb480,0x0000019707aeb560] = 224stub code      [0x0000019707aeb560,0x0000019707aeb578] = 24oops           [0x0000019707aeb578,0x0000019707aeb580] = 8metadata       [0x0000019707aeb580,0x0000019707aeb590] = 16scopes data    [0x0000019707aeb590,0x0000019707aeb5c8] = 56scopes pcs     [0x0000019707aeb5c8,0x0000019707aeb688] = 192dependencies   [0x0000019707aeb688,0x0000019707aeb690] = 8想看汇编代码可以自己用其他工具生成汇编代码,任选其一即可
<https://github.com/ymm135/hsdis-jitwatch>
1.HSDIS插件
2.JitWatch 需本机jdk环境版本为jdk11及以上
<https://github.com/AdoptOpenJDK/jitwatch>

编译优化技术

经过前面对即时编译的讲解,读者应该已经建立起一个认知:编译器的目标虽然是做
由程序代码翻译为本地机器码的工作,但其实难点并不在于能不能成功翻译出机器码,输出代码优化
质量的高低才是决定编译器优秀与否的关键,下面列举一些翻译机器码优化的方法

HotSpot 的优化技术非常全面,实现起来也比较复杂,但是对于理解它们来说却显得没那么困难,我们将列举几项最有代表性的优化技术。

1. 方法内联

方法内联的重要性要优于其他优化措施,它的主要目的有两个,一是去除方法调用的成本,二是为其他优化建立良好的基础。

方法内联的行为很简单,就是把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用而已。在前面jitwatch分析github实例也演示过

int foo() { int x=2, y=3; return bar(x,y);
}
final int bar(int a, int b) { return a+b;
}

如果编译器可以证明这个 bar就是 foo()中调用的那个方法,则 bar中的代码可以取代 foo()中对 bar()的调用。这时,bar()方法是 final类型,因此肯定是 foo()中调用的那个方法。甚至在一些虚调用例子中,动态 JIT 编译器通常能够推测性地内联目标方法的代码,并且在绝大多数情况下能够正确使用。编译器将生成以下代码:

int foo() { int x=2, y=3; return x+y;
}

2. 公共子表达式消除

如果一个表达式 E 已经计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生变化,那么 E 的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替 E 就可以了。我们来举个例子来模拟下它的优化过程:

public static void main(String[] args) {int a = 1;int b = 1;int c = 1;int d = (c * b) * 12 + a + (a + b * c);// 1. 提取公共子表达式int E = c * b;d = E * 12 + a + (a + E);// 2. 代数化简d = E * 13 + a * 2;}

3. 数组边界检查消除

当我们尝试对数组越界访问的时候,Java 会向我们抛一个 java.lang.ArrayIndexOutOfBoundsException,这对软件开发者来说是一件很好的事情,即使没有专门编写防御代码,也可以避免大部分的溢出攻击,但是对虚拟机来说,意味着每一次的数组访问都带有一次隐含的条件判定操作,即数组边界检查,那么有没有办法消除这种检查呢?

虚拟机一般是在即时编译期间通过数据流分析来确定是否可以消除这种检查,比如 foo[3] 的访问,只有在编译的时候确定 3 不会超过 foo.length - 1 的值,就可以判断该次数组访问没有越界,就可以把数组边界检查消除。

4. 逃逸分析

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

如果能证明一个对象不会逃逸到方法或者线程之外,则可以为这个变量进行一些高效的优化:

  1. XX:+DoEscapeAnalysis 手动开启/关闭逃逸分析,默认开启,C2 编译器有效
  2. XX:+PrintEscapeAnalysis 查看逃逸分析的结果(debug 虚拟机支持)
  3. XX:+EliminateAllocations 手动开启/关闭标量替换,默认开启
  4. XX:+PrintEliminateAllocations 查看标量替换情况(debug 虚拟机支持)
  5. XX:+EliminateLocks 手动开启/关闭同步消除,默认开启

1) 栈上分配

如果确定一个对象不会逃逸出方法之外,假如能使用栈上分配这个对象,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力将会小很多。然而遗憾的是,目前的 HotSpot 虚拟机还没有实现这项优化。

2)同步消除

如果确定一个对象不会被其他线程访问到,那么这个变量就不存在线程间的争抢,对这个变量实施的同步措施也可以消除掉。

3)标量替换

标量:无法被进一步分解的数据,比如原始数据类型(int、long以及 reference 类型等)聚合量:可以被持续分解的数据,典型的就是 Java 中对象,它们还可以被分解成成员变量等。

标量替换指的是如果把一个 Java 对象拆散分解,根据程序访问的情况,将其使用到的成员变量恢复到原始类型来访问。

如果能确定一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候就可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。

尽管目前逃逸分析技术仍在发展之中,未完全成熟,但它是即时编译器优化技术的一个重要前进
方向,在日后的Java虚拟机中,逃逸分析技术肯定会支撑起一系列更实用、有效的优化技术。
由于复杂度等原因,HotSpot中目前暂时还没有做这项优化

参考:

https://github.com/ymm135/hsdis-jitwatch

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)

https://www.codenong.com/p12155840/

Java真的是一门编译型的语言吗——即时编译器JIT相关推荐

  1. java源程序编译型_Java语言的源程序不是编译型的,而是编译解释型的。

    Java语言的源程序不是编译型的,而是编译解释型的. 语言译解2情绪状态的种类有(). 程序感冒期间忌食以下哪些食物 区分团花剪纸的方法,不编译主要看单独纹样重复出现的情况的说法正确吗?( ) 编释型 ...

  2. java源程序编译型_Java语言的源程序不是编译型的,而是编译解释型的。( )_学小易找答案...

    [单选题]下列选项中,不属于Java语言特点的一项是( ). [单选题]贺铸<半死桐>(重过阊门万事非)悼念其妻病逝于: [判断题]Java语言具有较好的安全性和可移植性及与平台无关等特性 ...

  3. java源程序编译型_Java语言的源程序不是编译型的,而是编译解释型的

    各项下列,语言译解破伤症状不属的是驱期风前. 程序后无户的的账结账期末余额是(. 不编译周期期主延长要采用的营销有(产品成熟策略生命.各项中不下列"病"痒因的属于是.脉弦细数,耳鸣 ...

  4. Java是解释型还是编译型?

    有人说Java是编译型的.因为所有的Java代码都是要编译的,.java不经过编译就无法执行. 也有人说Java是解释型的.因为java代码编译后不能直接运行,它是解释运行在JVM上的,所以它是解释型 ...

  5. 大前端开发者需要了解的基础编译原理和语言知识

    转自:https://yq.aliyun.com/articles/180879 在我刚刚进入大学,从零开始学习 C 语言的时候,我就不断的从学长的口中听到一个又一个语言,比如 C++.Java.Py ...

  6. 执行效率做比较,Go、python、java、c#、delphi、易语言等

    比较环境,在win7   64位,比较各种语言的整数型运算,下面的比较只作为单项比较. 具体方式,40000*40000遍历相加.为了防止编译器优化,生成一个随机数. 1:c#,在NET2.0框架下作 ...

  7. Java 答疑:编译器和解释器有何区别?Java 语言属于编译型编程语言还是解释型编程语言?

    文章目录 前言 一.Java 与 Java 字节码 二.什么是编译器(Compiler) 三.什么是解释器(Interpreter) 四.编译器与解释器有何区别? 五.Java 语言属于哪种语言? 总 ...

  8. 编译型与解释型、动态语言与静态语言、强类型语言与弱类型语言概念辨析

    编译型与解释型.动态语言与静态语言.强类型语言与弱类型语言概念辨析 转自:https://blog.csdn.net/u010533843/article/details/76215487 编译型和解 ...

  9. “Java是编译执行的语言”这句话对吗?

    现在让你谈谈对Java平台的理解,你是否会感觉内容过于庞大?这个问题是比较宽泛的,Java发展到现在已经不仅仅是语言这么简单了,Java平台涉及的,包括但不仅限于下面提到的这些内容: Java语言本身 ...

最新文章

  1. teamviewer设备数量上限_智能控制设备连接故障排除
  2. OpenCV图像处理使用笔记(一)——使用鼠标选取任何形状的ROI
  3. JAVA工程师必学技能,进阶涨薪的推进器!这份实战教程请收下
  4. usb检测串口是哪个角_怎样测试串口和串口线是否正常
  5. 三星s2 硬刷Android 8,三星T710(GALAXY Tab S2 8.0)一键救砖教程,轻松刷回官方系统...
  6. 数据结构专题(二):2.2单链表与顺序表,求元素个数,取值与查找
  7. linux sonar 安装教程,Linux下为Sonarqube安装中文插件
  8. web安全day39:渗透测试方法论
  9. 《K-means聚类算法研究综述》笔记
  10. 【C语言】九九乘法口诀表
  11. Linux之校准服务器时间
  12. Java Jsoup爬虫入门
  13. 如何使用a标签下载文件
  14. Oracle gsd服务是什么,oracle RAC GSD简介
  15. 第1015期机器学习日报(2017-06-29)
  16. RC微分积分电路 RC滤波电路 RC脉冲分压器 RC耦合电路 good
  17. 先电iaas云平台搭建(openstack)————搭建过程
  18. C# 彻底删除文件或目录,不可恢复,文件清除工具
  19. 菜鸟带你使用JieCaoMediaPlayer播放本地视频
  20. list和tuple的区别

热门文章

  1. stm32f407+lan8720 和 python 实现多个TCP客户端连接的TCP服务器
  2. Struts2中关于There is no Action mapped for namespace / and action name的错误解决
  3. python多进程与多线程使用
  4. Cisco 交换机/路由器 ssh 配置
  5. 小程序文章转发到企业微信朋友圈——自己记录可能不适合每个人
  6. mmsegmentation 训练自己的数据集
  7. kaggle比赛--Quora Question Pairs
  8. win10启用和关闭hyper-v虚拟化
  9. 使用nltk.stem.wordnet.WordNetLemmatizer()时报错BadZipFile(“File is not a zip file“)的解决方法
  10. react路由鉴权 / 路由守卫(常用经实践可行 推荐阅读)