Java最新技术介绍和分析 (202305)

说明:本文完成了2023年5月份,当时最新的LTS版本是Java17,本文在撰写时参考了美团技术团队和阿里JDK团队相关的文章,以及本文也引了用文章中的图片。在此表示感谢!

Java版本火车

相信老牌的Java开发者和爱好者把Java的版本停留在经典的java5,Java 6,以及Java8和Java11版本上,这几个版本都是java的里程碑版本。 Java 社区决定从Java 9开始每半年发布一个新版本,同时LTS(long-term support)版本从原来的每3年改为每2年发布一个。下图是我根据Oracle官网Support Roadmap做的甘特图。

Java 7,8,11. 17以及还未发布的Java 21均是LTS(Long Term Support)版本,Oracle提供5年的维护周期,以及3年的付费额外支持,一共8年维护周期。

到目前为主,工业界的主力版本依然是Java 8和11. 随着云计算,大数据,以及多核的快速发展,Java社区也在适就这些变化,在随后的Java版本中增加了很多对云场景、新硬件的技术,比如对容器技术的技术,对大内存和NUMA技术的支持。

当前团队从2021年开始就考虑使用ZGC新特性,充分发挥大内存的特点,能更好满足大数据对多核和大内存的发展趋势,目前我们正在考虑使用Java 17 LTS版本来支持大数据领域的技术发展。​

Java重要特性介绍

当前团队当前主流的Java版本是Java8和11,为了让大家对Java高版本特性有更好的了解,我从Java12开始直到Java17最为重要的特性做了初步的分析。由于篇幅有有限和对Java了解深度不足,本文只介绍Java虚拟机领域较新的特性,不涉及Java语言和Java框架发展分析。Java技术的爱好者均可从这里找到完整的Java提案。

因此,本文重点分析Java12到Java17在性能方面和云计算方面取得的进展。

性能​

ZGC

ZGC无疑是Java GC算法的一个大变革,相比之前的GC算法,提供了以下亮眼的特性:

  • 亚毫秒级的最大暂停时间 (最大暂停时间为10ms,但实际暂停时间基本在1ms以内)
  • 暂停时间与堆大小无关,与活跃对象和根引用集合大小无关
  • 堆大小从8M到16T都可以支持

ZGC最早出现在JDK 11中作为实验特性,从Java15开始作为生产特性使用. ZGC为什么能提供这么亮眼的特性,它的关键设计要点有如下:

  • 最大限度的Concurrent: 除了几个非常短和工作量较为固定的阶段需要STW(Stop The World)外,其它阶都是GC线程和业务线程并发执行
  • Region-Based: 可以灵活支持不同大小的Heap
  • Compacting: 使用Compact机制,同时也不再分年轻代和老年代
  • NUMA-aware: 感知机器的NUMA拓扑结构,让线程分配内存让尽量在本地Node上分配内存,让后续内存访延迟更小
  • 使用3颜色指针和读屏碍:这是ZGC一种创新,通过通过读屏障,在Compact阶段,用户线程无须停下来,而是帮着GC线程在访问对象的同时顺便把Compact的工作也做了;3颜色则表示对象指针的3种视图来表示对象是不是正在GC状态,无须在Java对象上标准Mark状态。
  • 使用透明大页(Transparent Huge Page, THP):THP是比Linux默认的hugetlb更灵活,ZGC天生支持THP,让堆对象的访问性能更高,THP(2M页)与4K页内存访问相比,性能会有5~8%的性能提升

ZGC一个回收集周期图示如下:

从上图可以看出,只有Initial Mark,Remark和Initial Transfer过程才需要STW,其它阶段都是GC线程与业务线得程并发执行,不会造成业务线程停顿。上述的3个会产生STW阶段的工作量只与根引用集合大小相关,实际上这几个过程都只需要做一次根对象扫描,耗时非常短,通常在1ms以内,也这是为什么ZGC号称最大停顿时间时只有亚毫秒级。

3颜色指针和读屏障

​为了更方便支持在Compact阶段与业务线程并发执行,ZGC提出了3颜色指针方案:方案原理非常简单,那就是将操作系统分配的一块物理内存,同时映射到3块不同的虚拟地址空间,并且同一个对象的虚拟地址在这3个空间里,只是地址的高位是不同的,低42位是相同。这样就可以通过修改对象引用地址的高位,实现不同的地址视图切换。

