Java ~ Reference ~ Finalizer【总结】

news2024/11/17 21:16:59

前言


 文章

  • 相关系列:《Java ~ Reference【目录】》(持续更新)
  • 相关系列:《Java ~ Reference ~ Finalizer【源码】》(学习过程/多有漏误/仅作参考/不再更新)
  • 相关系列:《Java ~ Reference ~ Finalizer【总结】》(学习总结/最新最准/持续更新)
  • 相关系列:《Java ~ Reference ~ Finalizer【问题】》(学习解答/持续更新)
  • 涉及内容:《Java ~ Reference【总结】》
  • 涉及内容:《Java ~ Reference ~ ReferenceQueue【总结】》
  • 涉及内容:《Java ~ Reference ~ Cleaner【总结】》
  • 涉及内容:《Java ~ Reference ~ FinalReference【总结】》

一 概述


 简介

    Finalizer(终结者)类是FinalReference(终引用)类的唯一子类,同时也是Finalization(终结)机制的具体定义及实现者。所谓的终结机制本质是finalize()方法的调用机制,即finalize()方法是如何实现在F类对象被GC回收之前调用的。Java在终结者类中实现了终结机制的流程及结构,通过对终结者类的研究,我们可以掌握终结机制的绝大部分内容,因此直接将终结者类视作终结机制本身也未尝不可,该知识点会在下文详述。

    终结者类必须搭配引用队列使用,并且所有终结者都会固定将f-queue作为自身的注册引用队列。f-queue是在终结者类中创建的全局唯一引用队列,直接作用是承载所有的终结者,而其根本作用有二:一是追踪所指对象/F类对象的GC状态;二是作为JVM执行终结机制的起点,该知识点会在下文详述。

    当F类对象被创建时,JVM会自动将之作为所指对象创建终结者。终结者类对开发者直接屏蔽,即开发者无法直接创建终结者,JVM会在F类对象创建时自动为之创建终结者以进行终结机制。所谓F类,是指finalize()方法被重写且方法体不为空的类,因此虽然所有的类都存在finalize()方法(因为所有的类都继承自Object(对象)类),但JVM并不会为所有的对象创建终结者,同理也并非所有对象的finalize()方法都会被执行。

 结构

在这里插入图片描述

二 使用


    实际上,讲述终结者类的使用其实并没有太大意义,因为终结者类不是公共类,无法被其本身所在包外的类访问,故而开发者并无法使用终结者类。但为了清楚了解终结者类的相应功能/结构,此处我们还是会简单的介绍一下其关键的几个方法。

 创建

    终结者类没有公共的构造方法,具体的实例通过其提供的工厂方法创建。

  • static void register(Object finalizee) —— 创建固定将f-queue作为注册引用队列的终结者,并默认将终结者以头插法加入未终结链表中。该方法会由JVM在F类对象创建时调用,以创建将F类对象作为所指对象的终结者。
        方法会直接调用私有构造方法Finalizer(Object finalizee)实现。其首先固定将f-queue作为当前终结者的注册引用队列,随后调用add()方法将当前终结者加入未终结引用队列中。令人比较疑惑的是:从上述流程来看,似乎没有使用工厂方法register(Object finalizee)创建终结者的必要,因为如果只是为了使创建的终结者存在于未终结链表中的话,直接使用构造方法也是可以做到的。
/**
 * @Description: 注册终结者(即创建一个终结者并加入终结者队列,该方法由JVM进行调用)
 */
/* Invoked by VM */
static void register(Object finalizee) {
    new Finalizer(finalizee);
}

private Finalizer(Object finalizee) {
    // 采用一个静态的引用队列作为终结者引用队列,因此可知所有的终结者注册的都是同一个引用队列。
    super(finalizee, queue);
    // 将终结者对象加入终结者队列中。
    add();
}

 方法

  • private void add() —— 新增 —— 将当前终结者以头插法加入未终结链表中。该方法会在终结者创建时被调用,因此所有的终结者初始时都存在于未终结链表中。

  • private void remove() —— 移除 —— 将当前终结者从未终结链表中移除,该方法会在当前终结者终结后调用。

  • private boolean hasBeenFinalized() —— 是否终结 —— 判断当前终结者是否终结,即其所指对象/F类对象是否已执行finalize()方法,是则返回true;否则返回false。
        方法本质是判断终结者是否已从未终结链表中移除。上文已经说过,remove()方法只会在终结者终结之后被调用,因此终结者是否已从未终结链表中移除可变相作为终结者是否终结的判断依据。即如果终结者已从未终结链表中移除,则其必然已终结。

