蒼穹之邊每辟,浩瀚之摯剑辫,眰恦之美; 悟心悟性影兽,善始善終揭斧,惟善惟道! —— 朝槿《朝槿兮年說(shuō)》
寫(xiě)在開(kāi)頭
從接觸 Java 開(kāi)發(fā)到現(xiàn)在峻堰,大家對(duì) Java 最直觀的印象是什么呢讹开?是它宣傳的 “Write once, run anywhere”,還是目前看已經(jīng)有些過(guò)于形式主義的語(yǔ)法呢捐名?有沒(méi)有靜下心來(lái)仔細(xì)想過(guò)旦万,對(duì)于 Java 到底了解到什么程度?
自從業(yè)以來(lái)镶蹋,對(duì)于Java的那些紛紛擾擾的問(wèn)題成艘,我們或多或少都有些道不明,說(shuō)不清的情緒贺归,一直心有余悸淆两,甚至困惑著我們的,可曾入夢(mèng)拂酣。
是不是有著秋冰,不論查閱了多少遍的資料,以及翻閱了多少技術(shù)大咖的書(shū)籍婶熬,也未能解開(kāi)心里那由來(lái)已久的疑惑剑勾,就像一個(gè)個(gè)未解之謎一般縈繞心扉,惶惶不可終日赵颅?
我一直都在問(wèn)自己虽另,一段Java代碼的中類(lèi),從編寫(xiě)到編譯饺谬,經(jīng)過(guò)一系列的步驟加載到JVM捂刺,再到運(yùn)行的過(guò)程,它究竟是如何運(yùn)作和流轉(zhuǎn)的商蕴,其機(jī)制是什么叠萍?我們看到的結(jié)果究竟是如何呈現(xiàn)出來(lái)的,這其中發(fā)生了什么绪商?
雖然苛谷,從學(xué)習(xí)Java之初,我們都會(huì)了解和記憶格郁,以及在后來(lái)大家在提及的時(shí)候腹殿,大多數(shù)都是一句“我們應(yīng)該都不陌生”独悴,甚至“我相信大家都了然于心”之類(lèi)話“蜻蜓點(diǎn)水”般輕描淡寫(xiě)。
但是锣尉,如果真的要問(wèn)一問(wèn)的話刻炒,能詳細(xì)說(shuō)道一二的,想必都會(huì)以“夏蟲(chóng)不可語(yǔ)冰“的悲劇上演了吧自沧!作為一名Java Develioer來(lái)說(shuō)坟奥,正確了解和掌握這些原理和機(jī)制,早已經(jīng)不是什么”不能說(shuō)的秘密“拇厢。
帶著這些問(wèn)題爱谁,今日我們便來(lái)扒一扒一個(gè)Java對(duì)象中的那些枝末細(xì)節(jié),一個(gè)Java對(duì)象是如何被創(chuàng)建和執(zhí)行的孝偎,我們又該如何理解和認(rèn)識(shí)這些原理和機(jī)制访敌,以及在日常開(kāi)發(fā)工作中,我們需要注意些什么衣盾?
關(guān)健術(shù)語(yǔ)
本文用到的一些關(guān)鍵詞語(yǔ)以及常用術(shù)語(yǔ)寺旺,主要如下:
- 指針壓縮(CompressedOops) : 全稱(chēng)為Compressed Ordinary Object Pointer,在HotSpot VM 64位(bit)虛擬機(jī)為了提升內(nèi)存使用率而提出的指針壓縮技術(shù)势决。主要是指將Java程序中的所有對(duì)象引用指針壓縮一半阻塑,主要闡述的是一個(gè)指針大小占用一個(gè)字寬單位大小,即就是HotSpot VM 64位(bit)虛擬機(jī)的一個(gè)字寬單位大小是64bit果复,在實(shí)際工作時(shí)叮姑,原本的指針會(huì)壓縮成32bit,Oracle JDK從6 update 23開(kāi)始在64位系統(tǒng)上開(kāi)始支持開(kāi)啟壓縮指針据悔,在JDK1.7版本之后默認(rèn)開(kāi)啟。
- 指針碰撞(Bump the Pointer), 指的Java對(duì)象為分配堆內(nèi)存的一種內(nèi)存分配方式耘沼,其分配過(guò)程是把內(nèi)存分為已分配內(nèi)存和空間內(nèi)存分別處于不同的一側(cè)极颓,主要通過(guò)一個(gè)指針指向分界點(diǎn)區(qū)分。一般JVM為一個(gè)新對(duì)象分配內(nèi)存的時(shí)候群嗤,把指針往往空閑內(nèi)存區(qū)域移動(dòng)指向相同對(duì)象大小的距離即可菠隆。一般適用于Serial和ParNew等不會(huì)產(chǎn)生內(nèi)存碎片,且堆內(nèi)存完整的收集器狂秘。
- 空閑列表(Clear Free List): 指的Java對(duì)象為分配堆內(nèi)存的一種內(nèi)存分配方式,其分配過(guò)程是把內(nèi)存分為已分配內(nèi)存和空間內(nèi)存相互交錯(cuò)骇径,JVM通過(guò)維護(hù)一張內(nèi)存列表記錄的可用空間內(nèi)存塊,創(chuàng)建新對(duì)象需要分配堆內(nèi)存時(shí)者春,從列表中尋找一個(gè)足夠大的內(nèi)存塊分配給對(duì)象實(shí)例破衔,同步更新列表記錄情況,當(dāng)GC收集器發(fā)生GC時(shí)钱烟,把已回收的內(nèi)存更新到內(nèi)存列表晰筛。一般適用于CMS等會(huì)產(chǎn)生內(nèi)存碎片嫡丙,且堆內(nèi)存不完整的收集器。
- 逃逸分析(Escape Analysis): 在編程語(yǔ)言的編譯優(yōu)化原理中读第,分析指針動(dòng)態(tài)范圍的方法稱(chēng)之為逃逸分析曙博。主要是判斷變量的作用域是否存在于其他內(nèi)存棧或者線程中怜瞒,當(dāng)一個(gè)對(duì)象的指針被多個(gè)方法或線程引用時(shí)父泳,我們稱(chēng)這個(gè)指針發(fā)生了逃逸。其用來(lái)分析這種逃逸現(xiàn)象的方法吴汪,就稱(chēng)之為逃逸分析惠窄。跟靜態(tài)代碼分析技術(shù)中的指針?lè)治龊屯庑畏治鲱?lèi)似。
- 標(biāo)量替換(Scalar Replacement):主要是指使用標(biāo)量替換聚合量(Java中的對(duì)象實(shí)例)浇坐,把一個(gè)對(duì)象進(jìn)行分解成一個(gè)個(gè)的標(biāo)量進(jìn)行逃逸分析睬捶,不可選的對(duì)象才能進(jìn)行標(biāo)量替換。標(biāo)量主要是指不可分割的量近刘,一般來(lái)說(shuō)主要是基本數(shù)據(jù)類(lèi)型和引用類(lèi)型擒贸。
- 棧上分配(Allocation on Stack): 一般Java對(duì)象創(chuàng)建出來(lái)會(huì)在棧上進(jìn)行內(nèi)存分配,不是所有的對(duì)象都可以實(shí)現(xiàn)棧上分配觉渴。要想實(shí)現(xiàn)棧上分配介劫,需要進(jìn)行逃逸分析和標(biāo)量替換。
基本概述
Java 本身是一種面向?qū)ο蟮恼Z(yǔ)言案淋,最顯著的特性有兩個(gè)方面座韵,一是所謂的“書(shū)寫(xiě)一次,到處運(yùn)行”(Write once, run anywhere)踢京,能夠非常容易地獲得跨平臺(tái)能力誉碴;另外就是垃圾收集(GC, Garbage Collection),Java 通過(guò)垃圾收集器(Garbage Collector)回收分配內(nèi)存瓣距,大部分情況下黔帕,程序員不需要自己操心內(nèi)存的分配和回收。
我們?nèi)粘?huì)接觸到 JRE(Java Runtime Environment)或者 JDK(Java Development Kit)蹈丸。 JRE成黄,也就是 Java 運(yùn)行環(huán)境,包含了 JVM 和 Java 類(lèi)庫(kù)逻杖,以及一些模塊等奋岁。而 JDK 可以看作是 JRE 的一個(gè)超集,提供了更多工具荸百,比如編譯器闻伶、各種診斷工具等。
對(duì)于“Java 是解釋執(zhí)行”這句話管搪,這個(gè)說(shuō)法不太準(zhǔn)確虾攻。我們開(kāi)發(fā)的 Java 的源代碼铡买,首先通過(guò) Javac 編譯成為字節(jié)碼(bytecode),然后霎箍,在運(yùn)行時(shí)奇钞,通過(guò) Java 虛擬機(jī)(JVM)內(nèi)嵌的解釋器將字節(jié)碼轉(zhuǎn)換成為最終的機(jī)器碼。但是常見(jiàn)的 JVM漂坏,比如我們大多數(shù)情況使用的 Oracle JDK 提供的 Hotspot JVM景埃,都提供了 JIT(Just-In-Time)編譯器,也就是通常所說(shuō)的動(dòng)態(tài)編譯器顶别,JIT 能夠在運(yùn)行時(shí)將熱點(diǎn)代碼編譯成機(jī)器碼谷徙,這種情況下部分熱點(diǎn)代碼就屬于編譯執(zhí)行,而不是解釋執(zhí)行驯绎。
眾所周知完慧,我們通常把 Java 分為編譯期和運(yùn)行時(shí)。這里說(shuō)的 Java 的編譯和 C/C++ 是有著不同的意義的剩失,Javac 的編譯屈尼,編譯 Java 源碼生成“.class”文件里面實(shí)際是字節(jié)碼,而不是可以直接執(zhí)行的機(jī)器碼拴孤。Java 通過(guò)字節(jié)碼和 Java 虛擬機(jī)(JVM)這種跨平臺(tái)的抽象脾歧,屏蔽了操作系統(tǒng)和硬件的細(xì)節(jié),這也是實(shí)現(xiàn)“一次編譯演熟,到處執(zhí)行”的基礎(chǔ)鞭执。
1.Java源碼分析
Java源碼依據(jù)JDK提供的API來(lái)組織有效的代碼實(shí)體,一般都是通過(guò)調(diào)用API來(lái)編織和組成代碼的芒粹。
對(duì)于一段Java源代碼(Source Code)來(lái)說(shuō)兄纺,要想正確被執(zhí)行,需要先編譯通過(guò)化漆,最后托管給所承載JVM囤热,最終才被運(yùn)行。
Java是一個(gè)主要思想是面向?qū)ο蟮幕袢渲械腏ava的數(shù)據(jù)類(lèi)型主要有基本數(shù)據(jù)類(lèi)型和包裝類(lèi)類(lèi)型,其中:
- 基本數(shù)據(jù)類(lèi)型(8大數(shù)據(jù)類(lèi)型锨苏,其中void):byte疙教、short、int伞租、long贞谓、float、double葵诈、char裸弦、boolean祟同、void
- 包裝類(lèi)類(lèi)型:Byte、Short理疙、Integer晕城、Long、Float窖贤、Double砖顷、Character、Boolean赃梧、Void
其中滤蝠,數(shù)據(jù)類(lèi)型主要是用來(lái)描述對(duì)象的基本特征和賦予功能屬性的一套語(yǔ)義分析規(guī)則。
一般來(lái)說(shuō)Java源碼的支持授嘀,會(huì)依據(jù)JDK提供的API來(lái)組織有效的代碼實(shí)體物咳,對(duì)于源代碼的實(shí)現(xiàn),通常我們都是通過(guò)調(diào)用API來(lái)編織和組成代碼的蹄皱。
2.Java編譯機(jī)制
Java編譯機(jī)制主要可以分為編譯前端和編譯后端兩個(gè)階段览闰,一般來(lái)說(shuō)主要是指將源代碼翻譯為目標(biāo)代碼的過(guò)程,稱(chēng)為編譯過(guò)程夯接。
編譯從一定意義上來(lái)說(shuō)焕济,根本上就是“翻譯”,指的計(jì)算機(jī)能否識(shí)別和認(rèn)識(shí)盔几,促成我們與計(jì)算機(jī)通信的工作機(jī)制晴弃。
Java整個(gè)編譯以及運(yùn)行的過(guò)程相當(dāng)繁瑣,總體來(lái)看主要有:詞法分析 --> 語(yǔ)法分析 --> 語(yǔ)義分析和中間代碼生成 --> 優(yōu)化 --> 目標(biāo)代碼生成逊拍。
具體來(lái)看上鞠,Java程序從源文件創(chuàng)建到程序運(yùn)行要經(jīng)過(guò)兩大步驟,其中:
- 編譯前端:Java文件會(huì)由編譯器編譯成class文件(字節(jié)碼文件)芯丧,會(huì)經(jīng)過(guò)編譯原理簡(jiǎn)單過(guò)程的前三步芍阎,屬于狹義的編譯過(guò)程,是將源代碼翻譯為中間代碼的過(guò)程缨恒。
- 編譯后端: 字節(jié)碼由java虛擬機(jī)解釋運(yùn)行谴咸,解釋執(zhí)行即為目標(biāo)代碼生成并執(zhí)行。因此骗露,Java程序既要編譯的同時(shí)也要經(jīng)過(guò)JVM的解釋運(yùn)行岭佳。屬于廣義的編譯過(guò)程,是將源代碼翻譯為機(jī)器代碼的過(guò)程萧锉。
從詳細(xì)分析來(lái)看珊随,在編譯前端的階段,最重要的一個(gè)編譯器就是javac 編譯器, 在命令行執(zhí)行javac命令叶洞,其實(shí)本質(zhì)是運(yùn)行了javac.exe這個(gè)應(yīng)用鲫凶。
而對(duì)于編譯后端的階段來(lái)說(shuō),最重要的是 運(yùn)行期即時(shí)編譯器(JIT衩辟,Just in Time Compiler)和 靜態(tài)的提前編譯器(AOT螟炫,Ahead of Time Compiler)。
特別指出惭婿,在Oracle JDK 9之前不恭, Hotspot JVM 內(nèi)置了兩個(gè)不同的 JIT compiler,其中:
- C1模式:屬于輕量級(jí)的Client編譯器财饥,對(duì)應(yīng)client 模式换吧,編譯時(shí)間短,占用內(nèi)存少钥星,適用于對(duì)于啟動(dòng)速度敏感的應(yīng)用沾瓦,比如普通 Java GUI 桌面應(yīng)用。
- C2模式:屬于重量級(jí)的Server編譯器谦炒,對(duì)應(yīng) server 模式贯莺,執(zhí)行效率高,大量編譯優(yōu)化宁改,它的優(yōu)化是為長(zhǎng)時(shí)間運(yùn)行的服務(wù)器端應(yīng)用設(shè)計(jì)的缕探,適用于服務(wù)器。
但是还蹲,我們需要注意的是爹耗,默認(rèn)是采用所謂的分層編譯(TieredCompilation)。
在Oracle JDK 9之后谜喊,除了我們?nèi)粘W畛R?jiàn)的 Java 使用模式潭兽,其實(shí)還有一種新的編譯方式,即所謂的 AOT編譯斗遏,直接將字節(jié)碼編譯成機(jī)器代碼山卦,這樣就避免了 JIT 預(yù)熱等各方面的開(kāi)銷(xiāo),比如 Oracle JDK 9 就引入了實(shí)驗(yàn)性的 AOT 特性诵次,并且增加了新的 jaotc 工具账蓉。
3.Java類(lèi)加載機(jī)制
Java類(lèi)加載機(jī)制主要分為加載,驗(yàn)證逾一,準(zhǔn)備剔猿,解析,初始化等5個(gè)階段嬉荆。
當(dāng)源代碼編譯完成之后,便是執(zhí)行過(guò)程酷含,其中需要一定的加載機(jī)制來(lái)幫助我們簡(jiǎn)化流程鄙早,從Java HotSpot(TM)的執(zhí)行模式上看汪茧,一般主要可以分為三種:
- 第一種:解析模式(Interpreted Mode)
Marklin:~ marklin$ java -Xint -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, interpreted mode)
Marklin:~ marklin$
- 第二種:編譯模式(Compiled Mode)
Marklin:~ marklin$ java -Xcomp -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, compiled mode)
Marklin:~ marklin$
- 第三種: 混合模式(Mixed Mode),主要是指編譯模式和解析模式的組合體
Marklin:~ marklin$ java -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode)
Marklin:~ marklin$
不論哪一種模式,只有在具體的使用場(chǎng)景上限番,Java HotSpot(TM)會(huì)依據(jù)系統(tǒng)環(huán)境自動(dòng)選擇啟動(dòng)參數(shù)舱污。
在Java HotSpot(TM)中,JVM類(lèi)加載機(jī)制分為五個(gè)部分:加載弥虐,驗(yàn)證扩灯,準(zhǔn)備,解析霜瘪,初始化珠插。其中:
- 加載:會(huì)在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的入口颖对。
- 驗(yàn)證: 確保Class文件的字節(jié)流中包含的信息是否符合當(dāng)前虛擬機(jī)的要求捻撑,并且不會(huì)危害虛擬機(jī)自身的安全。
- 準(zhǔn)備: 正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量的初始值階段缤底,即在方法區(qū)中分配這些變量所使用的內(nèi)存空間顾患。
- 解析: 虛擬機(jī)將常量池中的符號(hào)引用替換為直接引用的過(guò)程。
- 初始化: 前面的類(lèi)加載階段之后个唧,除了在加載階段可以自定義類(lèi)加載器以外江解,其它操作都由JVM主導(dǎo)。到了初始階段徙歼,才開(kāi)始真正執(zhí)行類(lèi)中定義的Java程序代碼犁河。
對(duì)于解析階段,我們需要理解符號(hào)引用和直接引用鲁沥,其中:
- 符號(hào)引用: 符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的布局無(wú)關(guān)呼股,引用的目標(biāo)并不一定要已經(jīng)加載到內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同画恰,但是它們能接受的符號(hào)引用必須是一致的,因?yàn)榉?hào)引用的字面量形式明確定義在Java虛擬機(jī)規(guī)范的Class文件格式中唱矛。符號(hào)引用就是class文件中主要包括CONSTANT_Class_info包个,CONSTANT_Field_info,CONSTANT_Method_info 等類(lèi)型的常量谜洽。
- 直接引用: 是指向目標(biāo)的指針逊彭,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄卸勺。如果有了直接引用砂沛,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
對(duì)于初始化階段來(lái)說(shuō)曙求,是執(zhí)行類(lèi)構(gòu)造器 client方法的過(guò)程碍庵。其方法是由編譯器自動(dòng)收集類(lèi)中的類(lèi)變量的賦值操作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并而成的映企。虛擬機(jī)會(huì)保證子類(lèi)構(gòu)造器 client方法執(zhí)行之前,父類(lèi)的類(lèi)構(gòu)造器 client方法已經(jīng)執(zhí)行完畢静浴,如果一個(gè)類(lèi)中沒(méi)有對(duì)靜態(tài)變量賦值也沒(méi)有靜態(tài)語(yǔ)句塊卑吭,那么編譯器可以不為這個(gè)類(lèi)生成類(lèi)構(gòu)造器 client方法。
特別需要注意的是马绝,以下幾種情況不會(huì)執(zhí)行類(lèi)初始化:
- 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,只會(huì)觸發(fā)父類(lèi)的初始化挣菲,而不會(huì)觸發(fā)子類(lèi)的初始化富稻。
- 定義對(duì)象數(shù)組,不會(huì)觸發(fā)該類(lèi)的初始化白胀。
- 常量在編譯期間會(huì)存入調(diào)用類(lèi)的常量池中椭赋,本質(zhì)上并沒(méi)有直接引用定義常量的類(lèi),不會(huì)觸發(fā)定義常量所在的類(lèi)或杠。
- 通過(guò)類(lèi)名獲取Class對(duì)象哪怔,不會(huì)觸發(fā)類(lèi)的初始化。
- 通過(guò)Class.forName加載指定類(lèi)時(shí)向抢,如果指定參數(shù)initialize為false時(shí)认境,也不會(huì)觸發(fā)類(lèi)初始化,其實(shí)這個(gè)參數(shù)是告訴虛擬機(jī)挟鸠,是否要對(duì)類(lèi)進(jìn)行初始化叉信。
- 通過(guò)ClassLoader默認(rèn)的loadClass方法,也不會(huì)觸發(fā)初始化動(dòng)作艘希。
在Java HotSpot(TM)虛擬機(jī)中硼身,其加載動(dòng)作放到JVM外部實(shí)現(xiàn),以便讓?xiě)?yīng)用程序決定如何獲取所需的類(lèi)覆享,主要提供了3種類(lèi)加載器佳遂,其中:
- 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader):負(fù)責(zé)加載 JAVA_HOME\lib 目錄中的,或通過(guò)-Xbootclasspath參數(shù)指定路徑中的撒顿,且被虛擬機(jī)認(rèn)可(按文件名識(shí)別丑罪,如rt.jar)的類(lèi)。
- 擴(kuò)展類(lèi)加載器(Extension ClassLoader):負(fù)責(zé)加載 JAVA_HOME\lib\ext 目錄中的核蘸,或通過(guò)java.ext.dirs系統(tǒng)變量指定路徑中的類(lèi)庫(kù)巍糯。
- 應(yīng)用程序類(lèi)加載器(Application ClassLoader): 負(fù)責(zé)加載用戶路徑(classpath)上的類(lèi)庫(kù)。 JVM通過(guò)雙親委派模型進(jìn)行類(lèi)的加載客扎,當(dāng)然我們也可以通過(guò)繼承java.lang.ClassLoader實(shí)現(xiàn)自定義的類(lèi)加載器祟峦。
當(dāng)一個(gè)類(lèi)收到了類(lèi)加載請(qǐng)求,首先不會(huì)嘗試自己去加載這個(gè)類(lèi)徙鱼,而是把這個(gè)請(qǐng)求委派給父類(lèi)去完成宅楞,每一個(gè)層次類(lèi)加載器都是如此针姿,因此所有的加載請(qǐng)求都應(yīng)該傳送到啟動(dòng)類(lèi)加載其中,只有當(dāng)父類(lèi)加載器反饋?zhàn)约簾o(wú)法完成這個(gè)請(qǐng)求的時(shí)候厌衙,一般來(lái)說(shuō)是指在它的加載路徑下沒(méi)有找到所需加載的Class距淫,子類(lèi)加載器才會(huì)嘗試自己去加載。
采用雙親委派的一個(gè)好處是比如加載位于rt.jar包中的類(lèi)java.lang.Object婶希,不管是哪個(gè)加載器加載這個(gè)類(lèi)榕暇,最終都是委托給頂層的啟動(dòng)類(lèi)加載器進(jìn)行加載,這樣就保證了使用不同的類(lèi)加載器最終得到的都是同樣一個(gè)Object對(duì)象喻杈。
由此可見(jiàn)彤枢,使用雙親委派之后蓉媳,外部類(lèi)想要替換系統(tǒng)JDK的類(lèi)時(shí)确徙,或者篡改其實(shí)現(xiàn)時(shí)珍策,父類(lèi)加載器已經(jīng)加載過(guò)的胳嘲,系統(tǒng)JDK子類(lèi)加載器便不會(huì)再次加載闷尿,從而一定程度上防止了危險(xiǎn)代碼的植入坛怪。
4.Java對(duì)象組成結(jié)構(gòu)
Java對(duì)象(Object實(shí)例)結(jié)構(gòu)主要包括對(duì)象頭簿寂、對(duì)象體和對(duì)齊字節(jié)三部分歉铝。
在一個(gè)Java對(duì)象(Object Instance)中谬晕,主要包含對(duì)象頭(Object Header),對(duì)象體(Object Entry),以及對(duì)齊字節(jié)(Byte Alignment)等內(nèi)容碘裕。
換句話說(shuō),一個(gè)JAVA對(duì)象在內(nèi)存中的存儲(chǔ)分布情況固蚤,其抽象成存儲(chǔ)結(jié)構(gòu)娘汞,在Hotspot虛擬機(jī)中,對(duì)象在內(nèi)存中的存儲(chǔ)布局分為 3 塊區(qū)域夕玩,其中:
- 對(duì)象頭(Object Header):對(duì)象頭部信息你弦,主要分為標(biāo)記信息字段,類(lèi)對(duì)象指針燎孟,以及數(shù)組長(zhǎng)度等三部分信息禽作。
- 對(duì)象體(Object Entry):對(duì)象體信息,也叫作實(shí)例數(shù)據(jù)(Instance Data),主要包含對(duì)象的實(shí)例變量(成員變量)揩页,用于成員屬性值旷偿,包括父類(lèi)的成員屬性值。這部分內(nèi)存按4字節(jié)對(duì)齊爆侣。
- 對(duì)齊字節(jié)(Byte Alignment):也叫作填充對(duì)齊(Padding)萍程,其作用是用來(lái)保證Java對(duì)象所占內(nèi)存字節(jié)數(shù)為8的倍數(shù)HotSpot VM的內(nèi)存管理要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍。
一般來(lái)說(shuō)兔仰,對(duì)象頭本身是填充對(duì)齊的參考指標(biāo)是8的倍數(shù)茫负,當(dāng)對(duì)象的實(shí)例變量數(shù)據(jù)不是8的倍數(shù)時(shí),便需要填充數(shù)據(jù)來(lái)保證8字節(jié)的對(duì)齊乎赴。其中忍法,對(duì)于對(duì)象頭來(lái)說(shuō):
- 標(biāo)記信息字段(Mark Word): 主要存儲(chǔ)自身運(yùn)行時(shí)的數(shù)據(jù)潮尝,例如GC標(biāo)志位、哈希碼饿序、鎖狀態(tài)等信息, 用于表示對(duì)象的線程鎖狀態(tài)勉失,另外還可以用來(lái)配合GC存放該對(duì)象的hashCode。
- 類(lèi)對(duì)象指針(Class Pointer): 用于存放方法區(qū)Class對(duì)象的地址原探,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例乱凿。是指向方法區(qū)中Class信息的指針,意味著該對(duì)象可隨時(shí)知道自己是哪個(gè)Class的實(shí)例咽弦。
- 數(shù)組長(zhǎng)度(Array Length): 如果對(duì)象是一個(gè)Java數(shù)組告匠,那么此字段必須有,用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)离唬;如果對(duì)象不是一個(gè)Java數(shù)組,那么此字段不存在划鸽,所以這是一個(gè)可選字段输莺。根據(jù)當(dāng)前JVM的位數(shù)來(lái)決定,只有當(dāng)本對(duì)象是一個(gè)數(shù)組對(duì)象時(shí)才會(huì)有這個(gè)部分裸诽。
其次嫂用,對(duì)于對(duì)象體來(lái)說(shuō),用于保存對(duì)象屬性值丈冬,是對(duì)象的主體部分嘱函,占用的內(nèi)存空間大小取決于對(duì)象的屬性數(shù)量和類(lèi)型。
而對(duì)于對(duì)齊字節(jié)來(lái)說(shuō)埂蕊,并不一定是必然存在的往弓,也沒(méi)有特別的含義,它僅僅起著占位符的作用蓄氧。當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊(8字節(jié)的整數(shù)倍)時(shí)函似,就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。
特別指出喉童,相對(duì)于對(duì)象結(jié)構(gòu)中的字段長(zhǎng)度來(lái)說(shuō)撇寞,其Mark Word、Class Pointer堂氯、Array Length字段的長(zhǎng)度都與JVM的位數(shù)息息相關(guān)蔑担。其中:
- 標(biāo)記信息字段(Mark Word):字段長(zhǎng)度為JVM的一個(gè)Word(字)大小,也就是說(shuō)32位JVM的Mark Word為32位咽白,64位JVM的Mark Word為64位啤握。
- 類(lèi)對(duì)象指針(Class Pointer):字段長(zhǎng)度也為JVM的一個(gè)Word(字)大小,即32位JVM的Mark Word為32位局扶,64位JVM的Mark Word為64位恨统。
也就是說(shuō)叁扫,在32位JVM虛擬機(jī)中,Mark Word和Class Pointer這兩部分都是32位的畜埋;在64位JVM虛擬機(jī)中莫绣,Mark Word和Class Pointer這兩部分都是64位的。
對(duì)于對(duì)象指針而言悠鞍,如果JVM中的對(duì)象數(shù)量過(guò)多对室,使用64位的指針將浪費(fèi)大量?jī)?nèi)存,通過(guò)簡(jiǎn)單統(tǒng)計(jì)咖祭,64位JVM將會(huì)比32位JVM多耗費(fèi)50%的內(nèi)存掩宜。
為了節(jié)約內(nèi)存可以使用選項(xiàng)UseCompressedOops來(lái)開(kāi)啟/關(guān)閉指針壓縮。
其中么翰,UseCompressedOops中的Oop為Ordinary Object Pointer(普通對(duì)象指針)的縮寫(xiě)牺汤。
如果開(kāi)啟UseCompressedOops選項(xiàng),以下類(lèi)型的指針將從64位壓縮至32位:
- Class對(duì)象的屬性指針(靜態(tài)變量)
- Object對(duì)象的屬性指針(成員變量)
- 普通對(duì)象數(shù)組的元素指針浩嫌。
當(dāng)然檐迟,也不是所有的指針都會(huì)壓縮,一些特殊類(lèi)型的指針不會(huì)壓縮码耐,比如指向PermGen(永久代)的Class對(duì)象指針(JDK 8中指向元空間的Class對(duì)象指針)追迟、本地變量、堆棧元素骚腥、入?yún)⒍丶洹⒎祷刂岛蚇ULL指
針等。
在堆內(nèi)存小于32GB的情況下束铭,64位虛擬機(jī)的UseCompressedOops選項(xiàng)是默認(rèn)開(kāi)啟的廓块,該選項(xiàng)表示開(kāi)啟Oop對(duì)象的指針壓縮會(huì)將原來(lái)64位的Oop對(duì)象指針壓縮為32位。其中:
- 手動(dòng)開(kāi)啟Oop對(duì)象指針壓縮的Java指令為:
java -XX:+UseCompressedOops tagretClass<目標(biāo)類(lèi)>
- 手動(dòng)關(guān)閉Oop對(duì)象指針壓縮的Java指令為:
java -XX:-UseCompressedOops tagretClass<目標(biāo)類(lèi)>
如果對(duì)象是一個(gè)數(shù)組契沫,那么對(duì)象頭還需要有額外的空間用于存儲(chǔ)數(shù)組的長(zhǎng)度(Array Length)字段剿骨。
這也就意味著,Array Length字段的長(zhǎng)度也隨著JVM架構(gòu)的不同而不同:在32位JVM上埠褪,長(zhǎng)度為32位浓利;在64位JVM上,長(zhǎng)度為64位钞速。
需要注意的是贷掖,在64位JVM如果開(kāi)啟了Oop對(duì)象的指針壓縮,Array Length字段的長(zhǎng)度也將由64位壓縮至32位渴语。
5.Java對(duì)象創(chuàng)建流程
Java對(duì)象創(chuàng)建流程主要分為對(duì)象實(shí)例化苹威,類(lèi)加載檢測(cè),對(duì)象內(nèi)存分配驾凶,值初始化牙甫,設(shè)置對(duì)象頭掷酗,執(zhí)行初始化等6個(gè)步驟。
在了解完一個(gè)Java對(duì)象組成結(jié)構(gòu)之后窟哺,我們便開(kāi)始進(jìn)入Java對(duì)象創(chuàng)建流程的剖析泻轰,掌握其本質(zhì)有利于我們?cè)趯?shí)際開(kāi)發(fā)工作中,可參考分析一段Java代碼的執(zhí)行后且轨,其在JVM中的產(chǎn)生的結(jié)果和影響浮声。
從大致工作流程來(lái)看,可以分為對(duì)象實(shí)例化旋奢,類(lèi)加載檢測(cè)泳挥,對(duì)象內(nèi)存分配,值初始化至朗,設(shè)置對(duì)象頭屉符,執(zhí)行初始化等6個(gè)步驟。其中:
- 對(duì)象實(shí)例化:一般在Java領(lǐng)域中指通過(guò)new關(guān)鍵字來(lái)實(shí)例化一個(gè)對(duì)象锹引,在此之前Java HotSpot(TM) VM需要進(jìn)行類(lèi)加載檢測(cè)筑煮。
- 類(lèi)加載檢測(cè):進(jìn)行類(lèi)加載檢測(cè),主要是檢測(cè)對(duì)應(yīng)的符號(hào)引用是否被加載和初始化粤蝎,最后才決定類(lèi)是否可以被加載。
- 對(duì)象內(nèi)存分配: 主要是指當(dāng)類(lèi)被加載完成之后袋马,Java HotSpot(TM) VM會(huì)為其分配內(nèi)存并開(kāi)辟內(nèi)存空間初澎,根據(jù)情況來(lái)確定最終內(nèi)存分配方案。
- 值初始化:根據(jù)Java HotSpot(TM) VM為其分配內(nèi)存并開(kāi)辟內(nèi)存空間虑凛,來(lái)進(jìn)行零值初始化碑宴。
- 設(shè)置對(duì)象頭: 完成值初始化之后,設(shè)置對(duì)象頭標(biāo)記對(duì)象實(shí)例桑谍。
- 執(zhí)行初始化: 執(zhí)行初始化函數(shù)延柠,一般是指類(lèi)構(gòu)造函數(shù),并為其設(shè)置相關(guān)屬性锣披。
從Java對(duì)象創(chuàng)建流程的各個(gè)環(huán)節(jié)贞间,具體詳細(xì)來(lái)看,其中:
首先雹仿,對(duì)于對(duì)象實(shí)例化來(lái)說(shuō)增热,主要是看寫(xiě)代碼時(shí),用關(guān)鍵詞class定義一個(gè)類(lèi)其實(shí)只是定義了一個(gè)類(lèi)的模板胧辽,并沒(méi)有在內(nèi)存中實(shí)際產(chǎn)生一個(gè)類(lèi)的實(shí)例對(duì)象峻仇,也沒(méi)有分配內(nèi)存空間。
而要想在內(nèi)存中產(chǎn)生一個(gè)類(lèi)的實(shí)例對(duì)象就需要使用相關(guān)方法申請(qǐng)分配內(nèi)存空間邑商,加上類(lèi)的構(gòu)造方法提供申請(qǐng)空間的大小規(guī)格摄咆,在內(nèi)存中實(shí)際產(chǎn)生一個(gè)類(lèi)的實(shí)例凡蚜,一個(gè)類(lèi)使用此類(lèi)的構(gòu)造方法,執(zhí)行之后就在內(nèi)存中分配了一個(gè)此類(lèi)的內(nèi)存空間吭从,有了內(nèi)存空間就可以向里面存放定義的數(shù)據(jù)和進(jìn)行方法的調(diào)用朝蜘。
在Java領(lǐng)域中,常見(jiàn)的Java對(duì)象實(shí)例化方式主要有:
- JDK提供的New 關(guān)健字:可以調(diào)用任意的構(gòu)造函數(shù)(無(wú)參的和帶參數(shù)的)創(chuàng)建對(duì)象影锈。
- Class的newInstance()方法: 使用Class類(lèi)的newInstance方法創(chuàng)建對(duì)象芹务。其中,newInstance方法調(diào)用無(wú)參的構(gòu)造函數(shù)創(chuàng)建對(duì)象鸭廷。
- Constructor的newInstance()方法: java.lang.reflect.Constructor類(lèi)里也有一個(gè)newInstance方法可以創(chuàng)建對(duì)象枣抱,從而可以通過(guò)newInstance方法調(diào)用有參數(shù)的和私有的構(gòu)造函數(shù)。
- 實(shí)現(xiàn)Cloneable接口并實(shí)現(xiàn)其定義的clone()方法:調(diào)用一個(gè)對(duì)象的clone方法辆床,jvm就會(huì)創(chuàng)建一個(gè)新的對(duì)象佳晶,將前面對(duì)象的內(nèi)容全部拷貝進(jìn)去。用clone方法創(chuàng)建對(duì)象并不會(huì)調(diào)用任何構(gòu)造函數(shù)讼载。
- 反序列化的方式:當(dāng)我們序列化和反序列化一個(gè)對(duì)象轿秧,jvm會(huì)給我們創(chuàng)建一個(gè)單獨(dú)的對(duì)象。在反序列化時(shí)咨堤,Java HotSpot(TM) VM創(chuàng)建對(duì)象并不會(huì)調(diào)用任何構(gòu)造函數(shù)菇篡。
其次,對(duì)于類(lèi)加載檢測(cè)來(lái)說(shuō)一喘,當(dāng)對(duì)象實(shí)例化之前驱还,其Java HotSpot(TM) VM會(huì)自行進(jìn)行檢測(cè),主要是:
- 檢測(cè)對(duì)象實(shí)例化的指令是否在類(lèi)的常量池信息中定位到類(lèi)的符號(hào)引用凸克。
- 檢測(cè)符號(hào)引用是否被加載和初始化议蟆,倘若沒(méi)有的話便對(duì)類(lèi)進(jìn)行加載。
然而萎战,對(duì)于對(duì)象內(nèi)存分配來(lái)說(shuō)咐容,創(chuàng)建一個(gè)對(duì)象所需要的內(nèi)存大小其實(shí)類(lèi)加載完成就已經(jīng)確定,內(nèi)存分配主要是在堆中劃出一塊對(duì)象大小的對(duì)應(yīng)內(nèi)存蚂维。具體的分配方式依據(jù)堆內(nèi)存的對(duì)齊方式來(lái)決定戳粒,而堆內(nèi)存的對(duì)齊方式是根據(jù)當(dāng)前程序的GC機(jī)制來(lái)決定的。
再者虫啥,對(duì)于值初始化來(lái)說(shuō)享郊,這只是依據(jù)Java HotSpot(TM) VM自動(dòng)分配的內(nèi)存對(duì)其進(jìn)行初始化,并設(shè)置為零值孝鹊。
接著炊琉,對(duì)于設(shè)置對(duì)象頭來(lái)說(shuō),就是對(duì)于每一個(gè)進(jìn)入Java HotSpot(TM) VM的對(duì)象實(shí)例進(jìn)行對(duì)象頭信息設(shè)置。
最后苔咪,對(duì)于執(zhí)行初始化來(lái)說(shuō)锰悼,算是Java HotSpot(TM) VM真正意義上的執(zhí)行。
6.Java對(duì)象內(nèi)存分配機(jī)制
Java對(duì)象內(nèi)存分配機(jī)制可以大致分為堆上分配团赏,棧上分配箕般,TLAB分配,以及年代區(qū)分配等方式舔清。
一般來(lái)說(shuō)丝里,在理解Java對(duì)象內(nèi)存分配機(jī)制之前,我們需要明確理解Java領(lǐng)域中的堆(Heap)與棧(Stack)概念体谒,才能更好地掌握和清楚對(duì)應(yīng)到相應(yīng)的Java內(nèi)存模型上去杯聚,主要是大多數(shù)時(shí)候,我們都是把這兩個(gè)結(jié)合起來(lái)講的抒痒,就是常說(shuō)的“堆棧(Heap-Stack)“模型幌绍。其中:
- 堆(Heap): 用來(lái)存放程序動(dòng)態(tài)生成的實(shí)例數(shù)據(jù),是對(duì)象實(shí)例化(一般是指new)之后將其存儲(chǔ)故响,Java HotSpot(TM) VM會(huì)依據(jù)對(duì)象大小在Java Heap中為其開(kāi)辟對(duì)應(yīng)內(nèi)存空間大小傀广。
- 棧(Stack):用來(lái)存放基本數(shù)據(jù)類(lèi)型和引用數(shù)據(jù)類(lèi)型的實(shí)例。一般主要是指實(shí)例對(duì)象的在堆中的首地址彩届,其中每一個(gè)線程都有自己的線程棧伪冰,被線程獨(dú)享。
因此樟蠕,我們可以理解為堆內(nèi)存和棧內(nèi)存的概念贮聂,相對(duì)來(lái)說(shuō):
- 堆內(nèi)存: 用于存儲(chǔ)java中的對(duì)象和數(shù)組,當(dāng)我們new一個(gè)對(duì)象或者創(chuàng)建一個(gè)數(shù)組的時(shí)候坯墨,就會(huì)在堆內(nèi)存中開(kāi)辟一段空間給它,用于存放病往。堆內(nèi)存的特點(diǎn)就是:先進(jìn)先出捣染,后進(jìn)后出。堆可以動(dòng)態(tài)地分配內(nèi)存大小停巷,生存期也不必事先告訴編譯器耍攘,因?yàn)樗窃谶\(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的,但缺點(diǎn)是畔勤,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存蕾各,存取速度較慢。由Java HotSpot(TM) VM虛擬機(jī)的自動(dòng)垃圾回收器來(lái)管理庆揪。
- 棧內(nèi)存: 主要是用來(lái)執(zhí)行程序用的式曲,棧內(nèi)存的特點(diǎn):先進(jìn)后出,后進(jìn)先出。存取速度比堆要快吝羞,僅次于寄存器兰伤,棧數(shù)據(jù)可以共享,但缺點(diǎn)是钧排,存在棧中的數(shù)據(jù)大小與生存必須是確定的敦腔,缺乏靈活性。棧內(nèi)存可以稱(chēng)為一級(jí)緩存恨溜,由垃圾回收器自動(dòng)回收符衔。
Java程序在Java HotSpot(TM) VM中運(yùn)行時(shí),從數(shù)據(jù)在內(nèi)存區(qū)域的分布來(lái)看糟袁,大致可以分為線程私有區(qū)判族,線程共享區(qū),直接內(nèi)存等3大內(nèi)存區(qū)域系吭。其中 :
- 線程私有區(qū)(Thread Local): 線程私有數(shù)據(jù)主要是內(nèi)存區(qū)域主要有程序計(jì)數(shù)器五嫂、虛擬機(jī)棧、本地方法區(qū)肯尺,該區(qū)域生命周期與線程相同, 依賴(lài)用戶線程的啟動(dòng)/結(jié)束 而 創(chuàng)建/銷(xiāo)毀沃缘。
- 線程共享區(qū)(Thread Shared): 線程共享區(qū)的數(shù)據(jù)主要是JAVA 堆、方法區(qū)则吟。其區(qū)域生命周期伴隨虛擬機(jī)的啟動(dòng)/關(guān)閉而創(chuàng)建/銷(xiāo)毀槐臀。
- 直接內(nèi)存(Direct Memory):非JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分, 但也會(huì)被頻繁的使用,不受Java HotSpot(TM) VM中GC控制氓仲。比如水慨,在JDK 1.4引入的NIO提供了基于Channel與Buffer的IO方式, 它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存, 然后使用DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作, 這樣就避免了在Java堆和Native堆中來(lái)回復(fù)制數(shù)據(jù), 因此在一些場(chǎng)景中可以顯著提高性能。
由此可見(jiàn)敬扛,Java堆(Java Heap)是虛擬機(jī)所管理的內(nèi)存中最大的一塊晰洒。Java堆是被所 有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建啥箭。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例谍珊,Java 世界里“幾乎”所有的對(duì)象實(shí)例都在這里分配內(nèi)存。
對(duì)于對(duì)象內(nèi)存分配來(lái)說(shuō)急侥,創(chuàng)建一個(gè)對(duì)象所需要的內(nèi)存大小其實(shí)類(lèi)加載完成就已經(jīng)確定砌滞,內(nèi)存分配主要是在堆中劃出一塊對(duì)象大小的對(duì)應(yīng)內(nèi)存。具體的分配方式依據(jù)堆內(nèi)存的對(duì)齊方式來(lái)決定坏怪,而堆內(nèi)存的對(duì)齊方式是根據(jù)當(dāng)前程序的GC機(jī)制來(lái)決定的贝润。
對(duì)于線程共享區(qū)的數(shù)據(jù)來(lái)說(shuō),常見(jiàn)的對(duì)象在堆內(nèi)存分配主要有:
- 指針碰撞: 主要針對(duì)堆內(nèi)存對(duì)齊的情況
- 空閑列表: 主要針對(duì)堆內(nèi)存無(wú)法對(duì)齊的情況铝宵,相互交錯(cuò)
- CAS自旋鎖和TLAB本地內(nèi)存: 主要針對(duì)分配出現(xiàn)并發(fā)情況的解決方案
對(duì)于線程私有區(qū)的數(shù)據(jù)來(lái)說(shuō)打掘,常見(jiàn)的對(duì)象在堆內(nèi)存分配原則主要有:
- 嘗試棧上分配:滿足棧上分配條件,進(jìn)行棧上分配,否則進(jìn)行嘗試TLAB分配胧卤。
- 嘗試TLAB分配:滿足TLAB分配條件唯绍,進(jìn)行TLAB分配,否則進(jìn)行嘗試?yán)夏甏峙洹?/li>
- 嘗試?yán)夏甏峙洌簼M足老年代分配條件枝誊,進(jìn)行老年代分配况芒,否則嘗試新生代分配。
- 嘗試新生代分配:滿足新生代分配條件叶撒,進(jìn)行新生代分配绝骚。
需要特別注意的是,不論是否能進(jìn)行分配都是在Eden區(qū)進(jìn)行分配的祠够,主要是當(dāng)出現(xiàn)多個(gè)線程同時(shí)創(chuàng)建一個(gè)對(duì)象的時(shí)候压汪,TLAB分配做了優(yōu)化,Java HotSpot(TM) VM虛擬機(jī)會(huì)在Eden區(qū)為其分配一塊共享空間給其線程使用古瓤。
Java對(duì)象成員初始化順序大致順序?yàn)殪o態(tài)代碼快/靜態(tài)變量->非靜態(tài)代碼快/普通變量->一般類(lèi)構(gòu)造方法止剖,其中:
按照J(rèn)ava程序代碼執(zhí)行的順序來(lái)看,被static修飾的變量和代碼塊肯定是優(yōu)先初始化的落君,其次結(jié)合繼承的思想穿香,父類(lèi)要比子類(lèi)優(yōu)先初始化,最后才是一般構(gòu)造方法绎速。
寫(xiě)在最后
Java源碼依據(jù)JDK提供的API來(lái)組織有效的代碼實(shí)體皮获,一般都是通過(guò)調(diào)用API來(lái)編織和組成代碼的。
Java編譯機(jī)制主要可以分為編譯前端和編譯后端兩個(gè)階段纹冤,一般來(lái)說(shuō)主要是指將源代碼翻譯為目標(biāo)代碼的過(guò)程洒宝,稱(chēng)為編譯過(guò)程。
Java類(lèi)加載機(jī)制主要分為加載萌京,驗(yàn)證雁歌,準(zhǔn)備,解析知残,初始化等5個(gè)階段靠瞎。
Java對(duì)象(Object實(shí)例)結(jié)構(gòu)主要包括對(duì)象頭、對(duì)象體和對(duì)齊字節(jié)三部分橡庞。
Java對(duì)象內(nèi)存分配機(jī)制可以大致分為堆上分配较坛,棧上分配印蔗,TLAB分配扒最,以及年代區(qū)分配等方式。
綜上所述华嘹,一個(gè)Java對(duì)象從創(chuàng)建到被托管給JVM時(shí)吧趣,會(huì)經(jīng)歷和完成上面的一系列工作。
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循相關(guān)版權(quán)協(xié)議强挫,如若轉(zhuǎn)載或者分享請(qǐng)附上原文出處鏈接和鏈接來(lái)源岔霸。