上图是3个地址空间的划分,M0,M1和Remapped,中间有一个预留区域,是为了与下图的指针位相匹配的。

Marked 0, Marked 1和Remapped这3个标志位是互斥的,任何时候只能置一个位。当Marked 0置位时,指针指向M0空间,当前Marked 1置位时,指针向批M1空间,当Remapped置位时,指针指向Remapped空间。​

ZGC的几个关键流程如下:

  • 初始化:GC线程扫描所有根引用对象,并将这些引用视图修改为Remapped
  • 并发标记阶段:无论是并行的GC线程扫描到对象时,将对象视图切换成M0,业务线程在些阶段访问到对象时,读屏障代码会将对象视图修改为M0. 这个阶段结束后,对象的地址视图是M0时,表示是一个活跃对象。对象依然停留在Remapped视图时,表示它是非活跃对象,可以回收。
  • 并发转移阶段(compact):这个阶段的功能,是将活跃对象搬依次搬到Region的低地址区。过程是GC线程和业务并发执行,GC线程专注于搬移对象,但此过程里恰巧业务访问对象时,会在读屏障代码里将对象实施转移。对象是否已经转换成功了,全靠地址视图来判定。如果为M0时,说明还没有转移,则转移并将地址视图切换成Remapeed,否则不用做处理(因为有另一个线程帮忙做了)。

以下是JDK社区官方测试结果:

左图是Java标准的的性能测试套结果,数据做了归一处理,将ZGC的max-jOPS归一化成100%。max-jOPS表示吞吐性能,数值越大越好,critical-jOPS反应延迟性能,数据越大越好。从测试效果来看,ZGC比G1有明显的提升,max-jOPS提升10%,critical-jOPS提升约40%。

右图同时是SPECjbb@2015测试时,跟踪到的GC暂停时间。从测试结果来看,ZGC的平均值,99.99分位值和 Max值都在1ms左右,吊打此前任何版本的GC算法。

Virtual Thread

提到Java已发布的Virtual Thread,必须要提起它的真身——协程,而谈到协程,又必须多嘴提一下没有协程前的血泪史。

在刚开始的互联网时代,Appach HTTP server作为web server的实现者,以process per client的方式支持多client并发访问,在并发量达到10K时(可详见C10K问题),操作系统完全无法承受大量进程所占用的资源开销。接着Unix/Linux出现了kqueue/select多路侦听技术,client的消息调度与处理不再交给process或thread调度了,而是每个应用自己来调度client请求。通过类似于Java NIO或者事件回调这种异步处理技术,可以实现一个server高并发,性能表现非常优异,典型的例子就是Nginx。随着异步回调技术的发展,开发人员很快就陷入了回调地狱的陷阱。一个连贯的线性处理逻辑,必须拆分成多个小片段,散落在多个回调事件处理钩子中,可维护性差。在Java领域,使用reactive programming是一个方向,典型的是Vert.X框架帮助开发人员编写反应式程序。

随后协程的出现,能完美解决这个问题。在代码编写上(形式上):client处理代码只需写一个类似于while(1)的函数,读取request,然后process,再发送reponse给client,依次循环直接client主动关闭链接。在实际执行上,server由于大量的client链接,也有有大量的协程存在,但当一个client在recv 等待request时,协程调度器会快速切换另一协程(client)进行处理。 相对于传统的由kernel对线程进行调度,转换成由协程调度器在线程上下文内对协程进行调度,效率非常。

Java的Loom项目就是推动Java协程落地的项目,下图是从编程的复杂性和运行时的可伸缩性,分别几个技术方向的特点,协程是兼顾了编程简单化和可伸缩性两个方面,鱼和熊掌可兼得。

Java19开始提供Virtual Thread技术,作为一个preview特性,这意味Virtual Thread技术并没有定稿,它的实现和API在将来的生产版本也许会改变。

在介绍Virtual Thread编程API前,先说一下相关的技术概念 :

​​

如上图所示,相比传统的Java线程模型来说,增加了Virtual Thread和Platform Thread。 Virtual Thread比较容理解,就是程序代码中的一个Virtual Thread,但是Platform thread是什么呢?这个并不难,它是协程调度器,同时也是协程的执行载体。Virtual thread和Platform Thread关系并不难理解,Plaform Thread自己会执行 Virtual Thread代码,并且它将多个VT代码交织到一起串着执行。这就像操作系统上的线程与硬件CPU的关系,CPU执行线程,并且是将线程代码交织到一起串着执行。