三 实现


 f-queue

    f-queue的本质其实就是在终结者类中创建的全局唯一引用队列,由于其在终结机制中发挥了重要作用,因此在JVM相关书籍中将之命名为了f-queue。f-queue的直接作用是承载所有的终结者。终结者类与PhantomReference(虚引用)类一样都必须搭配引用对象使用,而终结者创建时都会固定将f-queue作为其注册引用队列,因此最终所有的终结者都会因为Reference(引用)机制加入f-queue中。f-queue的根本作用有二:一是追踪所指对象/F类对象的GC状态,即判断所指对象/F类对象是否已/会被GC回收,这是引用队列的基本功能,此处不在赘述;二是作为JVM执行终结机制的起点,即JVM会从f-queue中取出终结者以执行其所指对象/F类对象的finalize()方法。

 未终结链表

在这里插入图片描述

    未终结链表的作用是建立终结者与GC ROOTS的关联,以确保终结者不会在终结机制期间被GC回收,这与Cleaner(清洁工)类中持有的清洁工链表作用相同。由于终结者由JVM创建,因此初始情况下,如果没有未终结链表,则终结者处于未与GC ROOTS建立直接/间接关联的状态,这就可能出现终结者的所指对象/F类对象尚未执行finalize()方法(更准确的说是终结者加入待定列表前,因为待定链表也是全局静态变量,属于GC ROOTS的范畴,但发现列表不是)就被GC回收的情况,导致部分终结者无法终结。因此为了避免这一点,JVM必须建立所有终结者与GC ROOTS的直接/间接关联,而未终结链表起的就是这个作用。

    未终结链表是由终结者类内部持有的全局唯一双向链表,即未终结链表是一个全局静态变量。未终结链表是逻辑链表,即未终结链表并不是一个类似于LinkedList(链接列表)类对象的对象。终结者类中用于持有未终结链表的[unfinalized @ 未终结]实际上持有的是未终结链表头终结者/节点的引用,由于每个终结者都持有着其在未终结链表中的前驱/后继终结者/节点,因此只要持有了头终结者/节点就相当于持有了整个未终结链表。未终结链表是线程安全的,终结者类通过synchronized关键字搭配静态对象的方式实现了类锁来保证未终结链表的线程安全。

    终结者会在其创建时默认加入未终结链表,因此所有的终结者初始时都存在于未终结链表中。关于上述内容粗略的说法是:当F类对象创建时,JVM会自动将之作为所指对象调用终结者类的register()方法创建终结者并加入未终结链表中。但实际上,关于终结者的加入时间还能有更精确的描述/调整。众所周知Java对象的创建并非原子操作,大致可以分为实例化(内存分配)与初始化(构造方法执行)两个部分。通过对JVM参数-XX:+RegisterFinalizersAtInit的调整 ,可以设置终结者在F类对象实例化后、初始化前加入,也可以设置其在F类对象初始化后加入。

