JVM詳解 --- 類加載機(jī)制

類加載機(jī)制思維導(dǎo)圖

1.類的生命周期

Java類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸載(Unloading)七個(gè)階段。其中準(zhǔn)備、驗(yàn)證、解析3個(gè)部分統(tǒng)稱為連接(Linking),如下圖所示。

類的生命周期

注意:加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運(yùn)行時(shí)綁定(也稱為動(dòng)態(tài)綁定或晚期綁定)。

1.1類加載過程

當(dāng)程序主動(dòng)使用某個(gè)類時(shí),如果該類還未被加載到內(nèi)存中,則JVM會(huì)通過加載、連接、初始化3個(gè)步驟來對(duì)該類進(jìn)行初始化。如果沒有意外,JVM將會(huì)連續(xù)完成3個(gè)步驟,所以有時(shí)也把這個(gè)3個(gè)步驟統(tǒng)稱為類加載或類初始化。

1.1.1加載

類的加載過程主要完成三件事:

  • 通過全類名獲取定義此類的二進(jìn)制字節(jié)流(獲取.class文件的字節(jié)流)
  • 將字節(jié)流上面所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
  • 在內(nèi)存中生成一個(gè)代表該類的Class對(duì)象,作為方法區(qū)這些數(shù)據(jù)的訪問入口

值得注意的是,加載階段和連接階段的部分內(nèi)容是交叉進(jìn)行的,加載階段尚未結(jié)束,連接階段可能就已經(jīng)開始了。加載是類加載的一個(gè)階段,注意不要混淆。

ps:描述一下JVM加載Class文件的原理機(jī)制

Java中的所有類,都需要由類加載器裝載到JVM中才能運(yùn)行。類加載器本身也是一個(gè)類,而它的工作就是把class文件從硬盤讀取到內(nèi)存中。在寫程序的時(shí)候,我們幾乎不需要關(guān)心類的加載,因?yàn)檫@些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。

類裝載方式,有兩種 :

1.隱式裝載, 程序在運(yùn)行過程中當(dāng)碰到通過new 等方式生成對(duì)象時(shí),隱式調(diào)用類裝載器加載對(duì)應(yīng)的類到j(luò)vm中,
2.顯式裝載, 通過class.forname()等方法,顯式加載需要的類

Java類的加載是動(dòng)態(tài)的,它并不會(huì)一次性將所有類全部加載后再運(yùn)行,而是保證程序運(yùn)行的基礎(chǔ)類(像是基類)完全加載到j(luò)vm中,至于其他類,則在需要的時(shí)候才加載。這當(dāng)然就是為了節(jié)省內(nèi)存開銷。

1.1.2 連接

當(dāng)類被加載之后,系統(tǒng)為之生成一個(gè)對(duì)應(yīng)的Class對(duì)象,接著將會(huì)進(jìn)入連接階段,連接階段負(fù)責(zé)把類的二進(jìn)制數(shù)據(jù)合并到JRE中。類連接又可分為如下3個(gè)階段。

(1)驗(yàn)證:驗(yàn)證階段主要就是對(duì)文件格式,元數(shù)據(jù),字節(jié)碼以及符號(hào)應(yīng)用的一些驗(yàn)證,個(gè)人感覺跟編譯檢查一樣的工作。確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。其主要包括四種驗(yàn)證,文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證。

  • 文件格式驗(yàn)證:主要驗(yàn)證字節(jié)流是否符合Class文件格式規(guī)范,并且能被當(dāng)前的虛擬機(jī)加載處理。例如:主,次版本號(hào)是否在當(dāng)前虛擬機(jī)處理的范圍之內(nèi)。常量池中是否有不被支持的常量類型。指向常量的中的索引值是否存在不存在的常量或不符合類型的常量。
  • 元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語義的分析,分析是否符合java的語言語法的規(guī)范。
  • 字節(jié)碼驗(yàn)證:最重要的驗(yàn)證環(huán)節(jié),分析數(shù)據(jù)流和控制,確定語義是合法的,符合邏輯的。主要的針對(duì)元數(shù)據(jù)驗(yàn)證后對(duì)方法體的驗(yàn)證。保證類方法在運(yùn)行時(shí)不會(huì)有危害出現(xiàn)。
  • 符號(hào)引用驗(yàn)證:主要是針對(duì)符號(hào)引用轉(zhuǎn)換為直接引用的時(shí)候,是會(huì)延伸到第三解析階段,主要去確定訪問類型等涉及到引用的情況,主要是要保證引用一定會(huì)被訪問到,不會(huì)出現(xiàn)類等無法訪問的問題。

