11堆

堆、栈、方法区的关系:

HotSpot是使用指针的方式来访问对象:

Java堆中会存放指向类元数据的地址

五、三者的对比分析

三、堆与栈的核心区别

维度 堆(Heap) 栈(Stack)
所属 所有线程共享 每个线程私有
存储内容 对象实例、数组、成员变量 局部变量、对象引用、方法参数
生命周期 随对象是否被引用动态变化(GC管理) 随方法调用结束而销毁
内存管理 由GC自动回收 由JVM自动出栈入栈
访问速度 较慢(需通过引用定位) 高速(连续内存,直接操作)
典型异常 OutOfMemoryError(堆内存不足) StackOverflowError(栈溢出)

5.1 核心区别对比表

为更直观呈现 Heap、Non-Heap 和 Off-Heap 的区别,整理如下对比表:

对比项 Heap(堆内存) Non-Heap(非堆内存) Off-Heap(堆外内存)
存储内容 对象实例和数组 类元数据、JIT 编译代码、线程栈 大块内存缓存、与本地交互的数据堆外内存不受 JVM 直接管理,由OS负责
管理方式 GC 自动管理 JVM 自行管理(部分有有限 GC) 手动管理(或依赖 Cleaner 机制)
常见溢出错误 OutOfMemoryError: Java heap space OutOfMemoryError: MetaspaceOutOfMemoryError: CodeCache is full OutOfMemoryError: Direct buffer memory
调优参数 -Xms、-Xmx、-XX:NewRatio 等 -XX:MaxMetaspaceSize、-XX:ReservedCodeCacheSize 等 -XX:MaxDirectMemorySize

5.2 使用场景建议

  • 优先使用 Heap:对于常规 Java 对象,如业务实体类;生命周期短的临时对象;频繁创建和销毁的数据,堆内存是理想选择,GC 自动管理能减少开发者负担。
  • 考虑 Non-Heap:涉及类元信息(如动态代理生成的类、反射加载的类)、JIT 编译后的代码、线程栈相关场景时,与非堆内存相关。通常无需过多干预,出现内存溢出问题时再针对性处理。
  • 谨慎使用 Off-Heap:在高性能场景,如 Netty 网络框架为提升 I/O 性能;需要与本地代码交互;希望避免 GC 对性能影响的场景下,可使用堆外内存,但务必注意手动管理内存,防止内存泄漏

堆空间概述

Java栈中的引用存储的是指向堆中的对象的地址

  • 一个Java程序运行起来对应一个进程,一个进程对应一个JVM实例,一个JVM实例中有一个运行时数据区,一个运行时数据区中只存在一个堆内存和一个方法区。

  • 一个进程中可以包含多个线程,因此堆内存和方法区是线程共享的。堆中还有一小部分空间,是每个线程独有的,叫做线程私有缓冲区(Thread Local Allocation Buffer,TLAB),解决程序运行时的数据安全问题。

  • 一个线程各自拥有一套Java栈、本地方法栈和程序计数器。

  • 一个进程拥有自己一套jvm;

    • user-service.jar
    • hispital-service.jar
    • 。。。。
    • java -jar xxx.jar: 启动一个进程。 一个进程分配很多线程。
  • 堆是Java内存管理的核心区域,在JVM启动的时候被创建,堆内存的大小是可以调节的。当创建一个引用类型的对象时,JVM会为对象在堆中分配一个内存空间。堆是垃圾回收的重点区域

  • 口诀:

    • 频繁收集年轻代,少量收集老年代,基本不动永久代(方法区)

堆和对象

以前,Java 中“几乎”所有的对象都会在堆中分配,但随着 JIT 编译器的发展和逃逸技术的逐渐成熟,所有的对象都分配到堆上渐渐变得不那么“绝对”了。从 JDK 7 开始,Java 虚拟机已经默认开启逃逸分析了,

意味着如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

img

