Redian新闻
>
JVM 解释和编译指南 | Linux 中国

JVM 解释和编译指南 | Linux 中国

科技
 
导读:通过理解解释、即时编译和预先编译之间的区别,有效地使用它们。                           
本文字数:15794,阅读时长大约:18分钟

通过理解解释、即时编译和预先编译之间的区别,有效地使用它们。

Java 是一种跨平台的编程语言。程序源代码会被编译为 字节码(bytecode),然后字节码在运行时被转换为 机器码(machine code)解释器(interpreter) 在物理机器上模拟出的抽象计算机上执行字节码指令。即时(just-in-time)(JIT)编译发生在运行期,而 预先(ahead-of-time)(AOT)编译发生在构建期。

本文将说明解释器、JIT 和 AOT 分别何时起作用,以及如何在 JIT 和 AOT 之间权衡。

源代码、字节码、机器码

应用程序通常是由 C、C++ 或 Java 等编程语言编写。用这些高级编程语言编写的指令集合称为源代码。源代码是人类可读的。要在目标机器上执行它,需要将源代码转换为机器可读的机器码。这个转换工作通常是由 编译器(compiler) 来完成的。

然而,在 Java 中,源代码首先被转换为一种中间形式,称为字节码。字节码是平台无关的,所以 Java 被称为平台无关编程语言。Java 编译器 javac 将源代码转换为字节码。然后解释器解释执行字节码。

下面是一个简单的 Java 程序, Hello.java

  1. //Hello.java
  2. public class Hello {
  3. public static void main(String[] args) {
  4. System.out.println("Inside Hello World!");
  5. }
  6. }

使用 javac 编译它,生成包含字节码的 Hello.class 文件。

  1. $ javac Hello.java
  2. $ ls
  3. Hello.class Hello.java

现在,使用 javap 来反汇编 Hello.class 文件的内容。使用 javap 时如果不指定任何选项,它将打印基本信息,包括编译这个 .class 文件的源文件、包名称、公共和受保护字段以及类的方法。

  1. $ javap Hello.class
  2. Compiled from "Hello.java"
  3. public class Hello {
  4. public Hello();
  5. public static void main(java.lang.String[]);
  6. }

要查看 .class 文件中的字节码内容,使用 -c 选项:

  1. $ javap -c Hello.class
  2. Compiled from "Hello.java"
  3. public class Hello {
  4. public Hello();
  5. Code:
  6. 0: aload_0
  7. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  8. 4: return
  9. public static void main(java.lang.String[]);
  10. Code:
  11. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  12. 3: ldc #3 // String Inside Hello World!
  13. 5: invokevirtual #4 // Method
  14. java/io/PrintStream.println:(Ljava/lang/String;)V
  15. 8: return
  16. }

要获取更详细的信息,使用 -v 选项:

  1. $ javap -v Hello.class

解释器,JIT 和 AOT

解释器负责在物理机器上模拟出的抽象计算机上执行字节码指令。当使用 javac 编译源代码,然后使用 java 执行时,解释器在程序运行时运行并完成它的目标。

  1. $ javac Hello.java
  2. $ java Hello
  3. Inside Hello World!

JIT 编译器也在运行期发挥作用。当解释器解释 Java 程序时,另一个称为运行时 分析器(profiler) 的组件将静默地监视程序的执行,统计各部分代码被解释的次数。基于这些统计信息可以检测出程序的 热点(hotspot),即那些经常被解释的代码。一旦代码被解释次数超过设定的阈值,它们满足被 JIT 编译器直接转换为机器码的条件。所以 JIT 编译器也被称为分析优化的编译器。从字节码到机器码的转换是在程序运行过程中进行的,因此称为即时编译。JIT 减少了解释器将同一组指令模拟为机器码的负担。

AOT 编译器在构建期编译代码。在构建时将需要频繁解释和 JIT 编译的代码直接编译为机器码可以缩短 Java 虚拟机(Java Virtual Machine)(JVM) 的预热(warm-up)时间。(LCTT 译注:Java 程序启动后首先字节码被解释执行,此时执行效率较低。等到程序运行了足够的时间后,代码热点被检测出来,JIT 开始发挥作用,程序运行效率提升。JIT 发挥作用之前的过程就是预热。)AOT 是在 Java 9 中引入的一个实验性特性。jaotc 使用 Graal 编译器(它本身也是用 Java 编写的)来实现 AOT 编译。