(2)準(zhǔn)備準(zhǔn)備階段是為類的靜態(tài)變量分配內(nèi)存并設(shè)置默認(rèn)初始值,這些內(nèi)存都將在方法區(qū)中分配。對(duì)于該階段有以下幾點(diǎn)需要注意:

  • 這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(static),而不包括實(shí)例變量,實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一塊分配在 Java 堆中。
  • 這里所設(shè)置的初始值"通常情況"下是數(shù)據(jù)類型默認(rèn)的零值(如0、0L、null、false等),比如我們定義了public static int value=111 ,那么 value 變量在準(zhǔn)備階段的初始值就是 0 而不是111(初始化階段才會(huì)賦值)。特殊情況:比如給 value 變量加上了 fianl 關(guān)鍵字public static final int value=111 ,那么準(zhǔn)備階段 value 的值就被賦值為 111。

(3)解析:將類的二進(jìn)制數(shù)據(jù)(存儲(chǔ)在常量池)中的符號(hào)引用替換成直接引用。說明一下:符號(hào)引用:符號(hào)引用是以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何的字面形式的字面量,只要不會(huì)出現(xiàn)沖突能夠定位到就行。布局和內(nèi)存無關(guān)。直接引用:是指向目標(biāo)的指針,偏移量或者能夠直接定位的句柄。該引用是和內(nèi)存中的布局有關(guān)的,并且一定加載進(jìn)來的。

1.1.3 初始化

初始化是類加載的最后一步,也是真正執(zhí)行類中定義的 Java 程序代碼(字節(jié)碼),初始化階段是執(zhí)行類構(gòu)造器方法的過程,即為類的靜態(tài)變量賦予正確的初始值。對(duì)于構(gòu)造方法的調(diào)用,虛擬機(jī)會(huì)自己確保其在多線程環(huán)境中的安全性。因?yàn)闃?gòu)造方法是帶鎖線程安全,所以在多線程環(huán)境下進(jìn)行類初始化的話可能會(huì)引起死鎖,并且這種死鎖很難被發(fā)現(xiàn)。

ps:準(zhǔn)備階段和初始化階段看似有點(diǎn)矛盾,其實(shí)是不矛盾的,如果類中有語句:private static int a = 10,它的執(zhí)行過程是這樣的,首先字節(jié)碼文件被加載到內(nèi)存后,先進(jìn)行鏈接的驗(yàn)證這一步驟,驗(yàn)證通過后準(zhǔn)備階段,給a分配內(nèi)存,因?yàn)樽兞縜是static的,所以此時(shí)a等于int類型的默認(rèn)初始值0,即a=0, 然后到解析(后面在說),到初始化這一步驟時(shí),才把a(bǔ)的真正的值10賦給a,此時(shí)a=10。

1.2 卸載

卸載類即該類的Class對(duì)象被GC。卸載類需要滿足3個(gè)要求:

  • 該類的所有的實(shí)例對(duì)象都已被GC,也就是說堆不存在該類的實(shí)例對(duì)象。
  • 該類沒有在其他任何地方被引用
  • 該類的類加載器的實(shí)例已被GC

所以,在JVM生命周期類,由jvm自帶的類加載器加載的類是不會(huì)被卸載的。但是由我們自定義的類加載器加載的類是可能被卸載的。

只要想通一點(diǎn)就好了,jdk自帶的BootstrapClassLoader,PlatformClassLoader,AppClassLoader負(fù)責(zé)加載jdk提供的類,所以它們(類加載器的實(shí)例)肯定不會(huì)被回收。而我們自定義的類加載器的實(shí)例是可以被回收的,所以使用我們自定義加載器加載的類是可以被卸載掉的。

