写点什么

解密新一代 Java JIT 编译器 Graal

  • 2018-07-26
  • 本文字数:15440 字

    阅读完需:约 51 分钟

关键要点

  • Java 的 C2 JIT 编译器寿终正寝。
  • 新的 JVMCI 编译器接口支持可插拔编译器。
  • 甲骨文开发了 Graal,一个用 Java 编写的 JIT,作为潜在的编译器替代方案。
  • Graal 也可以独立运行,是新平台的主要组件。
  • GraalVM 是下一代 VM,支持多种语言(不仅仅是那些可编译为 JVM 字节码的语言)。

甲骨文的 Java 实现是基于开源的 OpenJDK 项目,其中包括自 Java 1.3 以来一直存在的 HotSpot 虚拟机。HotSpot 包含两个独立的 JIT 编译器,分别是 C1 和 C2(有时称为“客户端”编译器和“服务器端”编译器),现在的 Java 通常会在运行程序期间同时使用这两个 JIT 编译器。

Java 程序首先在解释模式下启动,在运行了一段时间之后,经常被调用的方法会被识别出来,并使用 JIT 编译器进行编译——先是使用 C1,如果 HotSpot 检测到这些方法有更多的调用,就使用 C2 重新编译这些方法。这种策略被称为“分层编译”,是 HotSpot 默认采用的方式。

对于大多数 Java 应用程序来说,C2 编译器是整个运行环境中最重要的一个部分,因为它为程序中最重要的部分代码生成了高度优化的机器码。

C2 非常成功,可以生成与 C++ 相媲美(甚至比 C++ 更快)的代码,这要归功于 C2 的运行时优化,而这些在 AOT(Ahead of Time)编译器(如 gcc 或 Go 编译器)中是没有的。

不过,近年来 C2 并没有带来多少重大的改进。不仅如此,C2 中的代码变得越来越难以维护和扩展,新加入的工程师很难修改使用 C++ 特定方言编写的代码。

事实上,人们(Twitter 等公司以及像 Cliff Click 这样的专家)普遍认为,在当前的基础上根本不可做出重大的改进。也就是说,任何后续的 C2 改进都是微不足道的。

在最近发布的版本中有一些改进,比如使用了更多的 JVM 内联函数(intrinsic),文档中是这样描述的这项技术的(主要用于描述 @HotSpotIntrinsicCandidate 注解):

如果 HotSpot VM 使用手写汇编或手写编译器 IR(一种旨在提升性能的编译器内联函数)替换带注解的方法,那么这个方法就是内联的。

JVM 在启动时会探测它运行在哪个处理器上,因此 JVM 可以准确地知道 CPU 支持哪些特性。它创建了一个特定于当前处理器的内联函数表,也就是说 JVM 可以充分利用硬件的能力。

这与 AOT 编译不同,后者在编译时考虑的是通用芯片,并对可用的特性做出保守的假设,因为如果 AOT 编译的二进制文件在运行时试图执行当前 CPU 不支持的指令,就会崩溃。

HotSpot 已经支持了不少内联函数——例如众所周知的 Compare-And-Swap(CAS)指令,可用于实现原子整数等功能。在几乎所有的现代处理器上,这都是通过单个硬件指令来实现的。

JVM 预先知道这些内联函数,并依赖于操作系统或 CPU 架构对特定功能的支持。因此,它们特定于平台,并非每个平台都支持所有的内联函数。

一般来说,内联函数应该被视为点修复,而不是一种通用技术。它们具有强大、轻量级和灵活的优点,但要支持多种架构,带来了潜在的高开发和维护成本。

因此,尽管在内联函数方面取得了进展,但不管怎样,C2 已经走到了生命的尽头,必须被替换掉。

甲骨文最近宣布推出第一版 GraalVM ,这是一个研究项目,可能会成为 HotSpot 的替代方案。

Java 开发人员可以认为 Graal 是由几个独立但互相关联的项目组成的——它既是 HotSpot 的新型 JIT 编译器,也是一个新的多语言虚拟机。我们使用 Graal 来称呼这个新的编译器,使用 GraalVM 来称呼这个新虚拟机。