void Heap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object) {
    ScopedObjectAccess soa(self);
    ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object));
    jvalue args[1];
    args[0].l = arg.get();
    // 调用 Java 层静态方法 FinalizerReference#add
    InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);
    *object = soa.Decode<mirror::Object>(arg.get());
}

    JVM是如何做到在指定的时间段里调用终结者类的register()方法的呢?这个就要根据不同的情况判断。针对在实例化后、初始化前调用的F类对象,由于内存分配是统一由内存管理系统负责的,因此可以由其负责处理;而对于在初始化后的F类对象则相对复杂,难道要在所有的构造方法尾部都进行处理吗?当然是不可能。实际上JVM使用了一个很灵活的方式,在Java中,子类的构造方法会调用父类的构造方法,因此最终都会执行Object(对象)类的构造方法。JVM将对象类构造方法里的return指令替换为_return_register_finalizer指令,从而在侵入性很小的情况下完成了方法调用。

    将终结者从未终结链表中移除无需遍历。由于未终结链表是双向链表,因此将终结者从未终结链表中移除并不需要遍历,因为终结者自身组合了字段用于保存其在未终结链表中的前驱/后继终结者/节点,因此直接在类锁的保护下将前驱/后继终结者/节点重新链接即可。终结者被移除后,其前驱/后继引用会指向自身,即自引用,作为其已从未终结链表中移除的标志。之所以不使用null作为标志是因为null已经被作为了头/尾终结者/节点的标志,因此可通过判断终结者是否自引用来判断终结者是否已被移除。由于终结者只会在其所指对象/F类对象的finalize()方法执行后才会被JVM从未终结链表中移除,因此其也可以变相作为终结者是否终结的判断依据,而事实上终结者类也确实是这么做的。

 终结者线程

    终结者线程是终结者类自实现内部类FinalizerThread(终结者线程)类的唯一实例,专用于执行终结机制。终结者线程类是Thread(线程)类的子类,因此其实例同样也是一个线程。终结者线程在终结者类的静态块中创建,是一个优先级为8的守护线程,因此整个JVM中只有一条终结者线程。很多资料都会很简略的介绍说终结机制的执行线程的优先级很低,容易造成终结者/F类对象堆积而导致OOM,但实际上这种说法并不准确,因为相对于大量优先级为5的用户线程来说,优先级为8的终结者线程优先级并不算低。导致OOM的原因更多是因为终结者线程只有一条,难以与数量众多的用户线程竞争CPU资源。

    终结者线程用于执行终结机制,简单的说就是不断的将终结者从f-queue及未终结链表中获取/移除终结者,并执行其所指对象/F类对象的finalize()方法,最后断开两者间的引用,以加速两者被GC回收,该知识点会在下文详述。

四 终结机制