2.類與類加載器

類加載器負(fù)責(zé)加載所有的類,其為所有被載入內(nèi)存中的類生成一個(gè)java.lang.Class實(shí)例對(duì)象。一旦一個(gè)類被加載如JVM中,同一個(gè)類就不會(huì)被再次載入了。正如一個(gè)對(duì)象有一個(gè)唯一的標(biāo)識(shí)一樣,一個(gè)載入JVM的類也有一個(gè)唯一的標(biāo)識(shí)。在Java中,一個(gè)類用其全限定類名(包括包名和類名)作為標(biāo)識(shí);但在JVM中,一個(gè)類用其全限定類名和其類加載器作為其唯一標(biāo)識(shí)。例如,如果在pg的包中有一個(gè)名為Person的類,被類加載器ClassLoader的實(shí)例kl負(fù)責(zé)加載,則該P(yáng)erson類對(duì)應(yīng)的Class對(duì)象在JVM中表示為(Person.pg.kl)。這意味著兩個(gè)類加載器加載的同名類:(Person.pg.kl)和(Person.pg.kl2)是不同的、它們所加載的類也是完全不同、互不兼容的。

ps:限定類名,就是類名全稱,帶包路徑的用點(diǎn)隔開,例如: java.lang.String,非限定類名是相對(duì)于限定類名來說的,在Java中有很多類,不同的類之間會(huì)存在相同的函數(shù)或者方法,所以有時(shí)候就需要限定類名來調(diào)包。 而如果不存在相同的函數(shù)或者方法 ,就可以使用非限定(non-qualified)類名。

2.1類加載器分類

JVM預(yù)定義有三種類加載器,當(dāng)一個(gè) JVM啟動(dòng)的時(shí)候,Java開始使用如下三種類加載器:

  • 啟動(dòng)類加載器/根加載器(Bootstrap ClassLoader):它用來加載 Java 的核心類,是用原生代碼來實(shí)現(xiàn)的,并不繼承自 java.lang.ClassLoader(負(fù)責(zé)加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實(shí)現(xiàn),不是ClassLoader子類)。由于引導(dǎo)類加載器涉及到虛擬機(jī)本地實(shí)現(xiàn)細(xì)節(jié),開發(fā)者無法直接獲取到啟動(dòng)類加載器的引用,所以不允許直接通過引用進(jìn)行操作。
  • ExtensionClassLoader(擴(kuò)展類加載器) :它負(fù)責(zé)加載JRE的擴(kuò)展目錄,lib/ext或者由java.ext.dirs系統(tǒng)屬性指定的目錄中的JAR包的類。由Java語言實(shí)現(xiàn),父類加載器為null。
  • AppClassLoader(應(yīng)用程序/系統(tǒng)類加載器) :面向用戶的加載器,負(fù)責(zé)加載當(dāng)前應(yīng)用classpath下的所有jar包和類,一般來說,Java 應(yīng)用的類都是由它來完成加載的。程序可以通過ClassLoader的靜態(tài)方法getSystemClassLoader()來獲取系統(tǒng)類加載器。如果沒有特別指定,則用戶自定義的類加載器都以此類加載器作為父加載器。由Java語言實(shí)現(xiàn),父類加載器為ExtClassLoader。
  • 用戶自定義類加載器:通過繼承 java.lang.ClassLoader類的方式實(shí)現(xiàn)。

2.2雙親委派模型

2.2.1 類加載機(jī)制

