Java 垃圾回收,看一遍就懂

news2024/11/23 23:24:23

了解 Java 垃圾收集的工作原理并优化应用程序中的内存使用情况。详细了解 Java 中内存管理的复杂性。

f4470e4fe2894c76a7059c7df6654381.png
 

垃圾收集是一个关键过程,可以帮助任何Java 开发公司。编程语言中的这一强大功能可以巧妙地管理内存分配和释放,防止内存泄漏并优化资源利用率。它就像一个坚定的看门人,勤奋地清理未使用的对象,使我们免于被不必要的数字杂乱所淹没。作为一家 Java 开发公司,我们在编码工作中经常遇到这种挑战,而垃圾收集为此提供了一个优雅的解决方案。让我们深入研究一下这个复杂的机制。

GC 是 Java 编程中的无名英雄,它不仅为我们清理垃圾,还使 Java 内存更加高效。它至关重要,因为作为程序员,我们需要通过释放资源(尤其是未引用的对象)来管理好内存,而我们在寻求新想法时经常会忘记它(有时是因为懒惰)。

Java 垃圾回收的工作原理

让我们来研究一下这个安静清洁器在 Java 中的具体功能。

Java 中的垃圾收集是一个通过回收不再使用的未使用对象来自动管理内存的过程。

内存结构

在Java中,内存分为栈内存、堆内存、元空间,我们来详细了解一下。

堆栈内存

堆栈内存是方法执行期间存储局部变量、对象引用、方法参数和其他方法特定数据的内存区域。堆栈内存的大小是固定的。

堆内存

堆内存是用于存储实际对象的内存区域。其大小是固定的,并且可以根据需要动态增大或缩小。

这是一个例子。

Integer num = new Integer(12);

这会在堆栈内存中创建一个“num”变量,并在堆内存中创建一个新的 Integer 对象。堆栈内存中的“num”变量存储对原始对象的引用。

4e2b9b5a261c41da985a8336666faf6e.png

元空间

元空间是 Java 虚拟机 (JVM) 使用的本机内存的一部分,用于存储有关类和静态方法的元数据。它取代了永久代堆内存,后者是堆内存的一部分。

在早期版本的 Java 中,PermGen 用于存储有关类和静态方法的元数据。但它有一些限制。其中之一是它们的大小是固定的。另一个问题是 PermGen 空间与堆的其余部分一起被垃圾收集,这导致了性能问题。

Metaspace 可动态调整大小,并单独进行垃圾回收。它允许在多个 JVM 实例之间共享类元数据,从而减少内存使用量并提高性能。

垃圾收集资格

活动可访问对象是被程序的某些部分引用的对象,而死亡或不可访问对象是未被程序的任何部分引用的对象。例如:

Integer num = new Integer(12);
num = null;

如上所述,第一行在堆内存中创建一个新的 Integer 对象,并在堆栈内存中创建一个变量,该变量存储对原始对象的引用。

然后,在下一行中,我们更改了“num”的引用,这意味着“num”不引用我们之前创建的 Integer 对象。事实上,该 Integer 对象未被我们程序的任何部分引用。因此,它是一个无法访问或死对象。死对象可以被垃圾回收。

在以下情况下对象将变得无法访问:

  1. 所有引用该对象的变量都不再引用它(它们要么设置为空,要么设置为不同的值)。
  2. 当该方法从堆栈内存中释放时,在该方法内部创建的对象将变得无法访问。

隔离岛

隔离岛是指一组对象相互引用,但不再被程序中的任何对象引用。在下面给出的示例中,“a”和“b”相互引用,但不再被任何其他对象引用。