在这里插入图片描述

 简介

    终结机制的本质是finalize()方法的调用机制,即finalize()方法是如何实现在F类对象被GC回收之前调用的。Java在终结者类中实现了终结机制的流程及结构,通过对终结者类的研究,我们可以掌握终结机制的绝大部分内容。虽然没有硬性规定,但终结机制最适合用于资源的释放/回收。由于finalize()方法会在F类象被GC回收前执行,因此可以趁此机会执行一些自定义操作。但如果要说最合适,则是用于对资源进行释放,例如I/O流、网络连接等。资源最明显的特点是往往需要使用堆外内存,而由于堆外内存不受GC的管理,因此正常情况下如果不手动释放资源,堆外内存永远无法被回收(也有不正常的情况,例如堆内存溢出导致Full GC时也会顺势回收)。基于该原因,终结机制往往会被用来作为资源释放的保底机制,通过在资源类的finalize()方法中重写资源释放的代码,可使得即使没有手动的释放,也可以在资源类对象被GC回收时触发释放。

    虽然终结机制确实提供了一套关于某些问题的处理方案,但并不推荐去使用它,因为其是非常不稳定的。正是因为终结机制如此的不靠谱,因此其只能在必要的情况下作为保底机制使用,而不能作为处理问题的常规方案。终结机制可能存在的问题如下:

  • 终结线程的优先级相对较低,finalize()方法的执行速度可能小于终结者/F类对象新增的速度,使得终结者/F类对象累积而导致新生代频繁GC -> 老年代频繁GC -> FULL GC -> OOM;
  • 如果某个F类对象的finalize()方法执行时间很长,会使得终结者/F类对象累积而导致新生代频繁GC -> 老年代频繁GC -> FULL GC -> OOM;
  • JVM/程序运行时,只保证finalize()方法一定会被调用,但不保证会等待其执行结束;
  • JVM/程序退出时,无法保证累积的所有F类对象的finalize()方法都被执行,因此可能导致资源泄露。

 流程

    终结机制和引用机制的关系甚深。很难具体言明两者之间的关联,即可以认为终结机制是引用机制的一部分,也可以认为终结机制是引用机制的衍生,甚至还可以认为引用机制是终结机制的一部分。但无论是那种说法,终结机制依赖于引用机制实现都是毫无争议的事实,在囊括引用机制流程的情况下,终结机制的完整流程如下:

  • F类对象创建,JVM自动将之作为所指对象创建终结者。终结者会被默认加入未终结链表,并且终结者作为终引用也会被默认加入发现列表,故而终结者初始会同时存在于发现列表与未终结链表中,此时终结者处于活跃状态;
  • F类对象被GC判定为可回收,其终结者被GC线程从发现列表中移除并头插至待定列表(堆栈)中,此时终结者处于待定状态;
  • 引用处理器线程将终结者从待定列表(堆栈)中移除并头插至f-queue中,此时终结者处于入队状态;
  • 终结者线程将终结者从f-queue中移除,此时终结者处于怠惰状态;
  • 终结者线程继续将之从未终结链表中移除,随后执行其所指对象/F类对象的finalize()方法;
  • 当JVM判断F类对象的finalize()方法执行已/可结束后,终结者线程会断开终结者与其的关联,令两者可便于被GC回收(不是可被回收,即使不断开关联也是可以回收的)。

 辅助终结

    一方面为了避免终结者/F类对象累积过多而导致OOM,另一方面为了在JVM/程序退出时能尽可能保证所有F类对象的finalize()方法都被执行,终结机制存在加速/退出两种辅助终结操作(名字我自己瞎取的),会分别在上述两种情况下执行。虽说命名上有所差异,但实际上两种辅助终结操作的核心是一致的,即创建新的线程参与活动,以加速原本只由终结者线程进行的终结机制的执行效率。但其中需要注意的是:创建的辅助线程并非终结者线程类的实例,只是普通的用户线程,这些线程在名义上被称为二级终结者线程。

    当终结者/F类对象累积过多时,为了避免出现OOM,会触发加速辅助终结操作。该操作会创建二级终结者线程参与终结机制的执行,执行流程也与标准流程基本相同,唯一的区别在于当终结者线程发现f-queue为空时会进行无限等待状态,直至有新终结者入队被唤醒。而当二级终结者线程发现f-queue为空时会直接结束,即二级终结者线程会直接退出/消亡。说的更加通俗一点,即终结者线程从f-queue中移除/获取终结者使用的是引用队列类的remove()方法,而二级终结者线程使用的则是poll()方法。

    当JVM/程序退出时,为了尽可能保证所有F类对象的finalize()方法都被执行,会触发退出辅助终结操作。退出辅助终结操作同样会创建二级终结者线程,但在执行流程上却与标准流程有较大差异。该差异体现在其不会再从f-queue中获取终结者,而是直接从未终结链表中获取终结者。这是因为当JVM/程序退出时,引用机制可能已经不再执行,即相关GC、引用处理器等执行线程可能已经消亡,因此终结者可能已无法再加入f-queue中。为了尽可能保证所有F类对象的finalize()方法都被执行,直接从未终结链表中获取未终结的终结者是最好的方式。由于在执行F类对象的finalize()方法前会判断其终结者是否已从未终结链表中移除,因此无需担心多线程执行时重复执行的情况。需要注意的是:由于JVM/程序退出时的剩余运行时刻不可控,因此虽然退出辅助终结操作会被触发,但同样无法保证所有F类对象的finalize()方法都被执行。

 其它

    F类对象的finalize()方法只能被终结机制执行一次。这是面试中经常会遇到的一个问题,之所以只能执行一次,是因为执行finalize()方法的终结者线程/二级终结者线程会从f-queue/未终结链表中获取终结者。而一旦F类对象的finalize()方法被执行,则终结者必然已从f-queue/未终结链表中移除,因此无法执行第二次。但注意,只能执行一次只是单纯基于终结机制而言的,而开发者完全可以手动且不限次数的执行finalize()方法。这里可能存在一个疑问,即finalize()方法执行后F类对象不是会被GC回收吗?怎么还会有执行第二次的说法呢?这是因为finalize()方法执行后F类对象未必一定会被GC回收,如果F类对象在finalize()方法中成功与GC ROOTS重新建立了直接/间接关联,则确实是可以避免自身被GC回收的。

    F类对象至少要两次GC才能被真正回收。第一次GC将F类对象判定为可回收并执行终结机制;第二次GC则会将执行完finalize()方法并断开与终结者关联的F类对象正式回收。而在这两次GC之间也可能存在有多次的GC,因此是至少两次。