JVM的類加載機(jī)制主要有如下3種:

  • 全盤負(fù)責(zé):所謂全盤負(fù)責(zé),就是當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)Class時(shí),該Class所依賴和引用其他Class也將由該類加載器負(fù)責(zé)載入,除非顯示使用另外一個(gè)類加載器來載入。
  • 雙親委派:所謂的雙親委派,則是先讓父類加載器試圖加載該Class,只有在父類加載器無法加載該類時(shí)才嘗試從自己的類路徑中加載該類。通俗的講,就是某個(gè)特定的類加載器在接到加載類的請(qǐng)求時(shí),首先將加載任務(wù)委托給父加載器,依次遞歸,如果父加載器可以完成類加載任務(wù),就成功返回;只有父加載器無法完成此加載任務(wù)時(shí),才自己去加載。
  • 緩存機(jī)制。緩存機(jī)制將會(huì)保證所有加載過的Class都會(huì)被緩存,當(dāng)程序中需要使用某個(gè)Class時(shí),類加載器先從緩存區(qū)中搜尋該Class,只有當(dāng)緩存區(qū)中不存在該Class對(duì)象時(shí),系統(tǒng)才會(huì)讀取該類對(duì)應(yīng)的二進(jìn)制數(shù)據(jù),并將其轉(zhuǎn)換成Class對(duì)象,存入緩沖區(qū)中。這就是為很么修改了Class后,必須重新啟動(dòng)JVM,程序所做的修改才會(huì)生效的原因。

應(yīng)用程序是由三種類加載器互相配合從而實(shí)現(xiàn)類加載,除此之外還可以加入自己定義的類加載器。

下圖展示了類加載器之間的層次關(guān)系,稱為雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動(dòng)類加載器外,其它的類加載器都要有自己的【父加載器】。這里的父子關(guān)系一般通過組合關(guān)系(Composition)來實(shí)現(xiàn),而不是繼承關(guān)系(Inheritance)。

雙親委派模型
2.2.2 雙親委派機(jī)制工作流程

一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一層的類加載器都是如此,這樣所有的加載請(qǐng)求都會(huì)被傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載無法完成加載請(qǐng)求(它的搜索范圍中沒找到所需的類)時(shí),子加載器才會(huì)嘗試去加載類。

總結(jié):自底向上檢查類是否被加載過,自頂向下嘗試加載類。

(3)雙親委派模型優(yōu)劣

優(yōu)點(diǎn):(1)保證安全(2)避免重復(fù)加載

  • 首先,使得Java類隨著它的類加載器一起具有一種帶有優(yōu)先級(jí)的層次關(guān)系,保證基礎(chǔ)類的安全與統(tǒng)一,即核心API不被篡改。假設(shè)通過網(wǎng)絡(luò)傳遞一個(gè)名為java.lang.Integer的類,通過雙親委托模式傳遞到啟動(dòng)類加載器,而啟動(dòng)類加載器在核心Java API發(fā)現(xiàn)這個(gè)名字的類,發(fā)現(xiàn)該類已被加載,并不會(huì)重新加載網(wǎng)絡(luò)傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。
  • 其次,保證java程序更加穩(wěn)定,可以避免類的重復(fù)加載,確保一個(gè)類的全局唯一性。(jvm區(qū)分不同類的方式不僅僅根據(jù)類名,相同的類文件被不同的類加載器加載產(chǎn)生的是兩個(gè)不同的類,兩者不兼容)

存在的問題:。。。。

如何打破雙親委派機(jī)制?

ps:打破雙親委派機(jī)制則不僅要繼承ClassLoader類,還要重寫loadClass和findClass方法。

(4)具體實(shí)現(xiàn)

以下是抽象類 java.lang.ClassLoader的代碼片段,其中的 loadClass() 方法運(yùn)行過程如下:先檢查類是否已經(jīng)加載過,如果沒有則讓父類加載器去加載。當(dāng)父類加載器加載失敗時(shí)拋出 ClassNotFoundException,此時(shí)嘗試自己去加載。

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

2.3 自定義類加載器

以下代碼中的 FileSystemClassLoader是自定義類加載器,繼承自 java.lang.ClassLoader,用于加載文件系統(tǒng)上的類。它首先根據(jù)類的全名在文件系統(tǒng)上查找類的字節(jié)代碼文件(.class 文件),然后讀取該文件內(nèi)容,最后通過 defineClass() 方法來把這些字節(jié)代碼轉(zhuǎn)換成java.lang.Class 類的實(shí)例。

java.lang.ClassLoader 的 loadClass()實(shí)現(xiàn)了雙親委派模型的邏輯,自定義類加載器一般不去重寫它,但是需要重寫 findClass()方法。

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

3.類加載時(shí)機(jī)