Graal 的总体目标是重新思考如何更好地编译 Java(以及 GraalVM 支持的其他语言)。Graal 最初的出发点非常简单:

Java 的(JIT)编译器将字节码转换为机器码——在 Java 中,只不过是从一个 byte[] 到另一个 byte[] 的转换——那么如果转换代码是用 Java 编写的话会怎样呢?

事实证明,用 Java 编写编译器有如下的一些优点:

  • 工程师开发新编译器的进入门槛要低得多。
  • 编译器的内存安全性。
  • 能够利用成熟的 Java 工具进行编译器开发。
  • 更快的新编译器功能原型设计。
  • 编译器可以独立于 HotSpot。
  • 编译器能够自己编译自己,以生成更快的 JIT 编译版本。

Graal 使用了新的 JVM 编译器接口(JVMCI,对应 JEP 243 ),可以用在 HotSpot 中,也可以作为 GraalVM 的主要组成部分。Graal 已经发布,尽管它在 Java 10 中仍然是处于实验性阶段。要切换到新的 JIT 编译器,可以这样做:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

我们可以通过三种不同的方式运行一个简单的程序——使用常规的分层编译器,或者使用 Java 10 上的 Graal,或者使用 GraalVM 本身。

为了展示 Graal 的效果,我们使用了一个简单的例子,它可以长时间运行,这样就看到编译器的启动过程——进行简单的字符串哈希:

package kathik;

public final class StringHash {

    public static void main(String[] args) {
        StringHash sh = new StringHash();
        sh.run();
    }

    void run() {
        for (int i=1; i<2_000; i++) {
            timeHashing(i, 'x');
        }
    }

    void timeHashing(int length, char c) {
        final StringBuilder sb = new StringBuilder();
        for (int j = 0; j < length  * 1_000_000; j++) {
            sb.append(c);
        }
        final String s = sb.toString();
        final long now = System.nanoTime();
        final int hash = s.hashCode();
        final long duration = System.nanoTime() - now;
        System.out.println("Length: "+ length +" took: "+ duration +" ns");
    }
}

我们可以设置 PrintCompilation 标记来执行此代码,这样就可以看到被编译的方法(它还提供了一个基线,可与 Graal 运行进行比较):

java -XX:+PrintCompilation -cp target/classes/ kathik.StringHash > out.txt

要查看 Graal 在 Java 10 上运行的效果:

java -XX:+PrintCompilation \
     -XX:+UnlockExperimentalVMOptions \
     -XX:+EnableJVMCI \
     -XX:+UseJVMCICompiler \
     -cp target/classes/ \
     kathik.StringHash > out-jvmci.txt

对于 GraalVM:

java -XX:+PrintCompilation \
     -cp target/classes/ \
     kathik.StringHash > out-graal.txt

这些将生成三个输出文件——前 200 次调用 timeHashing() 后生成的输出看起来像这样:

$ ls -larth out*
-rw-r--r--  1 ben  staff    18K  4 Jun 13:02 out.txt
-rw-r--r--  1 ben  staff   591K  4 Jun 13:03 out-graal.txt
-rw-r--r--  1 ben  staff   367K  4 Jun 13:03 out-jvmci.txt

正如预期的那样,Graal 会产生更多的输出——这是由于 PrintCompilation 输出的不同。不过这一点也不足为奇——Graal 首先要编译 JIT 编译器,所以在 VM 启动后的前几秒内会有大量的 JIT 编译器预热动作。

让我们看一下在 Java 10 上使用 Graal 编译器的 JIT 输出(常规的 PrintCompilation 格式):

