写点什么

解密新一代 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:159950
用户头像

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

关注

评论 1 条评论

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

阿里P7亲自讲解!如何快速的开发一个完整的直播app,成功入职腾讯

欢喜学安卓

android 程序员 面试 移动开发

区块链产业革命:解决融资租赁之谜

旺链科技

区块链应用 融资租赁

【科创人】Testin云测总裁徐琨:创业必须要创造出肉眼可见的价值

科创人

迄今为止最好用的Flink SQL教程:Flink SQL Cookbook on Zeppelin

Apache Flink

flink

5 分钟部署一个 OAuth2 服务并对接 Shibboleth-IdP 3.4.6

冯骐

运维 开发 OAuth2 Shibboleth Go 语言

手把手教学,如何使用低代码快速构建应用程序步骤详解!

优秀

低代码

神经网络攻防:03.使用API修改神经网络参数

P小二

AIPwn AI安全 P小二 神经网络攻防

APICloud Avm.js前端框架的优势

YonBuilder低代码开发平台

小程序 大前端 移动开发 跨端开发 多端开发

Semaphore实战

叫练

CountDownLatch CyclicBarrier Semaphore 线程协作

阿里P7亲自讲解!驱动核心源码详解和Binder超系统学习资源,跳槽薪资翻倍

欢喜学安卓

android 程序员 面试 移动开发

云安全和访问管理

龙归科技

云计算 安全 云端 企业安全

都 2021 年了,也该抛弃 ExpressJS 了

LeanCloud

大前端 nodejs 框架

史上最全整合第三方登录的开源库

happlyfox

OAuth 2.0 28天写作 3月日更

四面阿里成功斩获offer,在此分享我的复盘经验总结!

Java架构之路

Java 程序员 架构 面试 编程语言

2021总结全网最新、最全、最实用Java岗面试真题!已收录GitHub

比伯

Java 编程 架构 面试 程序人生

Python学习心得

张鹤羽

28天写作 3月日更

园区网中 IPv6 地址的终端 mac 地址追溯

冯骐

Python 运维 日志 网络 ipv6

15 分钟部署一个 CAS 服务并对接 Shibboleth-IdP 3.4.6

冯骐

CAS 认证 Shibboleth 统一身份认证

Promise原理及常用操作

花明

软件开发,如何快速有效缩短项目周期

雯雯写代码

软件开发

GitHub上获赞10万star的高并发神级进阶资料,面试官再问高并发问题请你把这篇文章发给他!

Java架构之路

Java 程序员 架构 面试 编程语言

2021年最新京东技术岗现场三面:jvm调优+高并发+算法+网络+数据库+设计模式

Java架构之路

Java 程序员 架构 面试 编程语言

牛掰,阿里P8这份笔记不就相当于金三银四中的原子弹吗?已经帮助13位同行拿到了一线大厂的offer!

Java架构师迁哥

怎样在自己的 Web 中加入强大的日志系统?slf4j 的日志插件必须要知道!

老王说编程

slf4j java 日志 日志管理 日志框架

Shibboleth-IdP 的 OAuth2 对接方案详解

冯骐

OAuth2 SAML Shibboleth CARSI

程序员之禅(二)

每天读本书

读书笔记 每天读本书

智慧党建平台解决方案--高效开展党建工作

13530558032

神经网络攻防: 02.攻击模型的输出层

P小二

AIPwn AI安全 P小二 神经网络攻防

世界经济论坛:四大区块链趋势将在今年绽放异彩

CECBC

区块链

区块链电子合同--电子合同区块链签约平台

13530558032

beego + nginx 实现反向代理统一认证

冯骐

nginx 开发 ldap auth_request Go 语言

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