主要包括類的創(chuàng)建(new或者反射),訪問(靜態(tài)變量或者靜態(tài)方法),初始化一個(gè)類的子類和標(biāo)明啟動(dòng)類。

  • 創(chuàng)建類的實(shí)例,也就是new一個(gè)對(duì)象
  • 反射(Class.forName("com.it.load"))
  • 訪問某個(gè)類或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值
  • 調(diào)用類的靜態(tài)方法
  • 初始化一個(gè)類的子類(會(huì)首先初始化子類的父類)
  • JVM啟動(dòng)時(shí)標(biāo)明的啟動(dòng)類,即文件名和類名相同的那個(gè)類

除此之外,下面幾種情形需要特別指出:

  • 對(duì)于一個(gè)final類型的靜態(tài)變量,如果該變量的值在編譯時(shí)就可以確定下來,那么這個(gè)變量相當(dāng)于“宏變量”。Java編譯器會(huì)在編譯時(shí)直接把這個(gè)變量出現(xiàn)的地方替換成它的值,因此即使程序使用該靜態(tài)變量,也不會(huì)導(dǎo)致該類的初始化。反之,如果final類型的靜態(tài)Field的值不能在編譯時(shí)確定下來,則必須等到運(yùn)行時(shí)才可以確定該變量的值,如果通過該類來訪問它的靜態(tài)變量,則會(huì)導(dǎo)致該類被初始化。

拓展

主動(dòng)引用:對(duì)于初始化階段,虛擬機(jī)嚴(yán)格規(guī)范了有且只有5種情況下,必須對(duì)類進(jìn)行初始化(只有主動(dòng)去使用類才會(huì)初始化類):

1)當(dāng)遇到 new 、 getstatic、putstatic或invokestatic 這4條直接碼指令時(shí),比如 new 一個(gè)類,讀取一個(gè)靜態(tài)字段(未被 final 修飾)、或調(diào)用一個(gè)類的靜態(tài)方法時(shí)。

  • 當(dāng)jvm執(zhí)行new指令時(shí)會(huì)初始化類。即當(dāng)程序創(chuàng)建一個(gè)類的實(shí)例對(duì)象。
  • 當(dāng)jvm執(zhí)行g(shù)etstatic指令時(shí)會(huì)初始化類。即程序訪問類的靜態(tài)變量(不是靜態(tài)常量,常量會(huì)被加載到運(yùn)行時(shí)常量池)。
  • 當(dāng)jvm執(zhí)行putstatic指令時(shí)會(huì)初始化類。即程序給類的靜態(tài)變量賦值。
  • 當(dāng)jvm執(zhí)行invokestatic指令時(shí)會(huì)初始化類。即程序調(diào)用類的靜態(tài)方法。

2)使用 java.lang.reflect 包的方法對(duì)類進(jìn)行反射調(diào)用時(shí)如Class.forname("..."),newInstance()等等。 ,如果類沒初始化,需要觸發(fā)其初始化。

3)初始化一個(gè)類,如果其父類還未初始化,則先觸發(fā)該父類的初始化。

4)當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要定義一個(gè)要執(zhí)行的主類 (包含 main 方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)類。

5)MethodHandle和VarHandle可以看作是輕量級(jí)的反射調(diào)用機(jī)制,而要想使用這2個(gè)調(diào)用, 就必須先使用findStaticVarHandle來初始化要調(diào)用的類。

被動(dòng)引用:除此之外,所有引用類的方式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。被動(dòng)引用的常見例子包括:

1)通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化。

System.out.println(SubClass.value);  // value 字段在 SuperClass 中定義

2)通過數(shù)組定義來引用類,不會(huì)觸發(fā)此類的初始化。該過程會(huì)對(duì)數(shù)組類進(jìn)行初始化,數(shù)組類是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承自 Object 的子類,其中包含了數(shù)組的屬性和方法。

SuperClass[] sca = new SuperClass[10];

3)常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的類的初始化。

System.out.println(ConstClass.HELLOWORLD);

6.參考

https://github.com/CyC2018/CS-Notes
https://github.com/Snailclimb/JavaGuide
https://blog.csdn.net/m0_38075425/article/details/81627349

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容