class Node {
  Node next;
  Node prev;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.prev = a;

为了打破孤立岛,我们需要改变对象的引用。这里,只有改变“a”和“b”的引用(例如,将 a 和 b 设置为 null)后,它们才能被垃圾回收。

堆内存的各部分

前面提到过,堆内存是负责存储对象的内存部分,分为年轻代空间(Young Generation Space)和老生代空间(Old Generation Space)。

年轻一代

在 Java 中,年轻代堆内存是新对象被创建的地方。这部分内存又分为两个部分:Eden 空间和 Survivor 空间。

伊甸园空间

伊甸园空间是年轻代空间的一部分,新对象被分配于此。

cd044be05c8a40b7aa105ca8d9243796.png

幸存者空间

经过一轮垃圾收集后,Eden 空间中幸存下来的对象将被提升至幸存者空间。

Java 垃圾收集器中的 Survivor Spaces 数量取决于所使用的具体收集器。Survivor Spaces 数量取决于所使用的具体收集器。在并行收集器和 CMS 收集器中,有多个 Survivor Spaces。并行收集器将 Survivor Space 划分为多个区域,而 CMS 收集器使用多个 Survivor Spaces。我们将在下面仔细研究不同的 Java 垃圾收集器。

老一辈

经过一定次数的垃圾收集后仍存活的对象将被提升到老一代。老一代中的对象寿命更长。它们不适合进行次要 GC,只能在主要垃圾收集期间清除。

老生代又称为终身代。

垃圾收集涉及的步骤

Java 垃圾收集通过持续监视 Java 虚拟机的堆内存来识别不再使用的对象。

Java垃圾收集按以下步骤进行:

  • 标记: GC 首先识别堆中的所有活动对象并对其进行标记。
  • 清除:一旦识别并标记了所有活动对象,GC 就会清除堆并释放不再使用的内存。然后这些内存便可分配给新对象。
  • 压缩:在某些 Java 垃圾收集算法中,清除之后,剩余的对象会被压缩,这意味着它们会被移动到堆的一端,使得 JVM 更容易分配新对象。

小型垃圾收集器

次要垃圾收集是从年轻一代识别和收集垃圾的过程,保持其无垃圾并减少主要 Java 垃圾收集周期的频率。

小型 Java 垃圾收集在较小的堆大小上进行,因此比大型垃圾收集快得多。

次要垃圾收集的工作原理如下:

Eden 空间填充:随着新对象被分配到 Eden 空间,Eden 空间最终会被填满。当 Eden 空间已满时,垃圾收集器将开始一次次要 GC 循环。

初始标记:垃圾收集器通过执行初始标记阶段来开始次要垃圾收集周期。在此阶段,垃圾收集器会识别 Eden 空间和幸存者空间中的所有活动对象。垃圾收集器会标记这些活动对象以表明不应收集它们。

复制收集:初始标记阶段完成后,垃圾收集器将执行复制收集阶段。在此阶段,垃圾收集器将所有存活对象从 Eden 空间和其中一个幸存者空间复制到另一个幸存者空间。

清除未使用的空间:将存活对象复制到幸存者空间后,垃圾收集器将清除 Eden 空间和当前垃圾收集周期中未使用的幸存者空间。任何未复制到幸存者空间的对象都被视为垃圾,并由垃圾收集器回收。

对象的提升:经历了一定数量的垃圾收集周期而幸存下来的对象最终将被提升到老一代(也称为终身一代),在那里它们将由针对较长寿命的对象进行优化的不同垃圾收集算法进行管理。

多重循环:如果在当前 Java 垃圾收集周期中使用的幸存者空间已满,则收集器将执行额外的次要垃圾收集周期,直到足够多的对象被提升到老一代或幸存者空间再次变空。

bbb0637cad7a4882bb1ad9d943ed2bdb.png

大型垃圾收集器

当老年代空间被填满时,主垃圾收集器就会启动。之所以称为“主垃圾收集器”,是因为它作用于整个堆,并且调用频率低于次要垃圾收集器。它通常更耗时且占用资源更多。

主要垃圾收集所涉及的步骤与上面描述的非常相似。

Java 中的垃圾收集器类型

Java 提供了几种不同类型的垃圾收集器。以下是所有垃圾收集器的列表以及它们的工作原理和优点。

串行收集器或停止并复制

串行垃圾收集器是 Java 中的一种 GCr,它使用单个线程执行 Java 垃圾收集过程。它主要用于具有相对简单的内存使用模式的基本应用程序。

您可能已经猜到了,串行垃圾收集器按顺序工作,这意味着它在执行 Java 垃圾收集过程时会停止应用程序中的所有线程。应用程序执行中的这种暂停有时被称为“stop the world”事件。

要使用串行收集器,请传入 -XX:UseSerialCollector 作为参数。

例如,java -XX:UseSerialCollector YourProgram

bee6e2433f154093baa7a6ad93d981b0.png

串行收集器的优点:

  1. 简单:这是 Java 中最简单、最直接的垃圾收集器。它占用空间小,需要的调整最少。
  2. 可预测性:由于它使用单线程,因此其行为可预测且易于理解。这使其对于需要可预测内存使用模式的应用程序非常有用。
  3. 非常低的开销:这使其对于性能至关重要的小型应用程序非常有用。

串行收集器的缺点:

  1. 不设计为可扩展:它不是设计为随着堆的大小或系统上可用的处理器的数量而扩展。
  2. 内存使用率低:可用内存利用率低。
  3. 长暂停时间:由于其设计,长暂停时间被融入到流程中。

并行收集器

并行垃圾收集器是Java 中的默认垃圾收集器,它是一种利用多个线程来提高垃圾收集性能的方法。它对于具有复杂内存使用模式的大型应用程序特别有效。

通过将堆细分为更小的段,并行垃圾收集器利用多个线程同时执行垃圾收集过程。与串行收集器类似,并行收集器也会在垃圾收集期间导致应用程序执行暂时暂停。

要使用并行收集器,请传入 -XX:+UseParallelGC 作为参数。

598572d14703468380e6effbcb682211.png

并行收集器的优点:

  1. 更快:由于利用了多线程,与串行收集器相比,其性能更佳,可以实现更快的垃圾收集操作。
  2. 更好的可扩展性:设计用于通过堆的大小有效地扩展,使其适合具有更大内存需求的应用程序。

并行收集器的缺点:

  1. 更长的暂停时间:并行垃圾收集器在垃圾收集过程中停止应用程序,与其他垃圾收集器相比,这会导致更长的暂停时间。
  2. 更高的 CPU 开销:并行垃圾收集器使用多个线程,这会导致更高的开销和增加内存使用量。

并发标记清除收集器

CMS (并发标记和清除)收集器是另一种垃圾收集器。它与应用程序并发执行垃圾收集过程,或者换句话说,它使用多个垃圾收集器线程。它旨在最大限度地减少应用程序中的暂停时间并减少 Java 垃圾收集对性能的影响。

要使用 CMSCollector,请传入 -XX:+UseConcMarkSweepGC 作为参数。

56944805885c4cdcb7c8111d260482a1.png

CMS的优点:

  1. 低暂停时间: CMS 最大限度地减少垃圾收集期间的暂停时间,为延迟敏感的应用程序提供更流畅的体验。
  2. 可预测: CMS 提供更可预测的垃圾收集暂停,这对于实时系统或具有严格性能要求的应用程序至关重要。
  3. 适用于更大的堆:即使堆大小增加,CMS 仍能保持其性能,这使其成为具有大量内存需求的应用程序的可行选择。

CMS 的缺点:

  1. 更高的 CPU 开销: CMS 由于其并发特性而消耗更多的 CPU 资源,这可能会影响应用程序的整体性能。
  2. 碎片化风险: CMS 不是长期运行应用程序的理想选择,因为堆会随着时间的推移而变得碎片化。这种碎片化会导致内存使用量增加和性能下降。

G1 收集器

Garbage -First (G1) 收集器是 Java 7 中引入的一种垃圾收集算法,旨在解决传统垃圾收集器(例如并行收集器和 CMS 收集器)的局限性。G1 被设计为低暂停、以吞吐量为导向的收集器,可以处理非常大的堆。

要使用 G1 Collector,请传入以下参数:

-XX:+UseG1GC

G1收集器的优点:

  1. 低暂停时间: G1 的设计旨在最大限度地减少暂停时间,这使其适合实时应用。
  2. 可扩展性: G1 具有可扩展性,这使其适用于具有不同堆大小的大型应用程序。

G1收集器的缺点:

  1. 开销:与其他垃圾收集器相比,G1 消耗更多的 CPU 资源,导致 CPU 开销增加。
  2. 较长的初始标记时间: G1 中的初始标记阶段可能需要更长时间,特别是对于较大的堆大小,这可能会对应用程序性能产生负面影响。
  3. 不适合小堆:G1 收集器不适合堆大小较小的应用程序,因为它的优势在较大的内存环境中才能得到最好的体现。

ZGC

ZGC 垃圾收集器( ZGC) 是一款 Java 垃圾收集器,专门用于管理超大堆(最大 16TB),同时保持最短的暂停时间。其主要目标是最大限度地缩短垃圾收集过程的持续时间,从而最大限度地提高应用程序的吞吐量。

ZGC的优势:

  1. 低暂停时间:旨在最大限度地减少暂停时间,使其适用于实时或延迟敏感的应用程序。
  2. 可扩展性:它可以根据堆的大小和可用处理器的数量进行扩展。
  3. 高性能:针对高性能进行了优化,实现了可观的吞吐量,同时最大限度地减少了 Java 垃圾收集对应用程序性能的影响。

ZGC的缺点:

  1. 高内存开销: ZGC 需要大量内存才能有效运行。
  2. 兼容性有限: ZGC 仅在某些平台上可用,包括 Linux/x64,并且至少需要 JDK 11。
  3. 更高的 CPU 利用率:由于其先进的功能,ZGC 可能与其他垃圾收集器相比消耗更多的 CPU 资源,从而可能影响整体应用程序的性能。

雪兰多Shenandoah

Shenandoah是一款 Java 垃圾收集器,旨在实现超低暂停时间,同时保持高吞吐量。作为并发垃圾收集器,它与应用程序并行运行,非常适合对延迟敏感的应用程序。

要使用 Shenandoah Collector,请传入以下参数:

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

雪兰多的优势:

  1. 超低暂停时间:提供最短的暂停时间,使其成为实时或延迟敏感应用程序的理想选择。
  2. 低内存开销:有效管理内存,从而减少内存开销。
  3. 高吞吐量:尽管 Shenandoah 专注于降低暂停时间,但它仍然保持高吞吐量,确保最佳的应用程序性能。

Shenandoah 的缺点:

  1. 兼容性有限: Shenandoah 仅在某些平台上可用,包括 Linux/x64,并且至少需要 JDK 8u40。
  2. 增加 CPU 利用率:与其他垃圾收集器相比,这可能会消耗更多的 CPU 资源,从而可能影响整体应用程序的性能。

系统.gc()

Runtime.getRuntime().gc() 或 System.gc() 是要求 JVM 执行垃圾收集以释放一些内存的方法——强调“建议”这个词,因为这正是它所做的。

您无法强制进行垃圾收集。如果您收到“ java.lang.OutOfMemoryError ”,则调用 System,gc() 将无法解决问题,因为 JVM 通常会在抛出“ java.lang.OutOfMemoryError ”之前尝试运行垃圾收集器。可能的修复方法是使用不同的 GC 或增加堆大小。

一般来说,建议开发人员避免直接调用System.gc(),而是依赖JVM提供的自动垃圾收集功能。

垃圾收集的常见问题及其解决方法

以下是一些可能面临的问题:

内存不足错误:

当 JVM 内存不足时,就会发生此错误。要解决此问题,开发人员可以增加堆大小或优化应用程序以使用更少的内存。

  • -Xmx:为您的应用程序设置最大堆大小。
  • -Xms:为您的应用程序设置初始堆大小。

例如,要将最大堆大小设置为 2 GB,将初始堆大小设置为 512 MB,可以使用以下命令:

java -Xmx2g -Xms512m app.jar

长时间暂停:

垃圾收集期间的长时间暂停可能会导致应用程序无响应。要解决此问题,您可以选择专为低暂停时间设计的垃圾收集器或调整垃圾收集器参数。

内存泄漏:

内存泄漏是指对象未正确从内存中释放,导致内存使用量随时间增加。为了解决此问题,开发人员可以使用分析器等工具来识别内存泄漏并进行修复。

Java 内存管理的最佳实践

为了避免垃圾收集的常见问题并有效地管理内存,以下是一些最佳做法:

  1. 将引用设置为空:当不再需要某个对象时,始终将引用设置为空。
  2. 避免创建不必要的对象:创建不必要的对象会增加内存使用量并导致更频繁的垃圾回收。您应避免创建不必要的对象并尽可能重用现有对象。
  3. 使用匿名对象:这是当您不存储对对象的引用时。

例如,createUser(new User())。