Plaform thread在任一时刻,只能执行一个Virtutal Thread,其它还没有得到执行的Virtual Thread 称为Unmounted Ready Virtual Thread(未绑定的就绪虚执线程),另外还有一些在等待网络报文请求的Virtual Thread称为Unmounted Blocked Virtual Thread(未绑定的阻塞虚拟线程)。

当Virtual Thread执行到会阻塞的函数调用时(比如InputStream中的read方法),Platform会注监控到,然后把该Virtual Thread注册到一个类似于select/epoll的事件侦听器上(了解NIO的朋友应该不陌生),接下来是Platform Thread 会执行相关的功能,并把当前Vritual Thread放到 Blocked列表上,然后从Ready的Virtual Threads上挑一个来执行。当网络报文达到时, 操作通知到Plaform Thread,然后Platform Thread把相应的Virtual Thread唤醒,放到Ready Virtual Thread,等待下一次被选中执行。

下面简要介绍Virtual Thread的API:

创建Virtual Thread 可用 Thread.ofVirtual()方法:

Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);​

或者使用Thread.Builder来生成一个ThreadFactory,然后可以批量产生多个相同属性的Virtual Thread。

请注意,Virtual Thread在对象上没有创建新的子类,而且用java.lang.thread来表示传统线程或者Virtual Thread,可通过Thread.isVirtual()方法来测试是否为Virtual Thread。

由于Virtual Thread在调用阻塞方法时,都需要Platform Thread进行监控和捕捉,所有涉及IO操作的API都需要进行修改,以支持Virtual Thread功能。因此涉及到了大量的API修改,列表如下:

  • java.util.concurrent
  • Networking
  • http://java.io
  • Java Native Interface (JNI)
  • Debugging (JVM TI, JDWP, and JDI)
  • JDK Flight Recorder (JFR)
  • Java Management Extensions (JMX)
  • java.lang.ThreadGroup

然而Virtual Thread(和协程一样)并不是万能的,它的应用场景是高并发大量网络请求,通过Platform thread快速切换Virtual Thread获得高效的性能,同时提供线性逻辑的编程方式。但对对计算型任务没有任何效果,计算型任务通常是根据CPU个数来划分并行度,中间不涉及大量的线程调度。

Virtual Thread特性到目前为止,还没有进入商用阶段。但在回过头来看看Java发展最为红火时代出现的编程框架,无一例外都使用线程池来解决多大并发问题。这些多线程架框架在持续优化,为开发人员屏蔽了很多技术细节和复杂性。所以Virtual Thread的出现,能否给Java社区带来翻天覆地的变化,我们拭目以待。

Vector API

向量(Vector)这个词相信对大家来说并不陌生,传统的CPU属于SISD(单指令单数据),随着图形计算的需求,SIMD(单指令多数据)功能应运而生。Intel处理也经历了从MMX到SSE,再到现在的AVX512指令。从软件层面说来,很多底层运行库都开始使用SIMD指令来优化内存处理,比如典型的例子是glibc的memcpy函数就使用了SSE指令加速内存拷贝速度,另一个例子应该gcc编译器在-O3优化层级下,对C/C++ for循环也尽力做向量化处理(即用SSE指令进行优化)。

用下面两例代码来体会一下Java编程世界里SISD和SIMD的差异:

