JVM常用概念之扁平化堆容器
扁平化堆容器是OpenJDK Valhalla 项目提出的,其主要目标为将值对象扁平化到其堆容器中,同时支持这些容器的所有指定行为,从而达到不影响原有功能的情况下,显著减少内存空间的占用(理想条件下可以减少24倍)。
1.前言
1.1.容器
Java 变量可以是堆对象内的字段,也可以是堆数组内的数组元素;这些是可能的堆变量。还有非堆变量:它们是线程堆栈上活动框架内的局部变量或堆栈元素。
容器只是 Java 语言或 Java VM 定义的变量的实现。
容器这个术语关注的是变量的底层、具体的实现特征,而不是其逻辑的、指定的行为。
所有 Java 和 VM 变量(除原始类型的变量外,如int)都指定为引用,并且必须按引用的方式运行。除非有任何特殊约定,否则它们将初始化为null,并以指针的形式读取和写入。如果将它们类型化为基于值的类(或 Valhalla 值),则即使在竞争条件下,它们的连续值也会表现出完全一致的行为。
尽管其逻辑行为使变量看起来好像只包含对一系列连续分配的对象(和/或 null)的单个引用,但 Valhalla 允许将变量底层的容器展平。尽管它的行为类似于指针,但它的格式类似于 int 变量。如果值有多个字段,它的行为就像几个变量(int、引用等);这些变量彼此靠近存储,并且都在展平的容器内。容器可能还具有表示空引用的逻辑存在的策略,即使容器在物理上根本没有引用。(在 Valhalla 之前,每个变量的逻辑特征及其类型或多或少直接决定了容器的物理结构。(也有例外,例如标量化优化,它会将堆对象重新组织到线程寄存器中。)这就是为什么 “容器”一词主要在 Valhalla 的设置中有用。)
我们不会过多谈论非堆容器的实现,因为它们由 JIT 代码或解释器管理。幸运的是,它们永远不会受到竞争条件的影响,因为这样的容器仅由创建它的线程使用。您可以将值的非堆容器视为多个机器寄存器和/或线程堆栈位置,通常彼此不相邻。
位于堆中意味着容器位于 Java 堆上的封闭存储块、对象或数组中。通常,存储块的标识是动态计算的。(静态字段是例外;它们的包含块是常量。)但块内的容器必须具有相对于块的可预测位置。它还必须具有可预测的静态定义格式,以便从在多个线程中运行的多个代码位置高效地访问值。与格式(对于任何给定的容器)相关联的是访问方法,用于将值对象(或 null)写入或读出(或可能是 CAS)堆容器的过程。
具体来说就是堆位置是动态计算的。除静态字段外,任何给定的访问都可能是对许多动态选择的封闭对象或数组之一的访问。相比之下,堆栈或局部变量是线程私有的,只能通过一个方法调用访问,并且位于堆栈框架内,激活后不再进行进一步的动态选择。
并且堆位置是静态格式化的。为了使访问方法平等地应用于许多动态选择的位置,所有这些位置必须具有通用布局,以便访问方法能够工作。这要求 VM 发明此类布局(与访问方法相结合)并将它们适当地分配给不同类型的容器。请注意,final、volatile 或 normal 值字段可能具有不同的布局和访问方法。此外,可空和非可空值容器可能具有不同的布局和访问方法。根据约定,每个不同的堆栈或局部变量都可以由特定于该变量的 JIT 代码直接在寄存器或控制堆栈位置中操作。局部变量需要布局(或访问方法)之类的东西的唯一时间是当它必须跨越方法调用边界(作为参数或返回值)并且方法调用未内联时。然后应用 VM 的调用序列规则。
那什么是访问方法?访问方法可以被认为是 VarHandle 方法的底层体现。它可以读取、写入,甚至 CAS 容器中的值。访问方法需要(作为参数)包含容器的存储块的基地址。如果容器是数组,则访问方法还需要索引来识别所有数组元素中的哪个容器。
最后让我们回顾在 Valhalla 之前,堆容器只有少数几种固定格式。如果容器的类型不是原语(字节、双精度等),它只是指向另一个堆对象的指针(当然,也可以是空值)。因此,每个 Valhalla 之前的堆容器都可以非常舒适地装入机器字中。这种轻松和舒适是以指针在多个堆块中移动为代价的。即使一个对象只有一个字节字段,将其放入另一个对象的容器中也是一件大事,可能需要 64 位用于容器在堆中保存指向它的指针,还需要另外 128 位用于分配堆块以包含该字节字段。Valhalla 可以简单地将单个“有效负载”字节放入容器中,而无需指针或块,从而将占用空间减少高达 24 倍(在这种极端情况下)。
1.2.一致性
Java 语言和 VM 都保证从变量读取始终保持一致,这是 Java 内存模型定义的特定意义,除非存在降低一致性的特殊规定(在 Valhalla 之前这是不可能的)。如果引用存储在变量中,则只能观察到与先前引用相关联的类字段组合。没有办法“凭空”创建一个引用,或者以某种方式混合其他两个引用的特征。即使多个线程争相读取变量中的值,变量也必须将每个连续的变量值与所有其他值分开。
在 Valhalla 中,当容器在物理上被扁平化时,事情会变得更加困难。Valhalla 值在逻辑上仍然是存储在变量中的引用。虚拟机字节码指令将每个连续的值作为单个自洽对象引用存储到变量中。但是,如果多字段值类在容器中被扁平化,则值的物理字段可能会通过单独的硬件指令分别写入容器的存储中。如果线程同时读取和写入容器,则某个线程可能会观察到一个值包含从不同逻辑值获得的不同物理字段值的混合,这些值是在不同的时刻写入的。这种现象称为逐字段撕裂。这是 Java 内存模型保证的一致性的一种特殊故障。
因此,一个非常合理的物理活动可能会产生一个不合理的逻辑结果,即一个“撕裂”的值类实例,凭空而来,没有人真正构建过,而且(大概)没有人愿意看到。我们确实希望声称扁平化容器是对基于值的类的优化,现在成为值类。但如果它通过逐个字段撕裂凭空创建新值,那么这种优化就会被破坏。
值得注意的是这里没有任何内容表明单个字段可能因竞争而损坏,无论是在 Valhalla 之前还是之后。值或任何其他对象内的单个字段值永远不会因竞争而撕裂。作为一个狭隘的例外,可变的 long 或 double 字段可能会显示字撕裂,但这对于值来说是不可能的,因为 long 或 double 将存储在值类的安全发布的最终字段中。同样,如果对象未安全发布,字段的预初始化值(空或零)也可能会在竞争下泄漏,这是一种看起来像字段撕裂的不一致。但对于值来说,这同样是不可能的,因为发布规则对它们更加严格。这些限制与当前对灵活构造函数主体的工作相结合;值必须始终在调用 super 之前初始化其字段,而不是之后。
完全一致性是指多字段值对象的所有字段必须作为一个单元进行读取和写入,即整个值对象。(也就是说,值对象的竞争不会比类似的基于值的类对象多。)完全一致性的反义词是字段松散一致性,或简称为松散一致性,这是 Valhalla 中新增的一个容器属性,它允许竞争条件混合和匹配来自不同逻辑写入的不同物理字段值。
在 Valhalla 之前,读取或写入类值对象([基于值的类])的唯一方法是加载或存储单字机器地址,即指向类值对象的不变实例的指针。(它也可以为空。)Java 内存模型 (JMM) 规定,不可能通过这种加载的指针读取不一致的字段值。因此,所有 Valhalla 之前的类值对象都作为一致单元进行读取和写入(通过物理指针)。在 Valhalla 之前始终存在完全一致性。
关于一致性可以详细阐述的还是有很多,正如上述所示,这就是为什么 Valhalla 之前的规范没有过多谈论一致性(我们在这里使用该术语的方式)。一个例外是长整型和双整型基元的字拆分,这是 Valhalla 中字段拆分的前身。字拆分的工作方式就像将长整型作为一个值类分成两个 32 位字段,然后对其进行逐字段拆分一样。
那么,如果不让值看起来不一致,Valhalla 之前的竞争还能做什么呢?有两点:不同的线程可以在不同的时间读取不同的指针值,并且值的出现顺序可能并不总是完全合理。此外,如果引用指向可变对象(而不是基于值的类),则对该对象字段的读取可能会变得彼此不一致,这仅仅是因为竞争线程可能会读取一个字段的“旧”值和另一个字段的“新”值,尽管某种方法正在努力同时一致地更新字段。
具体来说,想象一个堆中的复数值对象,由具有两个字段 {re=0,im=1} 的“旧”写入写入。现在想象一个存储 {re=2,im=3} 的“新”写入,以及读取变量的不同线程。该线程可以观察到什么?至少,我们不会期望“凭空而来”的结果:永远不会有 re=99,而只有 re=0 或 re=2,对于 im=1、im=3 也是如此。但在某些硬件上,如果不特别注意一致性,线程可能会读取旧字段值和新字段值的“撕裂”混合:{re=0,im=3} 或 {re=2,im=1}。请注意,撕裂的值 {re=0,im=3} 从未写入过,因此如果应用程序可以读取它,那就有些可疑了,而且撕裂的值可能会间歇性出现,这甚至更可疑。虽然对于复数来说这可能是可以接受的行为,但这种逐字段撕裂可能会破坏具有微妙内部不变量的值对象的完整性。 (例如,间隔对象可能要求其结束字段大于其起始字段,但撕裂会破坏这个不变量。)这就是为什么 Valhalla 中的值默认是完全一致的。
在硬件层面,现代 CPU 承诺在一条指令中加载或存储(甚至 CAS 操作)值时,将原子性保持在 64 位(对齐)以内,有时保持在 128 位(再次对齐)以内。这与我们的一致性概念有关:如果您将机器字视为由字节构成的值(好像每个字节都是一个值字段),则原子加载或存储将保持完全一致性:即使在最具挑战性的竞争条件下也不会出现“字节撕裂”。当读取和写入是原子时,它们不会在读取或写入字时拆分字,并且字的所有位和字节都会同时更改,无论哪个线程正在观察该字。但是,如果两个 32 位写入指令将两个字段存储到一个 64 位变量中,则无法保证原子性,并且可能会发生某种字节撕裂。
现代 CPU 并不保证任何多条写入指令序列的原子性。通常没有适用于多条写入指令的“锁定前缀”,以使它们的内存效果成为全有或全无的事务。英特尔的 TSX 功能(据称可以做到这一点)显然没有成功。我们只能使用 64 位原子性,或者最多(并且有性能风险)128 位原子性。
在 Valhalla 原型中,实验性值类注释 @LooselyConsistentValue 拒绝完全一致性,允许 VM 使用更快的访问方法,但可能会导致字段撕裂。
1.3.什么是扁平化?
扁平化是对容器的任何重组,它用一组直接包含值数据的子字段替换单个指针,指向其他地方的某些值数据。
也就是说每个子字段都实现了值的字段。之所以称为子字段,是因为值本身位于某个较大对象的字段中。子字段是字段的字段。如果声明值包含值,则可以嵌套此关系。但在某种意义上,子字段相当于顶级包含对象的字段,并且它属于存储在对象中的扁平值这一事实是一种观点。
举例说明,示列代码如下:
value record Complex(double re, double im) { }
class TwoValues {Complex a, b;// layout: // a: {double, double}// b: {double, double}// null channels (explained elsewhere):// a.NC: byte, b.NC: byte;
}
此示例对象的布局与具有四个双精度字段和两个字节字段的 Valhalla 前期对象的布局大致相同。但由于它嵌套了值,因此它包含两个容器,每个容器都被展平为两个子字段和一个合成空通道。
由于多种原因,扁平化堆容器对 VM 实现者来说是一个挑战。光是记录在哪里使用哪种格式就已经够难的了。让加载和存储的速度至少与“经典”基于指针的加载和存储一样快是件棘手的事。尽量减少碎片损失(以免削弱 Valhalla 减少占用空间的承诺)也是一个挑战。多个线程对扁平表示的访问必须服从现有允许的竞争(最多没有新的竞争),这是一个非常微妙的挑战。
但是,当包含的值对象包含大约一个机器字的字段状态时,扁平化尤其棘手,并且容器承诺值的字段保持一致(即值以原子方式更新),并且容器允许空引用作为与值本身的任何实例不同的状态。
这听起来像是一系列不太可能发生的不幸事件,但实际上这很可能是常见的情况。假设 Long 被迁移为 Valhalla 值类型。(如果不是,假设用户定义一个与 Long 一样的值类型。无论如何,假设我们在一台具有 64 位原子内存操作的机器上。)任何 Long 字段或数组元素,如果要展平,都需要表示 Long.value 字段的所有 2^64 个值,该字段的类型为 long(请注意小写 l)。
可以想象空引用到底有多难?当值类的非空实例完全耗尽了我们希望使用的容器格式的表示能力时,空引用就会出现问题。我们使用 Java 类型 long 的运行示例,它有 2^64 个值,没有一个与空值相同。因此,一个 long 变量(如果不是可空的)可以很轻松地放入 64 位机器字中。可空的 long(大写 Long)就不那么合适了,因为它必须表示一个额外的可能值,与所有 2^64 个非空值不同。在这种情况下,我们需要找到一种方法来适应空值。
2.项目进度
截止2024年,经过多次迭代,Valhalla 草案设计(部分定义见JEP 401)允许容器(字段或数组元素)声明它们是否容忍 null,至少在它们被声明为值类时是这样。也就是说,所有值类都支持该类的可空和无空变量。值类还可以声明其实例是否容忍额外的逐字段撕裂竞争,这是对 VM 的让步,使布局值变得更容易“就像结构一样”。当然,每个值类也否认任何对象身份的要求,这是扁平化的先决条件。
空值限制字段使用字段注释进行声明,空值限制数组使用特殊工厂进行创建。容忍字段撕裂的类使用类注释进行声明。这些功能是临时的、私有的权宜之计,用于在 JDK 中进行实验。在更成熟的 Valhalla 版本中,它们可能会被用户可见的语言功能取代。具体来说,空值限制可以表示为一种称为“类型限制”的新型正式字段属性,它接受字段的声明类型,但随后指定禁止该类型允许的某些值(例如空值)的其他约束。
2.1 复数示例
松散一致(逐字段可撕裂)值类的一个简单示例是 Complex。在 C 等语言中,复数值通常会在多线程竞争下撕裂:竞争可能会混合旧的实数和新的虚数分量,反之亦然。这在数字代码中几乎从来都不是问题,但在数字代码中,防止撕裂的成本几乎总是不受欢迎的。因此,Java Complex 类很可能以一种容忍对其实数和复数分量进行“撕裂”更新的方式声明。
2.2.不容忍的示例
不能容忍松散一致性的值类的示例是内存引用对象(如 Panama FFM API 中的对象),它由基地址和访问该地址内存的安全大小组成。(大小用于强制性安全检查,将越界访问转换为 Java 异常,而不是内存故障。)将新的更大大小与旧基地址相结合的恶意混合可能允许 Java 代码使用该对象读取或写入越界内存地址。一个更简单的例子是具有两个界限的范围数据类型,其中上限必须始终超过下限;恶意混合可能会创建向后范围。
请注意,即使值类(例如Complex)声明它容忍松散一致性,该类的变量也可以使用 Java 关键字声明volatile。这将禁用该特定容器的撕裂,无论该类的容忍度如何。声明的变量会不惜volatile一切代价保持字段一致性,并确保对重新排序的额外约束。这些约束是并发控制的一个独特方面,在机器代码中通过使用隔离和禁用某些重新排序优化来体现。
与扁平化和可空性相关的联锁选项是 Valhalla 用户模型的一部分,如下图所示。