  1. 不再需要时释放资源:使用外部资源(例如文件句柄或数据库连接)的对象应在不再需要时释放,以避免内存泄漏。

结论

无论您是内部开发还是决定外包 Java 开发,了解 Java 垃圾收集机制都是必须的,特别是如果您想提高 Java 应用程序的性能。我们详细研究了 Java 编程的这一重要部分,从垃圾收集工作原理的基础知识和不同类型的垃圾收集器到内存管理的细节。请记住,即使您雇用了 Java 开发人员,选择正确的 Java 垃圾收集类型并有效地管理内存也会对应用程序的速度产生很大的影响。继续探索,继续编码,并记住每一点效率都可以为更流畅、更快的应用程序做出贡献。祝您编码愉快!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

pytorch库 04 神经网络代码基础

文章目录 一、神经网络基本骨架 nn.Module二、卷积层三、池化层四、非线性激活层五、线性层六、模型搭建小练习:CIFAR 10 model 结构七、损失函数与反向传播八、优化器九、现有网络模型的使用与修改十、网络模型的保存与读取十一、一个完整模型训练套路十二、GPU加速…

arm体系结构(1)

一 RAM: 随机存储(主存储器) 速度快、掉电数据丢失 ROM: 只读存储(辅助存储器) 速度慢、掉电数据不丢失 RAM SRAM DRAM SDRAM DDR2、3、4、5 静态 动态 同步 ROM PROM EPROM EEPROM 可编程 可擦除 电可擦可编程 flash:结合RAM和ROM的优点,快、掉…

Visual Studio 2022 LNK2001无法解析的外部符号 _wcscat_s 问题记录

ANSI C程序中,用到了wcsrchr、wcsncpy_s、wcscat_s、wcscpy_s等几个字符串函数,但是编译时提示: 错误 LNK2001 无法解析的外部符号 _wcscat_s 查了挺多帖子,没有解决。 https://bbs.csdn.net/topics/250012844 解决VS编译…

Python青少年简明教程:为何学习Python编程语言及Python编程环境搭建

Python青少年简明教程:为何学习Python编程语言及Python编程环境搭建 我们写给他人看的文章,使用类语言。人类语言有很多种如汉语,英语,法语等等。 而写给 计算机 执行的 程序文件 ,必须使用 编程语言 , 因…

Java 集成测试详解及示例

通过综合指南探索 Java 集成测试的世界。了解工具、流程和最佳实践,并辅以实际示例。 随着软件系统变得越来越大、越来越复杂,组件和服务以错综复杂的方式交互,集成测试已变得不可或缺。通过验证所有组件和模块在组合时是否正常工作&#xff…

三级_网络技术_27_计算机网络环境及应用系统的安装与调试

1.对于频繁改变位置并使用DHCP获取PP地址的DNS客户端,为减少对其资源记录的手动管理,可采取的措施是()。 允许动态更新 使用反向查找区域 增加别名记录 设置较小的生存时间 2.下列Windows 2003系统命令中,可以清空DNS缓存(DNScache)的是…

在Ubuntu上有什么命令,或者是系统文件能告诉我链接nvme ssd的pcie槽位是不是支持热插拔功能?

🏆本文收录于《CSDN问答解惑-专业版》专栏,主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收…

打靶记录11——Billu_b0x

靶机: https://download.vulnhub.com/billu/Billu_b0x.zip难度: 中(两种攻击路线) 目标: 取得root权限 涉及的攻击方法: 主机发现端口扫描Web信息收集SQL注入(Sqlmap跑不出来)…

Spring Boot 3.3 【四】Spring Boot 整合JPA

🌟 技术人聊管理 请关注 【技术管理修行】 一、JPA 简介 Spring Data JPA 是 Spring Data 项目的一部分,它为使用 Java Persistence API (JPA) 进行数据库访问提供了一种非常简便的方式。Spring Data JPA 的主要目的是简化基于 JPA 的数据访问层的开发工…

leetCode - - - 栈和队列

目录 1.有效的括号( LeetCode 20 ) 2.最小栈( LeetCode 155 ) 3.接雨水( LeetCode 42 ) 4.逆波兰表达式求值(LeetCode 150) 5.柱状图中最大的矩形(LeetCode 84&…

SAP LE学习笔记02 - WM和库存管理(IM)之间的关系,保管Lot(Quant)

上一章学习了LE的基础知识。 1,LE的概述,LE里面包含下面3个大的模块 - LE-WM 仓库管理 / - LE-SHP 发货/ - LE-TRA 运输 2,仓库的结构 - 仓库番号 / -保管域Type(存储区域)/ - 保管区画(存储区)/ - 棚番(Storage Bin 仓位&…

IDEA快捷键(Ctrl + tab)非常好用 切换最近使用的编辑器选项卡

文章目录 1、为什么要使用 ctrl tab 快捷键?2、使用 ctrl tab 快捷键 1、为什么要使用 ctrl tab 快捷键? 当我们点击 ctrl alt 鼠标左键点击 进入方法的实现时,这个时候我们会在这个实现类中不断的点击,查看源码&#xff0c…

【安全工具推荐-Search_Viewer资产测绘】

目录 一、工具介绍 二、工具配置 三、传送门 一、工具介绍 Search_Viewer,集Fofa、Hunter鹰图、Shodan、360 quake、Zoomeye 钟馗之眼、censys 为一体的空间测绘gui图形界面化工具,支持一键采集爬取和导出fofa、shodan等数据,方便快捷查看…

竞争与冒险/亚稳态/跨时钟域

竞争与冒险/亚稳态/跨时钟域 文章目录 竞争与冒险/亚稳态/跨时钟域1.亚稳态1.1 好文章1.2 什么是亚稳态1.3亚稳态的解决办法1.3.1 跨时钟域的亚稳态——采用同步机制1.3.1.1 单比特(脉冲和单比特流)的跨时钟域同步1.3.1.1.1 单比特流的跨时钟域同步1.3.1.1.2 脉冲的跨时钟域同步…

.NET辅助角色服务入门简介

在日常开发中,并不是所有的功能都是用户可见的,还在一些背后默默支持的程序,这些程序通常以服务的形式出现,统称为辅助角色服务。今天以一个简单的小例子,简述基于.NET开发辅助角色服务的相关内容,仅供学习…

旅行商问题变体:欧几里德平面中线段最小连接算法

问题描述 假设在欧几里德平面上有有限多条线段,如何将它们连接起来,形成一条最小长度的线段链? 首先,自然可以穷举所有情况,找到最优解。还可以采用动态规划、贪心算法找到局部最优解。 另外,则将其作为T…

定时器延时us(hal库)

目录 定时器延时us 配置cubemx ​编辑​编辑新建模块 代码实现 测试代码 定时器延时us 复制工程模板 配置cubemx 新建模块 新建文件HardWare 添加文件HardWare 添加文件路径 添加HardWare 或者直接输入/HardWare 添加.c和.h文件 .h文件 添加防重复定义代码 #ifnd…

麒麟v10(ky10.x86_64)升级——openssl-3.2.2、openssh-9.8p1

系统版本: ky10.x86_64 下载安装包并上传 openssh下载地址 https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable openssl下载地址 https://openssl-library.org/source/index.html zlib下载地址 https://zlib.net/fossils/ 上传安装包 备份配置文件 cp -r /etc/ssh /et…

DePT: Decoupled Prompt Tuning

当前的问题:Base-New Tradeoff(BNT)困境 现有的提示调优方法通常无法摆脱Base-New Tradeoff(BNT)困境,即调优/调整的模型对基本任务的泛化效果越好,对新任务的泛化效果就越差(包含不可见的类),反之新任务的泛化效果越好,所需要的…

北京城市图书馆-非遗文献馆:OLED透明拼接屏的璀璨应用

在数字化与传统文化深度融合的今天,北京城市图书馆的非遗文献馆以一场前所未有的视觉盛宴,向世人展示了OLED透明拼接屏的非凡魅力与无限可能。这座集阅读、展示、体验于一体的非遗文献馆,通过2*7布局的OLED透明拼接屏,不仅为传统非…