$ grep graal out-jvmci.txt | head
    229  293       3       org.graalvm.compiler.hotspot.HotSpotGraalCompilerFactory::adjustCompilationLevelInternal (70 bytes)
    229  294       3       org.graalvm.compiler.hotspot.HotSpotGraalCompilerFactory::checkGraalCompileOnlyFilter (95 bytes)
    231  298       3       org.graalvm.compiler.hotspot.HotSpotGraalCompilerFactory::adjustCompilationLevel (9 bytes)
    353  414   !   1       org.graalvm.compiler.serviceprovider.JDK9Method::invoke (51 bytes)
    354  415       1       org.graalvm.compiler.serviceprovider.JDK9Method::checkAvailability (37 bytes)
    388  440       1       org.graalvm.compiler.hotspot.HotSpotForeignCallLinkageImpl::asJavaType (32 bytes)
    389  441       1       org.graalvm.compiler.hotspot.word.HotSpotWordTypes::isWord (31 bytes)
    389  443       1       org.graalvm.compiler.core.common.spi.ForeignCallDescriptor::getResultType (5 bytes)
    390  445       1       org.graalvm.util.impl.EconomicMapImpl::getHashTableSize (43 bytes)
    390  447       1       org.graalvm.util.impl.EconomicMapImpl::getRawValue (11 bytes)

像这样的小实验应该谨慎对待。例如,太多的屏幕 IO 可能会影响预热性能。不仅如此,随着时间的推移,为不断增加的字符串分配的缓冲区将会变得越来越大,以至于必须在 Humongous Region(G1 回收器为大对象保留的特殊区域)中进行分配——Java 10 和 GraalVM 默认使用了 G1 回收器。这意味着在一段时间之后,G1 垃圾回收主要由 G1 Humongous 主导,而这通常是非常规的情况。 

在讨论 GraalVM 之前,我们需要注意的是,Java 10 为 Graal 编译器提供了另一种使用方式,即 Ahead-of-Time 编译器模式。

Graal(作为编译器)是一个从头开始开发的全新编译器,符合新的 JVM 接口(JVMCI)。所以,Graal 可以与 HotSpot 集成,但又不受其约束。

我们可以考虑使用 Graal 在离线模式下对所有方法进行全面编译而不执行代码,而不是使用配置驱动的方式编译热方法。这也就是“Ahead-of-Time 编译”(JEP 295)。

在 HotSpot 环境中,我们可以用它来生成共享对象 / 库(Linux 上的.so 或 Mac 上的.dylib),如下所示:

$ jaotc --output libStringHash.dylib kathik/StringHash.class

然后我们可以在以后的运行中使用已编译的代码:

$ java -XX:AOTLibrary=./libStringHash.dylib kathik.StringHash

这样用 Graal 只为了一个目的——加快启动速度,直到 HotSpot 的常规分层编译器可以接管编译工作。在完整的应用程序中,JIT 编译的实际测试基准应该能够胜过 AOT 编译,尽管具体情况要取决于实际的工作负载。

AOT 编译技术仍然是最前沿的,而且从技术上讲只支持(甚至是实验性质的)linux/x64。例如,在 Mac 上尝试编译 java.base 模块时,会出现以下错误(尽管仍会生成.dylib 文件):