栈就是前面提到的 JVM 栈(主要存储局部变量、方法参数、对象引用等),属于线程私有,通常随着方法调用的结束而消失,也就无需进行垃圾收集;堆前面也讲了,属于线程共享的内存区域,几乎所有的对象都在对上分配,生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不在被任何变量引用,然后被垃圾收集器回收。

简单解释一下 JIT 和逃逸分析(后面讲 JIT 会细讲)。

常见的编译型语言如 C++,通常会把代码直接编译成 CPU 所能理解的机器码来运行。而 Java 为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由 javac 编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java 可能会干不过 C++ 这类编译型语言。

img

为了优化 Java 的性能 ,JVM 在解释器之外引入了 JIT 编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。

逃逸分析(Escape Analysis)是一种编译器优化技术,用于判断对象的作用域和生命周期。

如果编译器确定一个对象不会逃逸出方法或线程的范围,它可以选择在栈上分配这个对象,而不是在堆上。这样做可以减少垃圾回收的压力,并提高性能。

元空间和方法区

方法区是 Java 虚拟机规范上的一个逻辑区域,在不同的 JDK 版本上有着不同的实现。在 JDK 7 的时候,方法区被称为永久代(PermGen),而在 JDK 8 的时候,永久代被彻底移除,取而代之的是元空间。

img

JDK 7 之前,只有常量池的概念,都在方法区中。

JDK 7 的时候,字符串常量池从方法区中拿出来放到了堆中,运行时常量池还在方法区中(也就是永久代中)。

JDK 8 的时候,HotSpot 移除了永久代,取而代之的是元空间。字符串常量池还在堆中,而运行时常量池跑到了元空间。

元空间的大小不再受限于 JVM 启动时设置的最大堆大小,而是直接利用本地内存,也就是操作系统的内存。有效地解决了 OutOfMemoryError 错误。

运行时常量池

在讲字节码的时候,我们详细的讲过常量池,它是字节码文件的资源仓库,先是一个常量池大小,从 1 到 n-1,0 为保留索引,然后是常量池项的集合,包括类信息、字段信息、方法信息、接口信息、字符串常量等。

运行时常量池,顾名思义,就是在运行时期间,JVM 会将字节码文件中的常量池加载到内存中,存放在运行时常量池中。

也就是说,常量池是在字节码文件中,而运行时常量池在元空间当中(JDK 8 及以后),讲的是一个东西,但形态不一样,就好像一个是固态,一个是液态;或者一个是模子,一个是模子里的锅碗瓢盆。

字符串常量池

  • 在JDK 7及之后的版本中,字符串常量池被移动到了堆内存中(Heap)
  • JDK 7之前,字符串常量池位于永久代(PermGen)

运行时常量池

  • 运行时常量池属于方法区(Method Area)
  • 在JDK 8及之后,方法区由元空间(Metaspace)实现
  • 元空间使用的是本地内存(直接从操作系统申请),而不是堆内存

具体来说:

  • JDK 7:字符串常量池移到堆中,运行时常量池仍在方法区(永久代)
  • JDK 8及之后:永久代被彻底移除,改用元空间(Metaspace)作为方法区的实现,运行时常量池在元空间中,而字符串常量池仍在堆中

所以,当前(JDK 8+)环境下,字符串常量池确实在堆里面,而运行时常量池在方法区(具体实现为元空间,使用本地内存,不是堆内存)。

这是JDK 7版本的重要改进,将字符串常量池从永久代移到堆中,是为了让字符串常量池能更好地参与垃圾回收,避免永久代内存不足导致的OOM问题。

字符串常量池

字符串常量池我们在讲字符串的时候已经详细讲过了,它的作用是存放字符串常量,也就是我们在代码中写的字符串。依然在堆中。

OK,方法区(不管是永久代还是元空间的实现)和堆一样,是线程共享的区域。

运行时数据区的主要组成

来总结一下运行时数据区的主要组成:

PC 寄存器(PC Register),也叫程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。