以 Hello.java 为例:

  1. //Hello.java
  2. public class Hello {
  3. public static void main(String[] args) {
  4. System.out.println("Inside Hello World!");
  5. }
  6. }
  7. $ javac Hello.java
  8. $ jaotc --output libHello.so Hello.class
  9. $ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libHello.so Hello
  10. Inside Hello World!

解释和编译发生的时机

下面通过例子来展示 Java 在什么时候使用解释器,以及 JIT 和 AOT 何时参与进来。这里有一个简单的程序 Demo.java :

  1. //Demo.java
  2. public class Demo {
  3. public int square(int i) throws Exception {
  4. return(i*i);
  5. }
  6. public static void main(String[] args) throws Exception {
  7. for (int i = 1; i <= 10; i++) {
  8. System.out.println("call " + Integer.valueOf(i));
  9. long a = System.nanoTime();
  10. Int r = new Demo().square(i);
  11. System.out.println("Square(i) = " + r);
  12. long b = System.nanoTime();
  13. System.out.println("elapsed= " + (b-a));
  14. System.out.println("--------------------------------");
  15. }
  16. }
  17. }

在这个程序的 main() 方法中创建了一个 Demo 对象的实例,并调用该实例的 square()方法,然后显示 for 循环迭代变量的平方值。编译并运行它:

  1. $ javac Demo.java
  2. $ java Demo
  3. 1 iteration
  4. Square(i) = 1
  5. Time taken= 8432439
  6. --------------------------------
  7. 2 iteration
  8. Square(i) = 4
  9. Time taken= 54631
  10. --------------------------------
  11. .
  12. .
  13. .
  14. --------------------------------
  15. 10 iteration
  16. Square(i) = 100
  17. Time taken= 66498
  18. --------------------------------

上面的结果是由谁产生的呢?是解释器,JIT 还是 AOT?在目前的情况下,它完全是通过解释产生的。我是怎么得出这个结论的呢?只有代码被解释的次数必须超过某个阈值时,这些热点代码片段才会被加入 JIT 编译队列。只有这时,JIT 编译才会发挥作用。使用以下命令查看 JDK 11 中的该阈值:

  1. $ java -XX:+PrintFlagsFinal -version | grep CompileThreshold
  2. intx CompileThreshold = 10000 {pd product} {default}
  3. [...]
  4. openjdk version "11.0.13" 2021-10-19
  5. OpenJDK Runtime Environment 18.9 (build 11.0.13+8)
  6. OpenJDK 64-Bit Server VM 18.9 (build 11.0.13+8, mixed mode, sharing)

上面的输出表明,一段代码被解释 10,000 次才符合 JIT 编译的条件。这个阈值是否可以手动调整呢?是否有 JVM 标志可以指示出方法是否被 JIT 编译了呢?答案是肯定的,而且有多种方式可以达到这个目的。

使用 -XX:+PrintCompilation 选项可以查看一个方法是否被 JIT 编译。除此之外,使用 -Xbatch 标志可以提高输出的可读性。如果解释和 JIT 同时发生,-Xbatch 可以帮助区分两者的输出。使用这些标志如下:

  1. $ java -Xbatch -XX:+PrintCompilation Demo
  2. 34 1 b 3 java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)
  3. 35 2 n 0 jdk.internal.misc.Unsafe::getObjectVolatile (native)
  4. 35 3 b 3 java.lang.Object::<init> (1 bytes)
  5. [...]
  6. 210 269 n 0 java.lang.reflect.Array::newArray (native) (static)
  7. 211 270 b 3 java.lang.String::substring (58 bytes)
  8. [...]
  9. --------------------------------
  10. 10 iteration
  11. Square(i) = 100
  12. Time taken= 50150
  13. --------------------------------