$ jaotc --output libjava.base.dylib --module java.base
Error: Failed compilation: sun.reflect.misc.Trampoline.invoke(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;: org.graalvm.compiler.java.BytecodeParser$BytecodeParserError: java.lang.Error: Trampoline must not be defined by the bootstrap classloader
       at parsing java.base@10/sun.reflect.misc.Trampoline.invoke(MethodUtil.java:70)
Error: Failed compilation: sun.reflect.misc.Trampoline.<clinit>()V: org.graalvm.compiler.java.BytecodeParser$BytecodeParserError: java.lang.NoClassDefFoundError: Could not initialize class sun.reflect.misc.Trampoline
       at parsing java.base@10/sun.reflect.misc.Trampoline.<clinit>(MethodUtil.java:50)

我们可以使用编译器指令文件来控制这些错误,从 AOT 编译中排除掉某些方法(有关详细信息,请参阅 JEP 295 )。

尽管存在编译器错误,我们仍然可以尝试将 AOT 编译的基本模块代码和用户代码一起运行,如下所示:

java -XX:+PrintCompilation \
     -XX:AOTLibrary=./libStringHash.dylib,libjava.base.dylib \
     kathik.StringHash

打开 PrintCompilation 标记,就可以看到 JIT 的编译情况——现在几乎没有。现在只有一些初始引导程序要用到的核心方法需要进行 JIT 编译:

   111    1     n 0       java.lang.Object::hashCode (native)  
   115    2     n 0       java.lang.Module::addExportsToAllUnnamed0 (native)   (static)

因此,我们可以得出结论,这个简单的 Java 应用程序现在是在几乎 100%的 AOT 编译模式下运行。

现在回到 GraalVM,让我们看一下该平台提供的重磅功能——能够将多种语言完整地嵌入到运行在 GraalVM 上的 Java 应用程序中。

这可以被认为是 JSR 223(Java 平台的脚本)的等效或替代方案,不过 Graal 比之前的 HotSpot 走得更深入更远。

该功能依赖于 GraalVM 和 Graal SDK——GraalVM 默认的类路径中包含了 Graal SDK,但在 IDE 中需要显式指定,例如:

<dependency>
    <groupId>org.graalvm</groupId>
    <artifactId>graal-sdk</artifactId>
    <version>1.0.0-rc1</version>
</dependency>

最简单的例子是 Hello World——让我们使用 GraalVM 默认提供的 Javascript 实现:

import org.graalvm.polyglot.Context;

public class HelloPolyglot {
    public static void main(String[] args) {
        System.out.println("Hello World: Java!");
        Context context = Context.create();
        context.eval("js", "print('Hello World: JavaScript!');");
    }
}

这在 GraalVM 上可以按预期运行,但尝试在 Java 10 上运行时,即使使用了 Graal SDK,仍然会产生这个(不足为奇的)错误:

$ java -cp target/classes:$HOME/.m2/repository/org/graalvm/graal-sdk/1.0.0-rc1/graal-sdk-1.0.0-rc1.jar kathik.HelloPolyglot
Hello Java!
Exception in thread "main" java.lang.IllegalStateException: No language and polyglot implementation was found on the classpath. Make sure the truffle-api.jar is on the classpath.
       at org.graalvm.polyglot.Engine$PolyglotInvalid.noPolyglotImplementationFound(Engine.java:548)
       at org.graalvm.polyglot.Engine$PolyglotInvalid.buildEngine(Engine.java:538)
       at org.graalvm.polyglot.Engine$Builder.build(Engine.java:367)
       at org.graalvm.polyglot.Context$Builder.build(Context.java:528)
       at org.graalvm.polyglot.Context.create(Context.java:294)
       at kathik.HelloPolyglot.main(HelloPolyglot.java:8)

自 Java 6 以来,随着 Scripting API 的引入,已经支持多语言。随着 Nashorn(基于 invokedynamic 的 JavaScript 实现)的出现,Java 8 对多语言的支持有了显著增强。

GraalVM 的与众不同之处在于,Java 生态系统现在明确提供了 SDK 和支持工具,用于实现多语言,并让它们成为运行在底层 VM 之上的平等且可互操作的公民。

完成这一步的关键在于一个叫作 Truffle 的组件和一个简单的 VM——SubstrateVM(能够执行 JVM 字节码)。

Truffle 为创建新语言实现提供了 SDK 和工具。一般过程如下:

  • 从语法开始
  • 应用解析器生成器(例如 Coco/R
  • 使用 Maven 构建解释器和简单的语言运行时
  • 在 GraalVM 上运行生成的语言实现
  • 等待 Graal(在 JIT 模式下)启动,自动增强新语言的性能
  • 在 AOT 模式下使用 Graal 将解释器编译为本机启动器(可选)

GraalVM 默认支持 JVM 字节码、JavaScript 和 LLVM。如果我们尝试向下面这样调用另一种语言,比如 Ruby:

context.eval("ruby", "puts \"Hello World: Ruby\"");

GraalVM 会抛出一个运行时异常:

Exception in thread "main" java.lang.IllegalStateException: A language with id 'ruby' is not installed. Installed languages are: [js, llvm].
       at com.oracle.truffle.api.vm.PolyglotEngineImpl.requirePublicLanguage(PolyglotEngineImpl.java:559)
       at com.oracle.truffle.api.vm.PolyglotContextImpl.requirePublicLanguage(PolyglotContextImpl.java:738)
       at com.oracle.truffle.api.vm.PolyglotContextImpl.eval(PolyglotContextImpl.java:715)
       at org.graalvm.polyglot.Context.eval(Context.java:311)
       at org.graalvm.polyglot.Context.eval(Context.java:336)
       at kathik.HelloPolyglot.main(HelloPolyglot.java:10)

要使用(当前为测试版)Truffle 版本的 Ruby(或其他语言),需要下载并安装它。对于 Graal 版本的 RC1(很快会推出 RC2),可以通过以下方式安装:

gu -v install -c org.graalvm.ruby

要注意,如果 GraalVM 是在系统级别安装的,则需要 sudo。如果使用的是 GraalVM 的非 OSS EE 版本(目前 Mac 上只有这个版本可用),则可以更进一步——可以将 Truffle 解释器转为本机代码。

为语言重建本机镜像(启动程序)可以提高它的性能,但这需要使用命令行工具,比如(假设 GraalVM 是安装在系统级别,因此需要 root 权限):

$ cd $JAVA_HOME
$ sudo jre/lib/svm/bin/rebuild-images ruby

这个工具还处于开发阶段,所以需要进行一些手动操作,开发团队希望在后续让这个流程变得更加顺畅。

如果在重建本机组件时遇到任何问题,请不要担心——即使不重建本机镜像仍然可以正常使用它。

让我们看一个更复杂的多语言示例:

Context context = Context.newBuilder().allowAllAccess(true).build();
Value sayHello = context.eval("ruby",
        "class HelloWorld\n" +
        "   def hello(name)\n" +
        "      \"Hello #{name}\"\n" +
        "   end\n" +
        "end\n" +
        "hi = HelloWorld.new\n" +
        "hi.hello(\"Ruby\")\n");
String rubySays = sayHello.as(String.class);
Value jsFunc = context.eval("js",
        "function(x) print('Hello World: JavaScript with '+ x +'!');");
jsFunc.execute(rubySays);

这段代码有点难以阅读,它同时用到了 TruffleRuby 和 JavaScript。首先,我们调用了一段 Ruby 代码:

class HelloWorld
   def hello(name)
      "Hello #{name}"
   end
end

hi = HelloWorld.new
hi.hello("Ruby")

这将创建一个新的 Ruby 类,并为这个类定义了一个方法,然后实例化了一个 Ruby 对象,最后调用它的 hello() 方法。这个方法返回一个(Ruby)字符串,该字符串在 Java 运行时中被强制转换为 Java 字符串。

然后我们创建了一个简单的 JavaScript 匿名函数,如下所示:

function(x) print('Hello World: JavaScript with '+ x +'!');

我们通过 execute() 调用这个函数,并将 Ruby 返回的结果传给函数,该函数在 JS 运行时中将其打印出来。

请注意,我们在创建 Context 对象时,需要放开该对象的访问权限。这样做是为了 Ruby——JS 没有这个问题——所以在创建对象时稍微复杂了一些。这是由当前的 Ruby 实现限制造成的,这个限制将来可能会被移除。

让我们看一个最终的多语言示例:

Value sayHello = context.eval("ruby",
        "class HelloWorld\n" +
        "   def hello(name)\n" +
        "      \"Hello Ruby: #{name}\"\n" +
        "   end\n" +
        "end\n" +
        "hi = HelloWorld.new\n" +
        "hi");
Value jsFunc = context.eval("js",
        "function(x) print('Hello World: JS with '+ x.hello('Cross-call') +'!');");
jsFunc.execute(sayHello);

在这个版本中,我们返回一个实际的 Ruby 对象,而不仅仅是一个字符串。这次我们没有将它强制转换为任何 Java 类型,而是将其直接传给这个 JS 函数:

function(x) print('Hello World: JS with '+ x.hello('Cross-call') +'!');

它输出了预期的内容:

Hello World: Java!
Hello World: JS with Hello Ruby: Cross-call!

这说明 JS 运行时可以调用处于其他运行时中的对象的方法,并进行无缝类型转换(至少可以进行简单类型转换)。

对于这种可跨多种具有不同语义和类型系统的语言的可互换能力,JVM 工程师已经讨论了很长一段时间(至少 10 年),而随着 GraalVM 的到来,它向主流迈出了非常重要的一步。

让我们使用这一小段打印 Ruby 对象的 JS 代码演示这些外部对象是如何在 GraalVM 中表示的:

function(x) print('Hello World: JS with '+ x +'!');

输出如下(或类似这样的):

Hello World: JS with foreign {is_a?: DynamicObject@540a903b<Method>, extend: DynamicObject@238acd0b<Method>, protected_methods: DynamicObject@34e20e6b<Method>, public_methods: DynamicObject@15ac59c2<Method>, ...}!

这些输出显示了外部对象被表示为一系列 DynamicObject 对象,在大多数情况下,它将语义操作委托给对象的主运行时。

在结束本文之前,我们应该谈谈基准和许可。我们必须搞清楚的是,尽管 Graal 和 GraalVM 有着巨大的前景,但目前仍处于早期阶段 / 实验技术阶段。

它尚未针对通用场景进行优化,并且尚需时日才能与 HotSpot/C2 平起平坐。微基准通常也会产生误导——在某些情况下它们可以指明方向,但对于性能分析来说,只有最终的用户级基准才算数。

我们可以这样想,C2 已经最大限度地提升了局部性能,并且即将寿终正寝。Graal 让我们有机会突破局部最大化,并转到一个更好的新领域——并且有可能会重新构思我们对 VM 设计和编译器的许多想法。但它仍然不够成熟,并且不太可能在几年内完全成为主流。

这意味着现在进行的任何性能测试都应该进行谨慎分析。性能测试的比较(特别是 HotSpot/C2 与 GraalVM)是苹果与橙子之间的比较——一个成熟的生产级运行时与一个还处于早期阶段的实验性产品。

还需要指出的是,GraalVM 的许可制度可能与迄今为止看到的有所不同。甲骨文在收购 Sun 公司时,HotSpot 已经是非常成熟的产品,并被冠以自由软件许可。他们很少在 HotSpot 核心产品之上增加价值和进行变现——例如 UnlockCommercialFeatures 开关。随着这些功能的退出(比如开源Mission Control ),可以说,该模型并没有取得巨大的商业成功。

Graal 与众不同——它起源于甲骨文 Research 项目,现在正朝着生产产品的方向发展。甲骨文已投入大量资金让 Graal 成为现实——该项目所需的人才和团队不足,而且他们都不便宜。因为使用了不同的底层技术,甲骨文可以自由地使用不同的商业许可模型,并尝试基于更广泛的客户群为 GraalVM 变现——包括那些目前不为 HotSpot 运行付费的客户。甲骨文甚至可以将 GraalVM 的某些功能定向提供给甲骨文云客户使用。

目前,甲骨文正在发布一个基于 GPL 许可的社区版本(CE),它可以免费用于开发和生产用途,以及一个企业版(EE),它可以免费用于开发和评估。这两个版本都可以从甲骨文的 GraalVM 网站下载,其中还可以找到更详细的信息。

关于作者

Ben Evans 是 JVM 性能优化公司 jClarity 的联合创始人。他是 LJC(伦敦 JUG)的组织者,也是 JCP 执行委员会的成员,帮助定义 Java 生态系统的标准。Ben 是 Java Champion、3 次 JavaOne Rockstar 演讲者,“The Well-Grounded Java Developer”、新版“Java in a Nutshell”和“Optimizing Java”的作者。他是 Java 平台、性能、架构、并发、初创公司和相关主题的演讲常客。Ben 有时也接受演讲、教学、写作和咨询活动的邀请,具体可以联系他。

查看英文原文 Getting to Know Graal, the New Java JIT Compiler

2018-07-26 18:159777
用户头像

发布了 731 篇内容, 共 444.3 次阅读, 收获喜欢 2000 次。

关注

评论 1 条评论

发布
暂无评论
发现更多内容

HTML和xml有哪些区别?

海拥(haiyong.site)

三周年连更

面向万物智联的应用框架的思考和探索(上)

HarmonyOS开发者

HarmonyOS

阿里巴巴官方上线!号称国内Java八股文天花板(终极版)首次开源

Java你猿哥

Java 微服务 算法 JVM 多线程

SPFA 算法:实现原理及其应用

繁依Fanyi

算法 SPFA

Java面试题1000+附答案大全(合适各级Java开发人员)

架构师之道

Java 面试

假期做了一项调研:大厂为啥都自研RPC?结果合乎情理!

冰河

程序员 RPC 架构师 技术提升 大厂招聘

IPRAN网络结构智能优化

鲸品堂

网络 通信 企业号 5 月 PK 榜

景区共享电动车与校内共享电单车是否可行

共享电单车厂家

共享电动车厂家 景区共享电单车 校内共享电单车 共享电动车投放

Apache Pulsar 在火山引擎 EMR 的集成与场景

字节跳动数据平台

大数据 开源 云原生 解决方案 企业号 5 月 PK 榜

如何保证 RabbitMQ 的消息可靠性

小小怪下士

Java 程序员 RabbitMQ 消息中间件

字节首次公开!23年Java后端面试上岸手册 ,竟含全套后端面试考点

Java你猿哥

Java 算法 JVM 多线程 java面试

Mac音乐制作软件推荐:Ableton Live 11 Suite中文版「win/Mac」

Rose

Ableton Live 11破解版 Ableton Live 11中文版 苹果软件下载

阿里巴巴爆款“Spring Cloud Alibaba 全彩笔记”正式发布

采菊东篱下

微服务

新来个技术总监:发现谁再用 delete 删数据直接开除!

Java你猿哥

Java MySQL ssm 存储 delete

神秘的IP地址8.8.8.8地址到底是什么?为什么会被用作DNS服务器地址呢?

wljslmz

DNS 三周年连更

xmind怎么导出为pdf?Xmind最全入门教程

Rose

Xmind 2022 XMind下载 思维导图软件

史上最强升级!音乐制作软件Logic Pro中文特别版

Rose

Logic Pro Mac音乐软件下载 Logic Pro破解版

Prometheus 瘦身第一步,使用 mimirtool 找到没用的 Prometheus 指标

秦晓辉

Grafana Prometheus Mimir mimirtool

阿里巴巴官方上线!号称国内2023最新Java八股文天花板(终极版)首次开源

程序员小毕

程序员 微服务 JVM java面试 Java八股文、

你想要的【微前端】都在这里了! | 京东云技术团队

京东科技开发者

前端 微前端 微前端框架 企业号 5 月 PK 榜 mirco

如何解决Paragon NTFS for Mac安装分卷失败?

Rose

Paragon NTFS ntfs 安装分卷失败

Redis Set 用了 2 种数据结构来存储,到现在才知道

Java你猿哥

Java ssm sets

Python网络爬虫原理及实践 | 京东云技术团队

京东科技开发者

Python 爬虫 python 爬虫 爬虫入门 企业号 5 月 PK 榜

手把手教会你|Sockets多用户-服务器数据库编程

TiAmo

服务器 socket通信 数据库编程

工业互联网:加速从“中国制造”迈向“中国智造”

华为云开发者联盟

云计算 工业互联网 华为云 华为云开发者联盟 企业号 5 月 PK 榜

主流框架都用SPI机制,看一下他们的区别和原理

Java你猿哥

ssm 框架 JavaSPI Spring SPI Dubbo SPI

The Foundry Modo 16 16.1v3激活版 专业3D建模软件

Rose

3d建模 The Foundry Modo

GitHub上“千金难求”的Spring Boot趣味实战全彩版手册,太干了

程序知音

Java spring 微服务 springboot Java进阶

10分钟带你徒手写个Java线程池

华为云开发者联盟

开发 华为云 华为云开发者联盟 企业号 5 月 PK 榜 Java线程池

从0到1:可自定义数据列的成绩查询小程序开发笔记

CC同学

基于 Rainbond 的混合云管理解决方案

北京好雨科技有限公司

Kubernetes 云原生 rainbond 混合云架构

解密新一代Java JIT编译器Graal_Java_Ben Evans_InfoQ精选文章