JVM 栈(Java Virtual Machine Stack),与 PC 寄存器一样,JVM 栈也是线程私有的。每一个 JVM 线程都有自己的 JVM 栈(也叫方法栈),这个栈与线程同时创建,它的生命周期与线程相同。

本地方法栈(Native Method Stack),JVM 可能会使用到传统的栈来支持 Native 方法的执行,这个栈就是本地方法栈。

堆(Heap),在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。

方法区(Method area),JDK 8 开始,使用元空间取代了永久代。方法区是 JVM 中的一个逻辑区域,用于存储类的结构信息,包括类的定义、方法的定义、字段的定义以及字节码指令。不同的是,元空间不再是 JVM 内存的一部分,而是通过本地内存(Native Memory)来实现的。

在 JVM 启动时,元空间的大小由 MaxMetaspaceSize 参数指定,JVM 在运行时会自动调整元空间的大小,以适应不同的程序需求。

堆的分代

image-20251114202938085

新生代

内存结构:

  • 新生代又分为两部分: 伊甸园区(Eden space)和幸存者区(Survivor pace)

  • 幸存者区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)

工作过程:

(1)新创建的对象先放在伊甸园区。

(2)当伊甸园的空间用完时,程序又需要创建新对象,此时,触发JVM的垃圾回收器对伊甸园区进行垃圾回收``(Minor GC/Young GC),将伊甸园区中不再被引用的对象销毁。(GC 伊甸园+某个幸存者区)

(3)然后将伊甸园区的剩余对象移动到空的幸存0区

(4)此时,伊甸园区清空

(5)被移到幸存者0区的对象上有一个年龄计数器,值是1

(6)然后再次将新对象放入伊甸园区。

(7)如果伊甸园区的空间再次用完,则再次触发垃圾回收对伊甸园区和s0区进行垃圾回收,销毁不再引用的对象。

(8)此时s1区为空,然后将伊甸园区和s0区的剩余对象移动到空的s1区

(9)此时,伊甸园区和s0区清空

(10)从伊甸园区被移到s1区的对象上有一个年龄计数器,值是1。从s0区被移到s1区的对象上的年龄计数器+1,值是2。

(11)然后再次将新对象放入伊甸园区。如果再次经历垃圾回收,那么伊甸园区和s1区的剩余对象移动到s0区。对象上的年龄计数器+1。

(12)当对象上的年龄计数器达到15时(-XX:MaxTenuringThreshold),则晋升到老年代。

总结: 针对幸存者s0,s1,复制(复制算法)之后有交换,谁空谁是to

每次垃圾回收后,幸存者区都有一个会被清空,此时这个区域称为to区,另一个区域被称为from区,下一次垃圾回收时,伊甸园区和from区对象会被移动到to区

老年代

经历多次Minor GC仍然存在的对象(默认是15次)会被移入老年代,老年代的对象比较稳定,不会频繁的GC。

若老年代也满了,那么这个时候将产生Major GC(同时触发Full GC),进行老年代的垃圾回收。

若老年代执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常OutOfMemoryError

永久代/元空间

JDK1.7

  • -XX:PermSize:设置永久代初始分配空间,默认值是20.75M。

  • -XX:MaxPermSize:设置永久代最大可分配空间,32位机器默认值是64M,64位机器默认82M。

JDK1.8

  • -XX:MetaspaceSize:设置元空间初始分配空间,64位系统,默认值是21M。

  • -XX:MaxMetaspaceSize:设置元空间最大可分配空间,,默认是-1。

永久代是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的关闭 JVM 才会释放此区域所占用的内存

如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。

尽管方法区在逻辑上属于堆的一部分,对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

于HotSpot虚拟机,很多开发者习惯将方法区称之为永久代 ,但严格说两者不同,或者说是使用永久代来实现方法区而已。

常用命令行(了解)

查看java进程:jps -l

查看某个java进程所有参数:jinfo 进程号

查看某个java进程总结性垃圾回收统计:jstat -gc 进程号

Java的垃圾回收