草案设计还允许将 final 字段排除在竞争条件之外,足以让它们即使在非 final 字段不容易展平的设置中也能展平。这要求 final 字段必须比 Valhalla 之前的设计“锁定”得更严格一些,使用严格 final 字段的概念(标记为 ACC_STRICT)。VM 可以证明严格字段永远不会被观察到发生变化,即使在构造封闭对象时也是如此。
相关文章:
JVM常用概念之扁平化堆容器
扁平化堆容器是OpenJDK Valhalla 项目提出的,其主要目标为将值对象扁平化到其堆容器中,同时支持这些容器的所有指定行为,从而达到不影响原有功能的情况下,显著减少内存空间的占用(理想条件下可以减少24倍)。…...
python面试题5:浅拷贝和深拷贝之间有什么区别?(难度--中等)
文章目录 题目回答1.浅拷贝2.深拷贝 题目 浅拷贝和深拷贝之间有什么区别? 回答 1.浅拷贝 浅拷贝对于不可变数据,如字符串,整数,数组,往往是直接复制其的值。对于可变对象如列表,则是指向同一个地址。这…...
Jetson Linux 上安装ZMQ
1. 安装ZMQ 框架 apt-get install libzmq3-dev 2. 或者自己build ZMQ https://github.com/zeromq/libzmq.git 参考官网教程 3. 安装CPPZMQ CPPZMQ 是ZMQ 的友好的C封装,只需要一个zmq.hpp 头文件即可 git clone https://github.com/zeromq/cppzmq.git cd cppz…...
【Pycharm】设置双击打开文件
概要 习惯真可怕。很多小伙伴用习惯了VsCode开发,或者其他一些开发工具,然后某些开发工具是单击目录文件就能打开预览的,而换到pycharm后,发现目录是双击才能打开预览,那么这个用起来就特别不习惯。 解决办法 只需一…...
Web前端后端架构:构建高效、稳定与可扩展的互联网应用
Web前端后端架构:构建高效、稳定与可扩展的互联网应用 在构建互联网应用的过程中,Web前端与后端架构的设计与实施至关重要。一个优秀的架构能够确保应用的稳定性、高效性和可扩展性,为用户提供流畅、安全的体验。本文将从四个方面、五个方面…...
数据仓库核心:事实表深度解析与设计指南
文章目录 1. 引言1.1基本概念1.2 事实表定义 2. 设计原则2.1 原则一:全面覆盖业务相关事实2.2 原则二:精选与业务过程紧密相关的事实2.3 原则三:拆分不可加事实为可加度量2.4 原则四:明确声明事实表的粒度2.5 原则五:避…...
Reactor和epoll
Reactor模式和epoll都是与事件驱动的网络编程相关的术语,但它们属于不同的概念层面: Reactor模式 Reactor模式是一种事件驱动的编程模型,用于处理并发的I/O事件。这种模式使用一个或多个输入源(如套接字),…...
Mybatis-Plus多种批量插入方案对比
背景 六月某日上线了一个日报表任务,因是第一次上线,故需要为历史所有日期都初始化一次报表数据 在执行过程中发现新增特别的慢:插入十万条左右的数据,SQL执行耗费高达三分多钟 因很早就听闻过mybatis-plus的[伪]批量新增的问题&…...
数据库面试
1. 简单介绍一下Spring中的事务管理。 答:事务就是对一系列的数据库操作(比如将insert,delete,update,select多条sql语句)作为一个整体执行,进行统一的提交或回滚操作,如果这组sql语…...
探索Web Components
title: 探索Web Components date: 2024/6/16 updated: 2024/6/16 author: cmdragon excerpt: 这篇文章介绍了Web Components技术,它允许开发者创建可复用、封装良好的自定义HTML元素,并直接在浏览器中运行,无需依赖外部库。通过组合HTML模…...
摄影师在人工智能竞赛中与机器较量并获胜
摄影师在人工智能竞赛中与机器较量并获胜 自从生成式人工智能出现以来,由来已久的人机大战显然呈现出一边倒的态势。但是有一位摄影师,一心想证明用人眼拍摄的照片是有道理的,他向算法驱动的竞争对手发起了挑战,并取得了胜利。 迈…...
CMU最新论文:机器人智慧流畅的躲避障碍物论文详细讲解
CMU华人博士生Tairan He最新论文:Agile But Safe: Learning Collision-Free High-Speed Legged Locomotion 代码开源:Code: https://github.com/LeCAR-Lab/ABS B站实际效果展示视频地址:bilibili效果地址 我会详细解读论文的内容,让我们开始吧…...
Spring中自定义注解进行类方法增强
说明 说到对类方法增强,第一时间想到自定义注解,通过aop切面进行实现。这是一种常用做法,但是在某些场景下,如开发公共组件,定义aop切面可能不是最优方案。以后通过原生aop方式,自定义注解,对类…...
TS:元组
问: 解释下什么是元组 回答: 元组(Tuple)是一种数据结构,类似于数组,但与数组不同的是,元组中的元素类型可以各不相同,且元组的长度是固定的。元组在许多编程语言中都有实现,包括 TypeScript…...
微服务 | Springboot整合Dubbo+Nacos实现RPC调用
官网:Apache Dubbo 随着互联网技术的飞速发展,越来越多的企业和开发者开始关注微服务架构。微服务架构可以将一个大型的应用拆分成多个独立、可扩展、可维护的小型服务,每个服务负责实现应用的一部分功能。这种架构方式可以提高开发效率&…...
读书的意义
...
第66集《摄大乘论》
请大家打开《讲义》第二二二页: 庚九、念(分二:辛一正念法身;辛二兼显净土) 辛一、正念法身(分二:壬一征;壬二释) 壬一、征 这个是讲到十门分别(二0三页),分别清净法身的第九段,讲到念&…...
VMware 桥接网络突然无法上网
VMware 桥接网络突然无法上网 0. 问题1. 解决方法 0. 问题 昨天,VMware 桥接网络正常使用,今天突然无法上网。 1. 解决方法 打开VMware的虚拟网络编辑器,将桥接模式的网络从“自动”改成你要使用的网卡,问题解决。 完成&#…...
面试题——Redis
★1.简述一下缓存穿透,缓存击穿,缓存雪崩 ? 缓存穿透:大量恶意请求一个不存在的数据,使得压力绕过Redis缓存层打到数据库,造成数据库瘫痪 处理:①设置黑名单,维护一个可能存在也可能不存在的黑名单数据列表,对请求进行过滤(简单高效) ②布隆过滤器,会出现误删,且相对麻烦(不…...
Java——构造器(构造方法)和 this
一、什么是构造器 构造器(Constructor)是Java类的一种特殊方法,用于初始化对象的状态。构造器在创建对象时被调用,可以对对象的成员变量进行初始化。 我之前的文章《Java——类和对象-CSDN博客》中也提到了构造器。 二、构造器…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...
K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
#Uniapp篇:chrome调试unapp适配
chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器:Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...