注意,上面命令的实际输出太长了,这里我只是截取了一部分。输出很长的原因是除了 Demo 程序的代码外,JDK 内部类的函数也被编译了。由于我的重点是 Demo.java 代码,我希望排除内部包的函数来简化输出。通过选项 -XX:CompileCommandFile 可以禁用内部类的 JIT:

  1. $ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler Demo

在选项 -XX:CompileCommandFile 指定的文件 hotspot_compiler 中包含了要排除的包:

  1. $ cat hotspot_compiler
  2. quiet
  3. exclude java/* *
  4. exclude jdk/* *
  5. exclude sun/* *

第一行的 quiet 告诉 JVM 不要输出任何关于被排除类的内容。用 -XX:CompileThreshold 将 JIT 阈值设置为 5。这意味着在解释 5 次之后,就会进行 JIT 编译:

  1. $ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \
  2. -XX:CompileThreshold=5 Demo
  3. 47 1 n 0 java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native)
  4. (static)
  5. 47 2 n 0 java.lang.invoke.MethodHandle::invokeBasic(LLLLL)L (native)
  6. 47 3 n 0 java.lang.invoke.MethodHandle::linkToSpecial(LLLLLLL)L (native)
  7. (static)
  8. 48 4 n 0 java.lang.invoke.MethodHandle::linkToStatic(L)I (native) (static)
  9. 48 5 n 0 java.lang.invoke.MethodHandle::invokeBasic()I (native)
  10. 48 6 n 0 java.lang.invoke.MethodHandle::linkToSpecial(LL)I (native)
  11. (static)
  12. [...]
  13. 1 iteration
  14. 69 40 n 0 java.lang.invoke.MethodHandle::linkToStatic(ILIIL)I (native)
  15. (static)
  16. [...]
  17. Square(i) = 1
  18. 78 48 n 0 java.lang.invoke.MethodHandle::linkToStatic(ILIJL)I (native)
  19. (static)
  20. 79 49 n 0 java.lang.invoke.MethodHandle::invokeBasic(ILIJ)I (native)
  21. [...]
  22. 86 54 n 0 java.lang.invoke.MethodHandle::invokeBasic(J)L (native)
  23. 87 55 n 0 java.lang.invoke.MethodHandle::linkToSpecial(LJL)L (native)
  24. (static)
  25. Time taken= 8962738
  26. --------------------------------
  27. 2 iteration
  28. Square(i) = 4
  29. Time taken= 26759
  30. --------------------------------
  31. 10 iteration
  32. Square(i) = 100
  33. Time taken= 26492
  34. --------------------------------

好像输出结果跟只用解释时并没有什么区别。根据 Oracle 的文档,这是因为只有禁用 TieredCompilation 时 -XX:CompileThreshold 才会生效:

  1. $ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \
  2. -XX:-TieredCompilation -XX:CompileThreshold=5 Demo
  3. 124 1 n java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native) (static)
  4. 127 2 n java.lang.invoke.MethodHandle::invokeBasic(LLLLL)L (native)
  5. [...]
  6. 1 iteration
  7. 187 40 n java.lang.invoke.MethodHandle::linkToStatic(ILIIL)I (native) (static)
  8. [...]
  9. (native) (static)
  10. 212 54 n java.lang.invoke.MethodHandle::invokeBasic(J)L (native)
  11. 212 55 n java.lang.invoke.MethodHandle::linkToSpecial(LJL)L (native) (static)
  12. Time taken= 12337415
  13. [...]
  14. --------------------------------
  15. 4 iteration
  16. Square(i) = 16
  17. Time taken= 37183
  18. --------------------------------
  19. 5 iteration
  20. 214 56 b Demo::<init> (5 bytes)
  21. 215 57 b Demo::square (16 bytes)
  22. Square(i) = 25
  23. Time taken= 983002
  24. --------------------------------
  25. 6 iteration
  26. Square(i) = 36
  27. Time taken= 81589
  28. [...]
  29. 10 iteration
  30. Square(i) = 100
  31. Time taken= 52393

可以看到在第五次迭代之后,代码片段被 JIT 编译了:

  1. --------------------------------
  2. 5 iteration
  3. 214 56 b Demo::<init> (5 bytes)
  4. 215 57 b Demo::square (16 bytes)
  5. Square(i) = 25
  6. Time taken= 983002
  7. --------------------------------

可以看到,与 square() 方法一起,构造方法也被 JIT 编译了。在 for 循环中调用 square() 之前要先构造 Demo 实例,所以构造方法的解释次数同样达到 JIT 编译阈值。这个例子说明了在解释发生之后何时 JIT 会介入。

要查看编译后的代码,需要使用 -XX:+PrintAssembly 标志,该标志仅在库路径中有反汇编器时才起作用。对于 OpenJDK,使用 hsdis 作为反汇编器。下载合适版本的反汇编程序库,在本例中是 hsdis-amd64.so,并将其放在 Java_HOME/lib/server 目录下。使用时还需要在 -XX:+PrintAssembly 之前增加 -XX:+UnlockDiagnosticVMOptions 选项。否则,JVM 会给你一个警告。

完整命令如下:

  1. $ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \ -XX:-TieredCompilation -XX:CompileThreshold=5 -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintAssembly Demo
  2. [...]
  3. 5 iteration
  4. 178 56 b Demo::<init> (5 bytes)
  5. Compiled method (c2) 178 56 Demo::<init> (5 bytes)
  6. total in heap [0x00007fd4d08dad10,0x00007fd4d08dafe0] = 720
  7. relocation [0x00007fd4d08dae88,0x00007fd4d08daea0] = 24
  8. [...]
  9. handler table [0x00007fd4d08dafc8,0x00007fd4d08dafe0] = 24
  10. [...]
  11. dependencies [0x00007fd4d08db3c0,0x00007fd4d08db3c8] = 8
  12. handler table [0x00007fd4d08db3c8,0x00007fd4d08db3f8] = 48
  13. ----------------------------------------------------------------------
  14. Demo.square(I)I [0x00007fd4d08db1c0, 0x00007fd4d08db2b8] 248 bytes
  15. [Entry Point]
  16. [Constants]
  17. # {method} {0x00007fd4b841f4b0} 'square' '(I)I' in 'Demo'
  18. # this: rsi:rsi = 'Demo'
  19. # parm0: rdx = int
  20. # [sp+0x20] (sp of caller)
  21. [...]
  22. [Stub Code]
  23. 0x00007fd4d08db280: movabs $0x0,%rbx ; {no_reloc}
  24. 0x00007fd4d08db28a: jmpq 0x00007fd4d08db28a ; {runtime_call}
  25. 0x00007fd4d08db28f: movabs $0x0,%rbx ; {static_stub}
  26. 0x00007fd4d08db299: jmpq 0x00007fd4d08db299 ; {runtime_call}
  27. [Exception Handler]
  28. 0x00007fd4d08db29e: jmpq 0x00007fd4d08bb880 ; {runtime_call ExceptionBlob}
  29. [Deopt Handler Code]
  30. 0x00007fd4d08db2a3: callq 0x00007fd4d08db2a8
  31. 0x00007fd4d08db2a8: subq $0x5,(%rsp)
  32. 0x00007fd4d08db2ad: jmpq 0x00007fd4d08a01a0 ; {runtime_call DeoptimizationBlob}
  33. 0x00007fd4d08db2b2: hlt
  34. 0x00007fd4d08db2b3: hlt
  35. 0x00007fd4d08db2b4: hlt
  36. 0x00007fd4d08db2b5: hlt
  37. 0x00007fd4d08db2b6: hlt
  38. 0x00007fd4d08db2b7: hlt
  39. ImmutableOopMap{rbp=NarrowOop }pc offsets: 96
  40. ImmutableOopMap{}pc offsets: 112
  41. ImmutableOopMap{rbp=Oop }pc offsets: 148 Square(i) = 25
  42. Time taken= 2567698
  43. --------------------------------
  44. 6 iteration
  45. Square(i) = 36
  46. Time taken= 76752
  47. [...]
  48. --------------------------------
  49. 10 iteration
  50. Square(i) = 100
  51. Time taken= 52888

我只截取了输出中与 Demo.java 相关的部分。

现在再来看看 AOT 编译。它是在 JDK9 中引入的特性。AOT 是用于生成 .so 这样的库文件的静态编译器。用 AOT 可以将指定的类编译成 .so 库。这个库可以直接执行,而不用解释或 JIT 编译。如果 JVM 没有检测到 AOT 编译的代码,它会进行常规的解释和 JIT 编译。

使用 AOT 编译的命令如下:

  1. $ jaotc --output=libDemo.so Demo.class

用下面的命令来查看共享库的符号表:

  1. $ nm libDemo.so

要使用生成的 .so 库,使用 -XX:+UnlockExperimentalVMOptions 和 -XX:AOTLibrary

  1. $ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libDemo.so Demo
  2. 1 iteration
  3. Square(i) = 1
  4. Time taken= 7831139
  5. --------------------------------
  6. 2 iteration
  7. Square(i) = 4
  8. Time taken= 36619
  9. [...]
  10. 10 iteration
  11. Square(i) = 100
  12. Time taken= 42085

从输出上看,跟完全用解释的情况没有区别。为了确认 AOT 发挥了作用,使用 -XX:+PrintAOT

  1. $ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
  2. 28 1 loaded ./libDemo.so aot library
  3. 80 1 aot[ 1] Demo.main([Ljava/lang/String;)V
  4. 80 2 aot[ 1] Demo.square(I)I
  5. 80 3 aot[ 1] Demo.<init>()V
  6. 1 iteration
  7. Square(i) = 1
  8. Time taken= 7252921
  9. --------------------------------
  10. 2 iteration
  11. Square(i) = 4
  12. Time taken= 57443
  13. [...]
  14. 10 iteration
  15. Square(i) = 100
  16. Time taken= 53586

要确认没有发生 JIT 编译,用如下命令:

  1. $ java -XX:+UnlockExperimentalVMOptions -Xbatch -XX:+PrintCompilation \ -XX:CompileCommandFile=hotspot_compiler -XX:-TieredCompilation \ -XX:CompileThreshold=3 -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
  2. 19 1 loaded ./libDemo.so aot library
  3. 77 1 aot[ 1] Demo.square(I)I
  4. 77 2 aot[ 1] Demo.main([Ljava/lang/String;)V
  5. 77 3 aot[ 1] Demo.<init>()V
  6. 77 2 aot[ 1] Demo.main([Ljava/lang/String;)V made not entrant
  7. [...]
  8. 4 iteration
  9. Square(i) = 16
  10. Time taken= 43366
  11. [...]
  12. 10 iteration
  13. Square(i) = 100
  14. Time taken= 59554

需要特别注意的是,修改被 AOT 编译了的源代码后,一定要重新生成 .so 库文件。否则,过时的的 AOT 编译库文件不会起作用。例如,修改 square() 方法,使其计算立方值:

  1. //Demo.java
  2. public class Demo {
  3. public int square(int i) throws Exception {
  4. return(i*i*i);
  5. }
  6. public static void main(String[] args) throws Exception {
  7. for (int i = 1; i <= 10; i++) {
  8. System.out.println("" + Integer.valueOf(i)+" iteration");
  9. long start = System.nanoTime();
  10. int r= new Demo().square(i);
  11. System.out.println("Square(i) = " + r);
  12. long end = System.nanoTime();
  13. System.out.println("Time taken= " + (end-start));
  14. System.out.println("--------------------------------");
  15. }
  16. }
  17. }

重新编译 Demo.java

  1. $ java Demo.java

但不重新生成 libDemo.so。使用下面命令运行 Demo

  1. $ java -XX:+UnlockExperimentalVMOptions -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:-TieredCompilation -XX:CompileThreshold=3 -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
  2. 20 1 loaded ./libDemo.so aot library
  3. 74 1 n java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native) (static)
  4. 2 iteration
  5. sqrt(i) = 8
  6. Time taken= 43838
  7. --------------------------------
  8. 3 iteration
  9. 137 56 b Demo::<init> (5 bytes)
  10. 138 57 b Demo::square (6 bytes)
  11. sqrt(i) = 27
  12. Time taken= 534649
  13. --------------------------------
  14. 4 iteration
  15. sqrt(i) = 64
  16. Time taken= 51916
  17. [...]
  18. 10 iteration
  19. sqrt(i) = 1000
  20. Time taken= 47132

可以看到,虽然旧版本的 libDemo.so 被加载了,但 JVM 检测出它已经过时了。每次生成 .class 文件时,都会在类文件中添加一个指纹,并在 AOT 库中保存该指纹。修改源代码后类指纹与旧的 AOT 库中的指纹不匹配了,所以没有执行 AOT 编译生成的原生机器码。从输出可以看出,现在实际上是 JIT 在起作用(注意 -XX:CompileThreshold 被设置为了 3)。

AOT 和 JIT 之间的权衡

如果你的目标是减少 JVM 的预热时间,请使用 AOT,这可以减少运行时负担。问题是 AOT 没有足够的数据来决定哪段代码需要预编译为原生代码。相比之下,JIT 在运行时起作用,却对预热时间有一定的影响。然而,它将有足够的分析数据来更高效地编译和反编译代码。

(题图:MJ/ed3e6e15-56c7-4c1d-aff1-84a225faeeeb)


via: https://opensource.com/article/22/8/interpret-compile-java

作者:Jayashree Huttanagoudar 选题:lkxed 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

LCTT 译者 :Xiangbin Ma
🌟🌟🌟
翻译: 17.0 篇
|
贡献: 3411 天
2014-07-01
2023-11-02
https://linux.cn/lctt/toknow-gh
欢迎遵照 CC-BY-SA 协议规定转载,
如需转载,请在文章下留言 “转载:公众号名称”,
我们将为您添加白名单,授权“转载文章时可以修改”。


微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
Linux 爱好者线下沙龙:LLUG 2023 深圳 - 活动预告 | Linux 中国在 Linux 的 VirtualBox 中从 USB 驱动器启动 | Linux 中国Vojtux:针对视力障碍用户改造 Linux | Linux 中国Librem 11:Purism 推出注重隐私的 Linux 平板电脑 | Linux 中国[日签] 不要在每一天结束的时候留下借口、解释和后悔在 Linux 文件系统中使用 attr 添加扩展属性 | Linux 中国如何在 Linux 中扩展 Veritas 文件系统(VxFS) | Linux 中国Linux Lite 6.6 发布:更新了欢迎应用和图标主题 | Linux 中国百度:单机几十万并发的系统JVM如何优化?如何制作一个 Linux Mint 立付 USB | Linux 中国故乡轶事(十一)我又梦见她了Bazzite:专为 Steam Deck 和 PC 上的 Linux 游戏打造的发行版 | Linux 中国Fedora Linux Flatpak 九月推荐应用 | Linux 中国首款 Linux 游戏本?!Tuxedo 推出 Linux 游戏本 Sirius 16JVM 语言比较研究:Java、Kotlin 和 Scala 的利与弊将手机作为你的 Linux 桌面的摄像头和麦克风 | Linux 中国Linux 游戏的下一个秘密武器:Bottles Next | Linux 中国Linux 爱好者线下沙龙:LLUG 2023 深圳硬核来袭 | Linux 中国道亦有道非常道硬核 JVM 压缩指针详解【七律】 癸卯孟秋感怀任务中心:一款流畅的 Linux 系统监控应用 | Linux 中国Linux 内核动手编译实用指南 | Linux 中国Linux 黑话解释:Linux 中的 Super 键是什么? | Linux 中国Arch Linux 下全面使用 Wayland 的配置指南 | Linux 中国备受欢迎的数字音频工作站 Studio One 新增了对 Linux 的支持 | Linux 中国Linux 上的最佳白板应用程序 | Linux 中国Linux 如何挽救老旧电脑(和地球) | Linux 中国在基于 Arm 的 Thinkpad X13S 笔记本上运行 Linux | Linux 中国如何设计一个 JVM 语言下的 LLM 应用开发框架?以 Chocolate Factory 为例Linus Torvalds:Linux 内核中的 Rust、AI 和疲劳的维护者 | Linux 中国Wolfi:改进云软件供应链的 Linux “非”发行版 | Linux 中国10 个在 Linux 终端中生成有趣的 ASCII 字符画的工具 | Linux 中国长篇小说《谷雨立夏间》序Carl Doy - 月亮代表我的心 - The Moon, My Heart and more
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。