什么是GC

Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是

GC: 内存中对象的分配与回收。

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)

从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。

GC种类

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC):收集整个 Java 堆和方法区。

GC种类总结:

部分收集:

  • 年轻代收集(Minor GC / Young GC):新生代垃圾收集(伊甸园区 + 幸存者区)
  • 老年代收集(Major GC / Old GC):老年代垃圾收集
  • Full GC = Minor + Major
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代。G1垃圾收集器有这种方式

整堆收集(Full GC):

  • 整个Java堆方法区的垃圾收集

年轻代GC触发机制(Minor GC ):

年轻代的Eden空间不足,触发Minor GC。

每次Minor GC在清理Eden的同时会清理Survivor From区。

Minor GC非常频繁回收速度块

引发STW(Stop The World),暂停其他用户线程,垃圾回收结束,用户线程恢复。

老年代GC触发机制(Major GC 和 Full GC):

老年代满了,对象从老年代消失是因为发生了Major GC和Full GC。

Major GC比Minor GC速度慢10倍以上,STW时间更长

如果Major GC后,内存还不足,就报OOM。

Full GC触发机制:

(1)调用System.gc(),系统建议执行Full GC,但是不是必然执行。

(2)老年代空间不足

(3)方法区空间不足

(4)通过Minor GC后进入老年代平均大小大于老年代可用内存

  • 频繁在新生代收集,很少在养老区收集,几乎不在永久区/元空间收集。

    特性 Minor GC Major GC Full GC
    作用区域 仅年轻代 (Young Gen) 仅老年代 (Old Gen) 整个堆 (Young + Old + Metaspace)
    触发频率 非常低 (应尽量避免)
    执行速度 非常慢
    STW停顿 短,通常可忽略 较长,影响明显 很长,严重影响应用
    触发原因 Eden区满 老年代满 1. 老年代满 2. 元空间满 3. System.gc() 4. 空间分配担保失败

    为什么要“担保”?

    想象一个场景:你要进行Minor GC了。理想情况下,Eden区里大部分对象都是垃圾,被回收掉,只有一小部分存活对象会晋升到老年代。

    最坏的情况是:Eden区里几乎所有对象都存活了,这次Minor GC后,存活对象的总大小非常大,需要全部晋升到老年代。

    “空间分配担保”机制就是为了应对这种最坏情况,确保老年代有足够的能力“兜底”,接收所有这些可能晋升上来的对象。如果老年代没能力兜底,冒然进行Minor GC就会导致内存不足的错误。

    “允许担保”的含义

    -XX:-HandlePromotionFailure 这个参数的名字直译是“是否处理晋升失败”。

    • -XX:+HandlePromotionFailure允许担保(JDK 6 Update 24之后默认即为此状态)。JVM会尝试做担保检查,如果检查失败,就提前进行Full GC来腾空间。
    • -XX:-HandlePromotionFailure不允许担保。JVM直接不做复杂的检查了,只要老年代剩余空间看起来不太够,就干脆地、安全地先进行一次Full GC。

    这个参数在JDK 6 Update 25之后就不再有实际作用了,规则已经固化

垃圾判断算法

引用计数算法

引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。

如果该对象被其它对象引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减 1,当该对象的引用计数为 0 时,那么该对象就会被回收。

可达性分析算法

可达性分析算法(Reachability Analysis)的基本思路是,通过 GC Roots 作为起点,然后向下搜索,当一个对象到 GC Roots 之间没有任何引用相连时,即从 GC Roots 到该对象节点不可达,则证明该对象是需要垃圾收集的。

GCROOTS

所谓的 GC Roots,就是一组必须活跃的引用,不是对象,它们是程序运行时的起点,是一切引用链的源头。在 Java 中,GC Roots 包括以下几种:

  • 虚拟机栈中的引用(方法的参数、局部变量等)
  • 本地方法栈中 JNI (本地方法)的引用
  • 类静态变量
  • 运行时常量池中的常量(String 或 Class 类型)