// SISD 例子
void scalarComputation(float[] a, float[] b, float[] c) {for (int i = 0; i < a.length; i++) {c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;}
}// SIMD 例子
void vectorComputation(float[] a, float[] b, float[] c) {int i = 0;    int upperBound = SPECIES.loopBound(a.length);for (; i < upperBound; i += SPECIES.length()) {// Boxing: from array to SIMD Vectorvar va = FloatVector.fromArray(SPECIES, a, i);var vb = FloatVector.fromArray(SPECIES, b, i);// parallel operation for multiple datavar vc = va.mul(va).add(vb.mul(vb)).neg();// Unboxing: from SIMD Vector to arrayvc.intoArray(c, i);}
}

SISD代码,只能依次对每份数据进行运算,而SIMD代码可以每次同时对16份数据(512 / 32 = 16)同时进行运算,是因为AVX512能同时处理位宽为512bit的多份数据。下图可以形象说明SISD和SIMD处理逻辑上的差异。

看似完美的SIMD肯定能带来性线的加速比,但事实并非如此,我们先看一下测试结果

从测试数据来看,效果并不理想。上图是一个基础的向量相加运算,并没有带来任何性能提升;下图是向量FMA (Fused-Multiply-Add)运算,在内存大小不超过L2时获取10+倍的性能加速,但数据量再大起来后,加速比回到2-3倍左右。

在这里我们可以有一些猜测,由于SIMD硬件宽度有限(以AVX512为例),每512bit数据要进行一次Boxing和Unboxing操作(FloatVector.fromArray和FloatVector.toArray),SIMD获得的红利大量分都这些操作的磨损了。

重新审视最新的 JEP (https://openjdk.org/jeps/438)会发现这个问题已在Java意料之中,Vector API有两个实现,其一是上面测试数据所体现的,使用API库的方式实现。第二个则是使用JVM的intrinsics上,通过C2 JIT编译器直接翻译成Native汇编代码,比如上图的代码在Intel X64机器翻译成如下的汇编代码:

0.43%   / │  0x0000000113d43890: vmovdqu 0x10(%r8,%rbx,4),%ymm07.38%  │ │  0x0000000113d43897: vmovdqu 0x10(%r10,%rbx,4),%ymm18.70%  │ │  0x0000000113d4389e: vmulps %ymm0,%ymm0,%ymm05.60%  │ │  0x0000000113d438a2: vmulps %ymm1,%ymm1,%ymm1
13.16%  │ │  0x0000000113d438a6: vaddps %ymm0,%ymm1,%ymm0
21.86%  │ │  0x0000000113d438aa: vxorps -0x7ad76b2(%rip),%ymm0,%ymm07.66%  │ │  0x0000000113d438b2: vmovdqu %ymm0,0x10(%r9,%rbx,4)
26.20%  │ │  0x0000000113d438b9: add    $0x8,%ebx6.44%  │ │  0x0000000113d438bc: cmp    %r11d,%ebx\ │  0x0000000113d438bf: jl     0x0000000113d43890​

直接把Java语言翻译成Native 代码,少了JDK API中间赚差价。从上面的汇编代码来,完全没有Boxing和Unboxing代码,性能发挥得极致。

从目前看来,向量计算在Java有以下的支持

  • 把向量当作值类型进行处理
  • 支持Vector API通用化,由intrisincs根据硬件特点,翻译相应该的SIMD硬件指令,同时能屏蔽SIMD功能上的差异,无法用硬件实现的退出成非向量指令
  • 应用Intel的SVML数学库,实现SIMD不能提供的计算能力全部转交给SVML来实现。

目前最新的JEP显示,Vector特性依然还是一个实验特性,期待能早日应用到生产中。

Numa感知​

NUMA (Non-uniform memory access) 技术是计算机体系统架构应对多核挑战下的重要优化:为了减少公共内存带宽的压力和瓶颈,它采用了内存分层的访问结构,正如IDC物理网络的Leaf-Spin架构一样。可从下图的物理结构一窥究竟:

上图是典型的Intel处理器当前的配置,分成两个socket,每个Socket有本地的内存,CPU和硬件。这里我们重要关注内存,所有socket内存条是统一编址的,但是CPU访问本地内存时,速度比访问远端socket的内存要快好几倍,因为他们之间需要一个socket间互联通道QPI进行通信。

实际上,硬件和操作系统对NUMA的支持已经是比较友好了,一个进程在申请内存时,操作系统会尽量从本地内存空间中分配内存给程序使用,减少远端内存访问的概率。

但是Java自身托管了Heap内存管理,所以它需要感知 NUMA,才能在适合的位置分配内存,让业务线程访问性能更高。同样由于NUMA对性能的意义重大,Java在ParallelGC是已支持NUMA感知的,但是G1回收算法在Java14之前并没有支持,所以在Java14提出了支持NUMA感知特性。​

G1感知NUMA的原理并不复杂:在Heap初始化时,比较平均地从两个Socket节点申请内存;当业务线程new对象时,优先考虑从线程所在socket的heap空间上分配对象,这样对年轻代对象非常友好。因为年轻代对象生存周期往往很短,所以业务线程访问时往往没有调度到另一个Socket的CPU上,极大概率是本地访问,性能高。但如果本地Heap内存空间不足时,触发GC回收,再分配。但如果万一本地Heap没有空闲内存了,但另一个socket Heap有较多的空闲空间,G1会failback到远端Heap分配对象。

下图是SPECjbb@2015的测试结果,G1支持NUMA感知是在JDK-14 b24合入的,B23是它前一个版本,它们的性能对比最能说明优化效果。该测试使用了512GB的G1 heap做SPECjbb@2015 benchmark测试,从max-JOPS指标来看,提升了20.64%,而critical-JOPS也有9.52%的提升。

同样地,ZGC在设计是就支持NUMA感知,这样不做详细分析和讨论。​

云原生​

GraalVM

在云计算之前, Java有着独特的优势,在企业应用领域大红大紫,开源大数据领域all in Java。但在随着云计算的蓬勃发展,Java的优势反而成了它的不足。 传统的Java程序难以支持毫秒级的启动时间,厚重的应用编程框架,大大逊色于轻量级Javacript来开发FaaS应用,Java实际应过程遇到的各种性能和多语言问题。 为了应对云计算,以及当下发展火热云原生技术, Oracle研发了GraalVM,一种通用JVM解决方案,以解决在云计算场景的痛点。

GraalVM有以下的技术特点:

  • 支持大量的编程语言:包括原本就支持的JVM语言,以及基于LLVM的语言(C/C++),动态语言:Javascript, Ruby, Python, R语言,也支持动态语言引擎:各种Javascript引擎,FastR引擎,RubyTruffle引擎
  • 支持静态编译:把各种语言直接翻译成Native代码,应用程序进行二进制执行,大大加速了应该程的启动时间
  • 高性能:完全Native代码运行,没有翻译解释执行,同时也消除跨语言调用的成本
  • 底噪小,快速启动,多语言联合调用:解决了传统Java应用需要占用大量内存, 以及启动慢问题,对Java应用上云提供便利

下图是GraalVM官网给出的生态系统:

启动时间和内存占用,这两个云计算极为关注的竞争力,它的表现非常优异,如下两图的测试结果可看到。

上两个用Java生态下的3个应用进行测试对比,启动时间快50多倍,内存底躁降低了5倍以上。

在跨语言方面,GraalVM提供了一种在多语言之间无逢传值的方法,而不像传统方式那样需要序列化和反序列化,大大提高的跨语言的性能。正是因为跨语言能力的出现,开发者可以很容易利用另一个语言最新的库进行软件开发,这种便利性大大提升了研发效率。

启动时间是Java整个社区面临的最大挑战之一。下图清楚地展示了为什么Java启动这么慢,JVM有太多的初始化工作需要做。

这个问题最彻底的做法是将Java代码直接翻译成Native,程序可以马上直接运行,不需要加载字节码过程和第一次的解释执行,性能效果最优。由于没有了JVM,JIT组件,可以省去这部分组件所占用的内存开销,也能减少应用image的体积,这完全符合原生的理念和方法。

Java在GraalVM之前并非没有尝试走过这条路,从Java 2之前的GCJ,再到后来的Excelsior JET,再到现在GraalVM提出的通过静态编译解决Java启动慢问题,一路走过,遇到不少挑战。

但这并不是一条容易的技术路线,Java从Bytecode层面来说,它是一个动态语言,它支持灵活的反射技术,支持AOP(Aspect-Oriented Programming)编译技术,意味着JVM给开发者开放了灵活的动态编程技术。因此,在这种便利下,很多信息(比如方法代码,甚至是类)都是无法在编译时确定的,而是在运行时动态地由代码来生成决定,所以无法在在编译阶段直接生成Native。这也就是为什么在此之前只能做到AOT( ahead-of-time)技术。

但如果要求开发者把上述所有态动性功能都去掉,不需要使用,Java生态下的重要组件,如Srping, Hibernate必然跑不起来,整个Java生态会轰然崩塌。于是一条迂回曲折之路必须会摆在各个架构师前面。前面提到GraalVM支持翻译成Native代码是指其它语言跑在GraalVM可以翻译成Native,或者那些没有使用高级动态功能的 Java可以翻译成Native,但对于绝大多数的Java应用来说,还是一条很长远的路。

容器化

云计算和云原生技术栈当中,容器是最重要的技术之一。以Docker为代表的容器技术流派是最通用的,它是Linux操作系统上的Namespace和cgroup两大技术为支柱。所以,Java支持容器技术变更犹为重要。

Kubernetes作为最重的容器调度平台,在上面部署应用时,当然不少了指定每个pod的 Request和Limit资源要求(包含CPU和memory)。但由于Linux下的Namespace是一种弱隔离,Java应用程序运行在Kubernetes上之后,它仍然要读取/proc/meminfo和/proc/cpuinfo来获取它运行环境的资源情况,来决定Java Heap应该开多大,池程池应该开多大。如果一个Java容器只申请了4C8G(4 core and 8 G memory)资源,但跑在一个96C和512G的机器上面。那它会理所当然地认识它有96个Core可运行,接近500G内存可自由使用。但在云原环境上,这样做很容易带来致命的问题,那就是不断地触发OOM,服务难以正常提供。当然开发者可以通过Java环境变量传递各种资源约束,但这种做法难以适合一个应用跑在多种不同的资源环境上。

自Java17开始,OpenJDK支持CgroupV2。JDK会读取它所在的cgroup配置来感知它自己的CPU和memory配置,而不是从原来的/proc/meminfo和/proc/cpuinfo来获取这些信息。 下面是个最简例子,展示了容器里面的java从Cgroup里读到了CPU和memory的配置。

$ java -XshowSettings:system -version
Operating System Metrics:Provider: cgroupv2Effective CPU Count: 2CPU Period: 100000usCPU Quota: 200000usCPU Shares: 1024usList of Processors: N/AList of Effective Processors, 4 total:0 1 2 3List of Memory Nodes: N/AList of Available Memory Nodes, 1 total:0Memory Limit: 1.00GMemory Soft Limit: 800.00MMemory & Swap Limit: 1.00Gopenjdk version "17.0.2" 2022-01-18
OpenJDK Runtime Environment 21.9 (build 17.0.2+8)
OpenJDK 64-Bit Server VM 21.9 (build 17.0.2+8, mixed mode, sharing 

当然,Java应没有屏去以往的获得资源方式,而且完全兼容,同时也支持通过API获取Cgroup的配置,具体的使用方式请参考相关的技术文档。有了这一能力,Java更贴近云原生,用户体验更好。

小结

本文重点分析Java12到Java17的一些重进展,以及一些现在还没有完全成熟的技术,比如Vector API, GraalVM。从收益来说,免费的性能收益是令人激动的,对云原生的支持也让运维成本大大降小。但除了这两个方面,Java还涉及很多其它的领域,比如编程语言能力的拓展,开发和调试工具的发展,对整个语言生态极其重要。希望有机会再做这些方面的分析和总结。

参考文献

  1. 从 JDK 9 到 19,认识一个新的 Java 形态(内存篇)https://mp.weixin.qq.com/s/cF6JgJIOCF6Jxg520rRbLA
  2. Java 18: Vector API — Do we get free speed-up? https://medium.com/@Styp/java-18-vector-api-do-we-get-free-speed-up-c4510eda50d2
  3. JDK ZGC introduction https://wiki.openjdk.org/display/zgc/Main
  4. 新一代垃圾回收器ZGC的探索与实践 https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
  5. JEP 426: Vector API (Fourth Incubator) https://openjdk.org/jeps/426
  6. JEP 425: Virtual Threads (Preview) https://openjdk.org/jeps/425
  7. 云原生时代,Java 的危与机 https://www.infoq.cn/article/rqfww2r2zpyqiolc1wbe
  8. Java 17: What’s new in OpenJDK's container awareness https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness
  9. NUMA-Aware Memory Allocation for G1 GC https://sangheon.github.io/2020/11/03/g1-numa.html

编辑于 2023-12-29 16:22・IP 属地广东

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/317816.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springboot日志

1、日志用途 故障排查和调试&#xff1a;当项目出现异常或者故障时&#xff0c;日志记录可以快速帮助我们定位到异常的部分以及知道异常的原因。性能监测和优化&#xff1a;通过在关键代码路径中添加日志记录&#xff0c;可以了解应用程序的性能表现&#xff0c;并根据性能表…

四、HTML 属性

属性是 HTML 元素提供的附加信息。 一、HTML 属性 HTML 元素可以设置属性属性可以在元素中添加附加信息属性一般描述于开始标签属性总是以名称/值对的形式出现&#xff0c;比如&#xff1a;name"value"。 二、 属性实例 HTML 链接由 <a> 标签定义。链接的地…

网约车“卷”向:滴滴、T3、麦田商旅们的下一个十年

配图来自Canva可画 近期&#xff0c;东北冰雪大世界、圣索菲亚教堂和中央大街等景点人气“爆棚”&#xff0c;为了方便南方“小土豆”出行&#xff0c;东北多地延长了公交、地铁的运营时间&#xff0c;同时呼吁本市市民文明待客&#xff0c;开网约车的东北大哥都成了“夹子音”…

Python贪吃蛇小游戏(PyGame)

文章目录 写在前面PyGame入门贪吃蛇注意事项写在后面 写在前面 本期内容&#xff1a;基于pygame的贪吃蛇小游戏 实验环境 python3.11及以上pycharmpygame 安装pygame的命令&#xff1a; pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pygamePyGame入门 pygam…

初识STL

目录 ​&#x1f4a1;STL &#x1f4a1;STL六大组件 &#x1f4a1;三大组件介绍 &#x1f4a1;容器 &#x1f4a1;算法 &#x1f4a1;迭代器 &#x1f4a1;示例 &#x1f4a1;STL C STL&#xff08;标准模板库&#xff09;是一套功能强大的 C 模板类&#xff0c;提供了…

IO进程线程 day4 文件IO与目录操作

1.使用标准IO完成两个文件拷贝 #include <head.h> int main(int argc, const char *argv[]) {//判断输入是否合法if(argc>3){printf("输入不合法\n");return -1;}//定义两个文件指针&#xff0c;用于读写FILE *fp1NULL;FILE *fp2NULL;if((fp1fopen(argv[1]…

如何评估 RAG 应用的质量?最典型的方法论和评估工具都在这里了

随着 LLM(Large Language Model)的应用逐渐普及&#xff0c;人们对 RAG(Retrieval Augmented Generation)场景的关注也越来越多。然而&#xff0c;如何定量评估 RAG 应用的质量一直以来都是一个前沿课题。 很显然&#xff0c;简单的几个例子的对比&#xff0c;并不能准确地衡量…

【操作系统xv6】学习记录--实验1 Lab: Xv6 and Unix utilities--未完

ref:https://pdos.csail.mit.edu/6.828/2020/xv6.html 实验&#xff1a;Lab: Xv6 and Unix utilities 环境搭建 实验环境搭建&#xff1a;https://blog.csdn.net/qq_45512097/article/details/126741793 搭建了1天&#xff0c;大家自求多福吧&#xff0c;哎。~搞环境真是折磨…

金和OA C6 UploadFileEditorSave.aspx 文件上传漏洞复现

0x01 产品简介 金和OA协同办公管理系统软件(简称金和OA),本着简单、适用、高效的原则,贴合企事业单位的实际需求,实行通用化、标准化、智能化、人性化的产品设计,充分体现企事业单位规范管理、提高办公效率的核心思想,为用户提供一整套标准的办公自动化解决方案,以帮助…

实现最高效的数据转换:深入了解Achronix JESD204C解决方案

作者&#xff1a;Manish Sinha&#xff0c;Achronix战略规划与业务发展部 长期以来&#xff0c;Achronix为不同行业的数据密集型和高带宽应用提供了创新性的FPGA产品和技术&#xff0c;并帮助客户不断打破性能极限。其中一些应用需要与先进的模拟/数字转换器&#xff08;ADC&a…

UI自动化测试框架搭建

今天给大家分享一个seleniumtestngmavenant的UI自动化&#xff0c;可以用于功能测试&#xff0c;也可按复杂的业务流程编写测试用例&#xff0c;今天此篇文章不过多讲解如何实现CI/CD&#xff0c;只讲解自己能独立搭建UI框架&#xff0c;需要阅读者有一定的java语言基础&#x…

【力扣100】46.全排列

添加链接描述 class Solution:def permute(self, nums: List[int]) -> List[List[int]]:# 思路是使用回溯if not nums:return []def dfs(path,depth,visited,res):# 出递归的条件是当当前的深度已经和nums的长度一样了&#xff0c;把path加入数组&#xff0c;然后出递归if …