五 JVM参数


  • -XX:+RegisterFinalizersAtInit —— 设置终结者加入未终结链表的时间。为false时在F类对象实例化后、初始化前加入,为true时在F类对象初始化后加入。默认true。

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

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

相关文章

基于Python的接口自动化-Requests模块

目录 引言 一、模块说明 二、Requests模块快速入门 1 发送简单的请求 2 发送带参数的请求 3 定制header头和cookie 4 响应内容 5 发送post请求 6 超时和代理 三、Requests实际应用 引言 在使用Python进行接口自动化测试时&#xff0c;实现接口请求…

2023春期末考试选择题R2-9AVL树插入调整详解

题目&#xff1a; 将 8, 9, 7, 2, 3, 5, 6, 4 顺序插入一棵初始为空的AVL树。下列句子中哪句是错的&#xff1f; A. 4 和 6 是兄弟 B. 5 是 8 的父结点 C. 7 是根结点 D. 3 和 8 是兄弟 解题要点&#xff1a; 需要对AVL树的4种旋转方式熟悉。 AVL旋转过程&#xff1a; 根据…

体验ChatGPT使用

ChatGPT是一种基于GPT&#xff08;Generative Pre-train Transformer&#xff09;模型的大型语言模型&#xff0c;由OpenAI公司开发。 交互时&#xff0c;有一定的技巧&#xff0c;可以快速准确的反馈正确答案。 一、开发贪吃蛇游戏 浏览器访问&#xff1a;https://chat.opena…

taro使用小记 —— 持续更新

目录 1、在 taro 中使用 axios2、在 taro 中添加全局组件自动引入和方法自动引入3、在 taro 中使用 pinia 1、在 taro 中使用 axios taro 3.6 版本已经支持了网络请求库。 需安装插件 tarojs/plugin-http 使用和注意事项说明&#xff1a; https://www.npmjs.com/package/taroj…

【笔试强训选择题】Day22.习题(错题)解析

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;笔试强训选择题 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01; 文章目录 前言 一、…

mac电脑m1搭建java开发环境参考手册

1 背景介绍 开发人员经常会换电脑&#xff0c;或者换新电脑&#xff0c;意味着重新搭建开发环境&#xff0c;很麻烦。但新电脑到手里面了&#xff0c;不换又不好&#xff0c;此篇专门用来记录mac电脑m1搭建java开发环境的步骤。希望对读者有所帮助&#xff0c;一条龙服务。 后…

初探 transformer

大部分QA的问题都可以使用seq2seq来实现。或者说大多数的NLP问题都可以使用seq2seq模型来解决。 但是呢最好的办法还是对具体的问题作出特定的模型训练。 概述 Transformer就是一种seq2seq模型。 我们先看一下seq2seq这个模型的大体框架(其实就是一个编码器和一个解码器)&a…

OpenGL 光照贴图

1.简介 现实世界中的物体通常并不只包含有一种材质&#xff0c;而是由多种材质所组成。想想一辆汽车&#xff1a;它的外壳非常有光泽&#xff0c;车窗会部分反射周围的环境&#xff0c;轮胎不会那么有光泽&#xff0c;所以它没有镜面高光&#xff0c;轮毂非常闪亮。 2.漫反射…