垃圾回收算法

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收。

在介绍JVM垃圾回收算法前,先介绍一个概念:Stop-the-World:

Stop-the-world意味着 JVM由于要执行GC而停止了应用程序的执行,并且这种情形会在任何一种GC算法中发生。

当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成

事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有高吞吐 、低停顿的特点。

标记清除(Mark-Sweep)

标记-清除算法是几种GC算法中最基础的算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。正如名字一样,算法分为2个阶段

(1)标记: 使用可达性分析算法,标记出可达对象。

(2)清除: 对堆内存从头到尾进行线性便遍历,如果发现某个对象没有被标记为可达对象,则将其回收。

缺点:

  • 效率问题(两次遍历)

  • 空间问题(标记清除后会产生大量不连续的碎片。JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。)

复制算法(Copying)

核心思想:

将活着的内存空间平均分成两块,每次只使用其中一块,垃圾收集时,将正在使用的内存中的存活对象复制到未被使用的内存块中,然后将之前的内存块清空,交换两个内存的角色,循环下去。

优点:

  • 实现简单

  • 不产生内存碎片

缺点:

  • 内存缩小为原来的一半,浪费了一半的内存空间,代价太高,所以在老年代一般不能直接选用这种算法。

  • 如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

年轻代中使用的是Minor GC,这种GC算法采用的就是复制算法:

HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法。

标记压缩(Mark-Compact)

也叫标记整理算法。

标记整理算法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存

优点:

标记整理算法不仅可以弥补标记清除算法中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。

缺点:

如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。

老年代一般是由标记清除或者是标记清除与标记整理的混合实现。

难道就没有一种最优算法吗?

回答:无,没有最好的算法,只有最合适的算法。==========>分代收集算法

分代收集算法(Generational-Collection)

执行速度:

复制算法 > 标记清除算法 > 标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。

内存利用率:

标记整理算法=标记清除算法>复制算法。

分代收集算法:

  • 将JVM管理的运行时数据区整片内存划分不同区域,垃圾回收工作在堆区
  • 堆进行分代管理
    • 新生代:复制(第一优先) + 标记清除 算法; 新生代GC频率高,所以必须选择执行速度快的算法。
    • 老年代:标记压缩算法; 老年代GC频率低,都是长期存活的对象,我们就一次性摆好位置。 标记压缩算法

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存

为了尽量兼顾上面所提到的三个指标,标记整理算法相对来说更平滑一些,但效率上依然不尽如人意。

比复制算法多了一个标记的阶段,又比标记清除多了一个整理内存的过程

分代回收算法实际上是复制算法和标记整理法的结合,并不是真正一个新的算法。

一般分为老年代(Old Generation)和年轻代(Young Generation)

老年代就是很少垃圾需要进行回收的,年轻代就是有很多的内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。

年轻代(Young Gen)(1/3,8:1:1)

年轻代特点是区域相对老年代较小,对像存活率低。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代(Tenure Gen)(2/3)

老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。(大对象可能会直接进入老年代)

老年代的特点是区域较大,对像存活率高。

这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现

四种引用

平时只会用到强引用和软引用。

强引用:不回收

只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

1
User obj = new User();  //User obj = 0x7788;     [0x7788] = new Object();

软引用:内存不足即回收

SoftReference 类实现软引用。在系统要发生内存溢出(OOM)之前,才会将这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。软引用可用来实现内存敏感的高速缓存

1
SoftReference<User> userSoftRef = new SoftReference<>(new User(1,"tom"));

弱引用:发现即回收

WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集(GC)之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

1
WeakReference<User> userWeakRef = new WeakReference<>(new User(1, "tom"));

虚引用(幽灵引用、幻影引用):对象回收跟踪

PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

1
2
ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<User> obj = new PhantomReference(new User(1, "tom"), phantomQueue);

end


11堆
http://example.com/2025/11/14/11堆/
作者
無鎏雲
发布于
2025年11月14日
许可协议