Baumer工业相机堡盟工业相机如何使用BGAPISDK对两个万兆网相机进行触发同步(C#)

Baumer工业相机堡盟工业相机如何使用BGAPISDK对两个万兆网相机进行触发同步&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机BGAPISDK和触发同步的技术背景Baumer工业相机使用BGAPISDK进行双相机主从相机触发1.引用合适的类文件2.使用BGAPISDK设置主相机硬件触发从相机…

ReentrantLock 底层原理

目录 一、ReentrantLock入门 二、AQS原理 1、AQS介绍 2、自定义锁 三、ReentrantLock实现原理 1、非公平锁的实现 加锁流程 释放锁流程 2、可重入原理 3、可打断原理 4、公平锁原理 5、条件变量原理 await流程 signal流程 一、ReentrantLock入门 相对于synchron…

对测试外包的一些粗略看法

什么叫外包&#xff0c;外包最直接理解就是让别人做事&#xff1b;外包其中一项目的就是降低企业经营成本。 从外包的含义和目的来看&#xff0c;就是我们帮人做事、听人指挥&#xff0c;当企业经济不好的时候&#xff0c;我们就成为了降低成本的最佳方案。说这些是让大家比较…

高并发编程:线程池

一、概述 线程池首先有几个接口先了解第一个是Executor&#xff0c;第二个是ExecutorService&#xff0c;在后面才是线程池的一个使用ThreadPoolExecutor。 二、Executor Executor看它的名字也能理解&#xff0c;执行者&#xff0c;所以他有一个方法叫执行&#xff0c;那么执…

JVM原理:JVM垃圾回收算法(通俗易懂)

目录 前言正文垃圾标记算法引用类型强引用软引用弱引用虚引用 引用计数法循环引用问题 根可达性分析法虚拟机栈&#xff08;栈帧的局部变量表&#xff09;中的引用方法区中类静态属性引用方法区中常量引用本地方法栈&#xff08;Native方法&#xff09;引用 垃圾回收算法标记清…

Java语法进阶及常用技术(八)--线程池

初识线程池 什么是“池” ---- 软件中的“池”&#xff0c;可以理解为计划经济。 我们的资源是有限的&#xff0c;比如只有十个线程&#xff0c;我们创造十个线程的线程池&#xff0c;可能我们的任务非常多&#xff0c;如1000个任务&#xff0c;我们就把1000个任务放到我们十个…

shell脚本学习记录(流程控制)

前言&#xff1a; 在shell脚本中&#xff0c;()、{}、[]都是用来表示命令或者变量的范围或者属性。它们的具体区别如下&#xff1a; ()&#xff1a;表示命令在子shell中运行。括号中的命令会在一个子shell中运行&#xff0c;并且该子shell拥符有自己的环境变量和文件描述&#…

【youcans动手学模型】DenseNet 模型-CIFAR10图像分类

欢迎关注『youcans动手学模型』系列 本专栏内容和资源同步到 GitHub/youcans 【youcans动手学模型】DenseNet 模型-CIFAR10图像分类 1. DenseNet 神经网络模型1.1 模型简介1.2 论文介绍1.3 改进方法与后续工作1.4 分析与讨论 2. 在 PyTorch 中定义 DenseNet 模型类2.1 DenseBlo…

性能测试实战——登录接口的性能测试(超详细总结)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 在实际业务场景中…

python:六个模块,概括全书(上万字最详细版)

拍摄于——无锡南长街 文章目录 模块一&#xff1a;基础知识1、python语言2、常见数字类型3、字符串4、数字类型转换5、标识符命名6、常见关键字7、运算符与表达式&#xff08;1&#xff09;算术运算符&#xff08;2&#xff09;关系运算符&#xff08;3&#xff09;逻辑运算符…

循序渐进,搞懂什么是动态规划

循序渐进&#xff0c;搞懂什么是动态规划 写在前面 温馨提示&#xff0c;本文的篇幅很长&#xff0c;需要花很长的时间阅读。如果要完全理解所有内容&#xff0c;还需要花更多的时间学习。如果打算认真学习动态规划&#xff0c;又不能一次看完&#xff0c;建议您收藏本文以便后…

《深入理解计算机系统》(6)存储器层次结构

1、存储技术 随机访问存储器&#xff0c;分为两类&#xff1a; RAM&#xff0c;同时也是易失性存储器&#xff0c;也分为两类&#xff1a; - SRAM&#xff1a;静态随机访问存储器&#xff0c;速度快&#xff0c;价格高。多用来作为高速缓存存储器。 - DRAM&#xff1a;动态随机…