最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

全網(wǎng)最硬核 JVM 內(nèi)存解析 - 13.JVM 線程內(nèi)存設(shè)計(jì)

2023-04-28 08:23 作者:干貨滿滿張哈希  | 我要投稿

個(gè)人創(chuàng)作公約:本人聲明創(chuàng)作的所有文章皆為自己原創(chuàng),如果有參考任何文章的地方,會(huì)標(biāo)注出來,如果有疏漏,歡迎大家批判。如果大家發(fā)現(xiàn)網(wǎng)上有抄襲本文章的,歡迎舉報(bào),并且積極向這個(gè)?github 倉(cāng)庫(kù)?提交 issue,謝謝支持~
另外,本文為了避免抄襲,會(huì)在不影響閱讀的情況下,在文章的隨機(jī)位置放入對(duì)于抄襲和洗稿的人的“親切”的問候。如果是正常讀者看到,筆者在這里說聲對(duì)不起,。如果被抄襲狗或者洗稿狗看到了,希望你能夠好好反思,不要再抄襲了,謝謝。
今天又是干貨滿滿的一天,這是全網(wǎng)最硬核 JVM 解析系列第四篇,往期精彩:

  • 全網(wǎng)最硬核 TLAB 解析

  • 全網(wǎng)最硬核 Java 隨機(jī)數(shù)解析

  • 全網(wǎng)最硬核 Java 新內(nèi)存模型解析

本篇是關(guān)于 JVM 內(nèi)存的詳細(xì)分析。網(wǎng)上有很多關(guān)于 JVM 內(nèi)存結(jié)構(gòu)的分析以及圖片,但是由于不是一手的資料亦或是人云亦云導(dǎo)致有很錯(cuò)誤,造成了很多誤解;并且,這里可能最容易混淆的是一邊是 JVM Specification 的定義,一邊是 Hotspot JVM 的實(shí)際實(shí)現(xiàn),有時(shí)候人們一些部分說的是 JVM Specification,一部分說的是 Hotspot 實(shí)現(xiàn),給人一種割裂感與誤解。本篇主要從 Hotspot 實(shí)現(xiàn)出發(fā),以 Linux x86 環(huán)境為主,緊密貼合 JVM 源碼并且輔以各種 JVM 工具驗(yàn)證幫助大家理解 JVM 內(nèi)存的結(jié)構(gòu)。但是,本篇僅限于對(duì)于這些內(nèi)存的用途,使用限制,相關(guān)參數(shù)的分析,有些地方可能比較深入,有些地方可能需要結(jié)合本身用這塊內(nèi)存涉及的 JVM 模塊去說,會(huì)放在另一系列文章詳細(xì)描述。最后,洗稿抄襲狗不得 house

本篇全篇目錄(以及涉及的 JVM 參數(shù)):

  1. 從 Native Memory Tracking 說起(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 1.從 Native Memory Tracking 說起開始)

    1. Native Memory Tracking 的開啟

    2. Native Memory Tracking 的使用(涉及 JVM 參數(shù):NativeMemoryTracking

    3. Native Memory Tracking 的 summary 信息每部分含義

    4. Native Memory Tracking 的 summary 信息的持續(xù)監(jiān)控

    5. 為何 Native Memory Tracking 中申請(qǐng)的內(nèi)存分為 reserved 和 committed

  2. JVM 內(nèi)存申請(qǐng)與使用流程(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 2.JVM 內(nèi)存申請(qǐng)與使用流程開始)

    1. Linux 大頁(yè)分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)

    2. Linux 大頁(yè)分配方式 - Transparent Huge Pages (THP)

    3. JVM 大頁(yè)分配相關(guān)參數(shù)與機(jī)制(涉及 JVM 參數(shù):UseLargePages,UseHugeTLBFS,UseSHM,UseTransparentHugePages,LargePageSizeInBytes

    4. JVM commit 的內(nèi)存與實(shí)際占用內(nèi)存的差異

    5. Linux 下內(nèi)存管理模型簡(jiǎn)述

    6. JVM commit 的內(nèi)存與實(shí)際占用內(nèi)存的差異

    7. 大頁(yè)分配 UseLargePages(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 3.大頁(yè)分配 UseLargePages開始)

  3. Java 堆內(nèi)存相關(guān)設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 4.Java 堆內(nèi)存大小的確認(rèn)開始)

    1. 驗(yàn)證?32-bit?壓縮指針模式

    2. 驗(yàn)證?Zero based?壓縮指針模式

    3. 驗(yàn)證?Non-zero disjoint?壓縮指針模式

    4. 驗(yàn)證?Non-zero based?壓縮指針模式

    5. 壓縮對(duì)象指針存在的意義(涉及 JVM 參數(shù):ObjectAlignmentInBytes

    6. 壓縮對(duì)象指針與壓縮類指針的關(guān)系演進(jìn)(涉及 JVM 參數(shù):UseCompressedOops,UseCompressedClassPointers

    7. 壓縮對(duì)象指針的不同模式與尋址優(yōu)化機(jī)制(涉及 JVM 參數(shù):ObjectAlignmentInBytes,HeapBaseMinAddress

    8. 通用初始化與擴(kuò)展流程

    9. 直接指定三個(gè)指標(biāo)的方式(涉及 JVM 參數(shù):MaxHeapSize,MinHeapSize,InitialHeapSize,Xmx,Xms

    10. 不手動(dòng)指定三個(gè)指標(biāo)的情況下,這三個(gè)指標(biāo)(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何計(jì)算的

    11. 壓縮對(duì)象指針相關(guān)機(jī)制(涉及 JVM 參數(shù):UseCompressedOops)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 5.壓縮對(duì)象指針相關(guān)機(jī)制開始)

    12. 為何預(yù)留第 0 頁(yè),壓縮對(duì)象指針 null 判斷擦除的實(shí)現(xiàn)(涉及 JVM 參數(shù):HeapBaseMinAddress

    13. 結(jié)合壓縮對(duì)象指針與前面提到的堆內(nèi)存限制的初始化的關(guān)系(涉及 JVM 參數(shù):HeapBaseMinAddress,ObjectAlignmentInBytes,MinHeapSize,MaxHeapSize,InitialHeapSize

    14. 使用 jol + jhsdb + JVM 日志查看壓縮對(duì)象指針與 Java 堆驗(yàn)證我們前面的結(jié)論

    15. 堆大小的動(dòng)態(tài)伸縮(涉及 JVM 參數(shù):MinHeapFreeRatio,MaxHeapFreeRatio,MinHeapDeltaBytes)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 6.其他 Java 堆內(nèi)存相關(guān)的特殊機(jī)制開始)

    16. 適用于長(zhǎng)期運(yùn)行并且盡量將所有可用內(nèi)存被堆使用的 JVM 參數(shù) AggressiveHeap

    17. JVM 參數(shù) AlwaysPreTouch 的作用

    18. JVM 參數(shù) UseContainerSupport - JVM 如何感知到容器內(nèi)存限制

    19. JVM 參數(shù) SoftMaxHeapSize - 用于平滑遷移更耗內(nèi)存的 GC 使用

  4. JVM 元空間設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 7.元空間存儲(chǔ)的元數(shù)據(jù)開始)

    1. jcmd <pid> VM.metaspace?元空間說明

    2. 元空間相關(guān) JVM 日志

    3. 元空間 JFR 事件詳解

    4. jdk.MetaspaceSummary?元空間定時(shí)統(tǒng)計(jì)事件

    5. jdk.MetaspaceAllocationFailure?元空間分配失敗事件

    6. jdk.MetaspaceOOM?元空間 OOM 事件

    7. jdk.MetaspaceGCThreshold?元空間 GC 閾值變化事件

    8. jdk.MetaspaceChunkFreeListSummary?元空間 Chunk FreeList 統(tǒng)計(jì)事件

    9. CommitLimiter?的限制元空間可以 commit 的內(nèi)存大小以及限制元空間占用達(dá)到多少就開始嘗試 GC

    10. 每次 GC 之后,也會(huì)嘗試重新計(jì)算?_capacity_until_GC

    11. 首先類加載器 1 需要分配 1023 字節(jié)大小的內(nèi)存,屬于類空間

    12. 然后類加載器 1 還需要分配 1023 字節(jié)大小的內(nèi)存,屬于類空間

    13. 然后類加載器 1 需要分配 264 KB 大小的內(nèi)存,屬于類空間

    14. 然后類加載器 1 需要分配 2 MB 大小的內(nèi)存,屬于類空間

    15. 然后類加載器 1 需要分配 128KB 大小的內(nèi)存,屬于類空間

    16. 新來一個(gè)類加載器 2,需要分配 1023 Bytes 大小的內(nèi)存,屬于類空間

    17. 然后類加載器 1 被 GC 回收掉

    18. 然后類加載器 2 需要分配 1 MB 大小的內(nèi)存,屬于類空間

    19. 元空間的整體配置以及相關(guān)參數(shù)(涉及 JVM 參數(shù):MetaspaceSize,MaxMetaspaceSize,MinMetaspaceExpansion,MaxMetaspaceExpansion,MaxMetaspaceFreeRatio,MinMetaspaceFreeRatio,UseCompressedClassPointers,CompressedClassSpaceSize,CompressedClassSpaceBaseAddress,MetaspaceReclaimPolicy

    20. 元空間上下文?MetaspaceContext

    21. 虛擬內(nèi)存空間節(jié)點(diǎn)列表?VirtualSpaceList

    22. 虛擬內(nèi)存空間節(jié)點(diǎn)?VirtualSpaceNode?與?CompressedClassSpaceSize

    23. MetaChunk

    24. 類加載的入口?SystemDictionary?與保留所有?ClassLoaderData?的?ClassLoaderDataGraph

    25. 每個(gè)類加載器私有的?ClassLoaderData?以及?ClassLoaderMetaspace

    26. 管理正在使用的?MetaChunk?的?MetaspaceArena

    27. 元空間內(nèi)存分配流程(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 9.元空間內(nèi)存分配流程開始)

    28. ClassLoaderData?回收

    29. ChunkHeaderPool?池化?MetaChunk?對(duì)象

    30. ChunkManager?管理空閑的?MetaChunk

    31. 類加載器到?MetaSpaceArena?的流程

    32. 從?MetaChunkArena?普通分配 - 整體流程

    33. 從?MetaChunkArena?普通分配 -?FreeBlocks?回收老的?current chunk?與用于后續(xù)分配的流程

    34. 從?MetaChunkArena?普通分配 - 嘗試從?FreeBlocks?分配

    35. 從?MetaChunkArena?普通分配 - 嘗試擴(kuò)容?current chunk

    36. 從?MetaChunkArena?普通分配 - 從?ChunkManager?分配新的?MetaChunk

    37. 從?MetaChunkArena?普通分配 - 從?ChunkManager?分配新的?MetaChunk?- 從?VirtualSpaceList?申請(qǐng)新的?RootMetaChunk

    38. 從?MetaChunkArena?普通分配 - 從?ChunkManager?分配新的?MetaChunk?- 將?RootMetaChunk?切割成為需要的?MetaChunk

    39. MetaChunk?回收 - 不同情況下,?MetaChunk?如何放入?FreeChunkListVector

    40. 什么時(shí)候用到元空間,以及釋放時(shí)機(jī)

    41. 元空間保存什么

    42. 什么是元數(shù)據(jù),為什么需要元數(shù)據(jù)

    43. 什么時(shí)候用到元空間,元空間保存什么

    44. 元空間的核心概念與設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 8.元空間的核心概念與設(shè)計(jì)開始)

    45. 元空間分配與回收流程舉例(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 10.元空間分配與回收流程舉例開始)

    46. 元空間大小限制與動(dòng)態(tài)伸縮(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 11.元空間分配與回收流程舉例開始)

    47. jcmd VM.metaspace?元空間說明、元空間相關(guān) JVM 日志以及元空間 JFR 事件詳解(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 12.元空間各種監(jiān)控手段開始)

  5. JVM 線程內(nèi)存設(shè)計(jì)(重點(diǎn)研究 Java 線程)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 13.JVM 線程內(nèi)存設(shè)計(jì)開始)

    1. 解釋執(zhí)行與編譯執(zhí)行時(shí)候的判斷(x86為例)

    2. 一個(gè) Java 線程 Xss 最小能指定多大

    3. JVM 中有哪幾種線程,對(duì)應(yīng)線程棧相關(guān)的參數(shù)是什么(涉及 JVM 參數(shù):ThreadStackSize,VMThreadStackSize,CompilerThreadStackSize,StackYellowPages,StackRedPages,StackShadowPages,StackReservedPages,RestrictReservedStack

    4. Java 線程棧內(nèi)存的結(jié)構(gòu)

    5. Java 線程如何拋出的 StackOverflowError

5. JVM 線程內(nèi)存設(shè)計(jì)(重點(diǎn)研究 Java 線程)

Java 19 中 Loom 終于 Preview 了,虛擬線程(VirtualThread)是我期待已久的特性,但是這里我們說的線程內(nèi)存,并不是這種 虛擬線程,還是老的線程。其實(shí)新的虛擬線程,在線程內(nèi)存結(jié)構(gòu)上并沒有啥變化,只是存儲(chǔ)位置的變化,實(shí)際的負(fù)載線程(CarrierThread)還是老的線程。

同時(shí),JVM 線程占用的內(nèi)存分為兩個(gè)部分:分別是線程棧占用內(nèi)存,以及線程本身數(shù)據(jù)結(jié)構(gòu)占用的內(nèi)存。

5.1. JVM 中有哪幾種線程,對(duì)應(yīng)線程棧相關(guān)的參數(shù)是什么

JVM 中有如下幾類線程:

  • VM 線程:全局唯一的線程,負(fù)責(zé)執(zhí)行?VM Operations,例如 JVM 的初始化,其中的操作大部分需要在安全點(diǎn)執(zhí)行,即 Stop the world 的時(shí)候執(zhí)行。所有的操作請(qǐng)參考:https://github.com/openjdk/jdk/blob/jdk-21+17/src/hotspot/share/runtime/vmOperation.hpp

  • GC 線程:負(fù)責(zé)做 GC 操作的線程

  • Java 線程:包括 Java 應(yīng)用線程(java.lang.Thread),以及?CodeCacheSweeper?線程,?JVMTI?的 Agent 與 Service 線程其實(shí)也是 JAva 線程。

  • 編譯器線程: JIT 編譯器的線程,有 C1 和 C2 線程(xi稿滾去shi)

  • 定時(shí)任務(wù)時(shí)鐘線程:全局唯一的線程,即 Watcher 線程,負(fù)責(zé)計(jì)時(shí)并執(zhí)行定時(shí)任務(wù),目前 JVM 中包括的定時(shí)任務(wù)可以通過查看繼承?PeriodicTask?的類看到,其中兩個(gè)比較重要的任務(wù)是:

    • StatSamplerTask:定時(shí)更新采集的 JVM Performance Data(PerfData)數(shù)據(jù), 包括 GC、類加載、運(yùn)行采集等等數(shù)據(jù),這個(gè)任務(wù)多久執(zhí)行一次是通過?-XX:PerfDataSamplingInterval?參數(shù)控制的,默認(rèn)為 50 毫秒(參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/share/runtime/globals.hpp)。這些數(shù)據(jù)一般通過 jstat 讀取,或者通過 JMX 讀取。

    • VMOperationTimeoutTask:由于 VM 線程是單線程,執(zhí)行?VM Operations,單個(gè)任務(wù)執(zhí)行不能太久,否則會(huì)阻塞其他?VM Operations。所以每次執(zhí)行?VM Operations?的時(shí)候,這個(gè)定時(shí)任務(wù)都會(huì)檢查當(dāng)前執(zhí)行了多久,如果超過?-XX:AbortVMOnVMOperationTimeoutDelay?就會(huì)報(bào)警。AbortVMOnVMOperationTimeoutDelay?默認(rèn)是 1000ms(參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/share/runtime/globals.hpp)。

  • 異步日志線程:全局唯一的線程, Java 17 引入的異步 JVM 日志特性,防止因?yàn)?JVM 日志輸出阻塞影響全局安全點(diǎn)事件導(dǎo)致全局暫停過長(zhǎng),或者 JVM 日志輸出導(dǎo)致線程阻塞,負(fù)責(zé)異步寫日志,通過?-Xlog:async?啟用 JVM 異步日志,通過?-XX:AsyncLogBufferSize=?指定異步日志緩沖大小,這個(gè)大小默認(rèn)是?2097152?即?2MB

  • JFR 采樣線程:全局唯一的線程,負(fù)責(zé)采集 JFR 中的兩種采樣事件,一個(gè)是?jdk.ExecutionSample,另一個(gè)是?jdk.NativeMethodSample,都是采樣當(dāng)前正在?RUNNING?的線程,如果線程在執(zhí)行 Java 代碼,就屬于?jdk.ExecutionSample,如果執(zhí)行 native 方法,就屬于?jdk.NativeMethodSample

相關(guān)的參數(shù)有:

  • ThreadStackSize:每個(gè)?Java 線程的棧大小,這個(gè)參數(shù)通過?-Xss?也可以指定,各種平臺(tái)的默認(rèn)值為:

    • linux 平臺(tái),x86 CPU,默認(rèn)為 1024 KB,參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp

    • linux 平臺(tái),aarch CPU,默認(rèn)為 2048 KB,參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp

    • windows 平臺(tái),x86 CPU,默認(rèn)為 0,即使用操作系統(tǒng)默認(rèn)值(64 位虛擬機(jī)為 1024KB),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp

    • windows 平臺(tái),aarch CPU,默認(rèn)為 0,即使用操作系統(tǒng)默認(rèn)值(64 位虛擬機(jī)為 1024KB),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp

  • VMThreadStackSizeVM 線程,GC 線程定時(shí)任務(wù)時(shí)鐘線程,異步日志線程,JFR 采樣線程的棧大小,各種平臺(tái)的默認(rèn)值為:

    • linux 平臺(tái),x86 CPU,默認(rèn)為 1024 KB,參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp

    • linux 平臺(tái),aarch CPU,默認(rèn)為 2048 KB,參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp

    • windows 平臺(tái),x86 CPU,默認(rèn)為 0,即使用操作系統(tǒng)默認(rèn)值(64 位虛擬機(jī)為 1024KB),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp

    • windows 平臺(tái),aarch CPU,默認(rèn)為 0,即使用操作系統(tǒng)默認(rèn)值(64 位虛擬機(jī)為 1024KB),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp

  • CompilerThreadStackSize編譯器線程的棧大小,各種平臺(tái)的默認(rèn)值為:

    • linux 平臺(tái),x86 CPU,默認(rèn)為 1024 KB,參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp

    • linux 平臺(tái),aarch CPU,默認(rèn)為 2048 KB,參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp

    • windows 平臺(tái),x86 CPU,默認(rèn)為 0,即使用操作系統(tǒng)默認(rèn)值(64 位虛擬機(jī)為 1024KB),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp

    • windows 平臺(tái),aarch CPU,默認(rèn)為 0,即使用操作系統(tǒng)默認(rèn)值(64 位虛擬機(jī)為 1024KB),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp

  • StackYellowPages:后面會(huì)提到并分析的黃色區(qū)域的頁(yè)大小

    • linux 平臺(tái),x86 CPU,默認(rèn)為 2 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp

    • linux 平臺(tái),aarch CPU,默認(rèn)為 2 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp

    • windows 平臺(tái),x86 CPU,默認(rèn)為 3 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp

    • windows 平臺(tái),aarch CPU,默認(rèn)為 2 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp

  • StackRedPages:后面會(huì)提到并分析的紅色區(qū)域的頁(yè)大小

    • linux 平臺(tái),x86 CPU,默認(rèn)為 1 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp

    • linux 平臺(tái),aarch CPU,默認(rèn)為 1 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp

    • windows 平臺(tái),x86 CPU,默認(rèn)為 1 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp

    • windows 平臺(tái),aarch CPU,默認(rèn)為 1 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp

  • StackShadowPages:后面會(huì)提到并分析的影子區(qū)域的頁(yè)大小

    • linux 平臺(tái),x86 CPU,默認(rèn)為 20 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp

    • linux 平臺(tái),aarch CPU,默認(rèn)為 20 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp

    • windows 平臺(tái),x86 CPU,默認(rèn)為 8 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp

    • windows 平臺(tái),aarch CPU,默認(rèn)為 20 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp

  • StackReservedPages:后面會(huì)提到并分析的保留區(qū)域的頁(yè)大小

    • linux 平臺(tái),x86 CPU,默認(rèn)為 1 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp

    • linux 平臺(tái),aarch CPU,默認(rèn)為 1 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp

    • windows 平臺(tái),x86 CPU,默認(rèn)為 0 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp

    • windows 平臺(tái),aarch CPU,默認(rèn)為 1 頁(yè),參考:https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp

  • RestrictReservedStack:默認(rèn)為 true,與保留區(qū)域相關(guān),保留區(qū)域會(huì)保護(hù)臨界區(qū)代碼(例如?ReentrantLock)在拋出?StackOverflow?之前先把臨界區(qū)代碼執(zhí)行完再結(jié)束,防止臨界區(qū)代碼執(zhí)行到一半就拋出?StackOverflow?導(dǎo)致狀態(tài)不一致導(dǎo)致這個(gè)鎖之后再也用不了了。標(biāo)記臨界區(qū)代碼的注解是?@jdk.internal.vm.annotation.ReservedStackAccess。在這個(gè)配置為 true 的時(shí)候,這個(gè)注解默認(rèn)只能 jdk 內(nèi)部代碼使用,如果你有類似于?ReentrantLock?這種帶有臨界區(qū)的代碼也想保護(hù)起來,可以設(shè)置?-XX:-RestrictReservedStack,關(guān)閉對(duì)于?@jdk.internal.vm.annotation.ReservedStackAccess?的限制,這樣你就可以在自己的代碼中使用這個(gè)注解了。

我們接下來重點(diǎn)分析 Java 線程棧。

5.2. Java 線程棧內(nèi)存的結(jié)構(gòu)

熟悉編譯器的人應(yīng)該知道激活記錄(Activation Record)這個(gè)概念,它是一種數(shù)據(jù)結(jié)構(gòu),其中包含支持一次函數(shù)調(diào)用所需的所有信息。它包含該函數(shù)的所有局部變量,以及指向另一個(gè)激活記錄的引用(或指針),其實(shí)你可以簡(jiǎn)單理解為,每多一次方法調(diào)用就多一個(gè)激活記錄。而線程棧幀(Stack Frame),就是激活記錄的實(shí)際實(shí)現(xiàn)。每在代碼中多一次方法調(diào)用就多一個(gè)棧幀,但是這個(gè)說法并不嚴(yán)謹(jǐn),比如,JIT 可能會(huì)內(nèi)聯(lián)一些方法,可能會(huì)跳過某些方法的調(diào)用等等。Java 線程的棧幀有哪幾種呢,其實(shí)根據(jù) Java 線程執(zhí)行的方法有 Java 方法以及原生方法(Native)就能推測(cè)出有兩種:

  • Java 虛擬機(jī)棧幀(Java Virtual Machine Stack Frame):用于保存 Java 方法的執(zhí)行狀態(tài),包括局部變量表、操作數(shù)棧、方法出口等信息。

  • Native 方法棧幀(Native Method Stack Frame):用于保存 Native 方法的執(zhí)行狀態(tài),包括局部變量表、操作數(shù)棧、方法出口等信息。

在最早的時(shí)候,Linux 還沒有線程的概念,Java 自己做了一種叫做?Green Thread?的東西即用戶態(tài)線程(與現(xiàn)在的虛擬線程設(shè)計(jì)差異很大,不是一個(gè)概念了),但是調(diào)度有諸多問題,所以在 Linux 有線程之后,Java 也舍棄了?Green Thread。Java 線程其實(shí)底層就是通過操作系統(tǒng)線程實(shí)現(xiàn),是一一對(duì)應(yīng)的關(guān)系。不過現(xiàn)在,虛擬線程也快要 release 了,但是這個(gè)并不是今天的重點(diǎn)。并且,在最早的時(shí)候,Java 線程棧與 Native 線程棧也是分開的,雖然可能都是一個(gè)線程執(zhí)行的。后來,發(fā)現(xiàn)這樣做對(duì)于 JIT 優(yōu)化,以及線程棧大小限制,以及實(shí)現(xiàn)高效的 StackOverflow 檢查都不利,所以就把 Java 線程棧與 Native 線程棧合并了,這樣就只有一個(gè)線程棧了。

JVM 中對(duì)于線程??梢允褂玫目臻g是限制死的。對(duì)于 Java 線程來說,這個(gè)限制是由?-Xss?或者?-XX:ThreadStackSize?來控制的-Xss?或者?-XX:ThreadStackSize?基本等價(jià), 一般來說,-Xss?或者?-XX:ThreadStackSize?是用來設(shè)置每個(gè)線程的棧大小的,但是更嚴(yán)謹(jǐn)?shù)恼f法是,它是設(shè)置每個(gè)線程棧最大使用的內(nèi)存大小,并且實(shí)際可用的大小由于保護(hù)頁(yè)的存在還要小于這個(gè)值,并且設(shè)置這個(gè)值不能小于保護(hù)頁(yè)需要的大小,否則沒有意義。根據(jù)前面對(duì)于 JVM 其他區(qū)域的分析我們可以推測(cè)出,對(duì)于每個(gè)線程,都會(huì)先?Reserve?出?-Xss?或者?-XX:ThreadStackSize?大小的內(nèi)存,之后隨著線程占用內(nèi)存升高而不斷?Commit?內(nèi)存。

同時(shí)我們還知道,對(duì)于一段 Java 代碼,分為編譯器執(zhí)行,C1 執(zhí)行,C2 執(zhí)行三種情況,因此,一個(gè) Java 線程的棧內(nèi)存結(jié)構(gòu)可能如下圖所示:

這個(gè)圖片我們展示了一個(gè)比較極端的情況,線程先解釋執(zhí)行方法 1,之后調(diào)用并解釋執(zhí)行方法 2,然后調(diào)用一個(gè)可能比較熱點(diǎn)的方法 3,方法 3 已經(jīng)被 C1 優(yōu)化編譯,這里執(zhí)行的是編譯后的代碼,之后調(diào)用可能更熱點(diǎn)的方法 4,方法 4 已經(jīng)被 C2 優(yōu)化編譯,這里執(zhí)行的是編譯后的代碼。最后方法 4 還需要調(diào)用一個(gè) native 方法 5。

5.3. Java 線程如何拋出的 StackOverflowError

JVM 線程內(nèi)存還有一些特殊的內(nèi)存區(qū)域,結(jié)構(gòu)如下:

  • 保護(hù)區(qū)域(Guard Zone),保護(hù)區(qū)的內(nèi)存沒有映射物理內(nèi)存,訪問的話會(huì)像前面第三章提到的?NullPointerException?優(yōu)化方式類似,即拋出?SIGSEGV?被 JVM 捕獲,再拋出?StackOverflowError。保護(hù)區(qū)包括以下三種:

    • 黃色區(qū)域(Yellow Zone):大小由前面提到的?-XX:StackYellowPages?參數(shù)決定。如果棧擴(kuò)展到了黃色區(qū)域,則發(fā)生?SIGSEGV,并且信號(hào)處理程序拋出?StackOverflowError?并繼續(xù)執(zhí)行當(dāng)前線程。同時(shí),這時(shí)候黃色頁(yè)面會(huì)被映射分配內(nèi)存,以提供一些額外的棧空間給異常拋出的代碼使用,拋出異常結(jié)束后,黃色頁(yè)面會(huì)重新去掉映射,變成保護(hù)區(qū)。

    • 紅色區(qū)域(Red Zone):大小由前面提到的?-XX:StackRedPages?參數(shù)決定。正常的代碼只會(huì)可能到黃色區(qū)域,只有 JVM 出一些 bug 的時(shí)候會(huì)到紅色區(qū)域,這個(gè)相當(dāng)于最后一層保證。保留這個(gè)區(qū)域是為了出這種 bug 的時(shí)候,能有空間可以將錯(cuò)誤信息寫入?hs_err_pid.log?文件用于定位。

    • 保留區(qū)域(Reserved Zone):大小由前面提到的?-XX:StackReservedPages?參數(shù)決定。在 Java 9 引入(JEP 270: Reserved Stack Areas for Critical Sections)(洗稿狗的區(qū)域是細(xì)狗區(qū)),主要是為了解決 JDK 內(nèi)部的臨界區(qū)代碼(例如ReentrantLock)導(dǎo)致?StackOverflowError?的時(shí)候保證內(nèi)部數(shù)據(jù)結(jié)構(gòu)不會(huì)處于不一致的狀態(tài)導(dǎo)致鎖無法釋放或者被獲取。如果沒有這個(gè)區(qū)域,在?ReentrantLock.lock()?方法內(nèi)部調(diào)用某個(gè)內(nèi)部方法的時(shí)候可能會(huì)進(jìn)入黃色區(qū)域,導(dǎo)致?StackOverflowError,這時(shí)候可能?ReentrantLock?內(nèi)部的一些數(shù)據(jù)可能已經(jīng)修改,拋出異常導(dǎo)致這些數(shù)據(jù)無法回滾讓鎖處于當(dāng)初設(shè)計(jì)的時(shí)候沒有設(shè)計(jì)的不一致狀態(tài)。為了避免這個(gè)情況,引入保留區(qū)域。在執(zhí)行臨界區(qū)方法的時(shí)候(被?@jdk.internal.vm.annotation.ReservedStackAccess?注解修飾的方法),如果進(jìn)入保留區(qū)域,那么保留區(qū)域會(huì)被映射內(nèi)存,用于執(zhí)行完臨界區(qū)方法,執(zhí)行完臨界區(qū)方法之后,再拋出?StackOverflowError,并解除保留區(qū)域的映射。另外,前面我們提到過,@jdk.internal.vm.annotation.ReservedStackAccess?這個(gè)注解默認(rèn)只能 jdk 內(nèi)部代碼使用,如果你有類似于?ReentrantLock?這種帶有臨界區(qū)的代碼也想保護(hù)起來,可以設(shè)置?-XX:-RestrictReservedStack,關(guān)閉對(duì)于?@jdk.internal.vm.annotation.ReservedStackAccess?的限制,這樣你就可以在自己的代碼中使用這個(gè)注解了。

  • 影子區(qū)域(Shadow Zone):這個(gè)區(qū)域的大小由前面提到的?-XX:StackShadowPages?參數(shù)決定。影子區(qū)域只是抽象概念,跟在當(dāng)前棧占用的頂部棧幀后面,隨著頂部棧幀變化而變化。這個(gè)區(qū)域用于保證 Native 調(diào)用不會(huì)導(dǎo)致?StackOverflowError在后面的分析我們會(huì)看到,每次調(diào)用方法前需要估算方法棧幀的占用大小,但是對(duì)于 Native 調(diào)用我們無法估算,所以我們就假設(shè) Native 大小最大不會(huì)超過影子區(qū)域大小,在發(fā)生?Native?調(diào)用前,會(huì)查看當(dāng)前棧幀位置加上影子區(qū)域大小是否會(huì)達(dá)到保留區(qū)域,如果達(dá)到了保留區(qū)域,那么會(huì)拋出?StackOverflowError,如果沒有達(dá)到保留區(qū)域,那么會(huì)繼續(xù)執(zhí)行。這里我們可以看出,JVM 假設(shè) Native 調(diào)用占用空間不會(huì)超過影子區(qū)域大小,JDK 中自帶的 native 調(diào)用也確實(shí)是這樣。如果你自己實(shí)現(xiàn)了 Native 方法并且會(huì)占用大量棧內(nèi)存,那么你需要調(diào)整?StackShadowPages。

我們看下源碼中如何體現(xiàn)的這些區(qū)域,參考源碼:https://github.com/openjdk/jdk/blob/jdk-21%2B18/src/hotspot/share/runtime/stackOverflow.hpp

size_t StackOverflow::_stack_red_zone_size = 0;
size_t StackOverflow::_stack_yellow_zone_size = 0;
size_t StackOverflow::_stack_reserved_zone_size = 0;
size_t StackOverflow::_stack_shadow_zone_size = 0;

void StackOverflow::initialize_stack_zone_sizes() {
?//讀取虛擬機(jī)頁(yè)大小,第二章我們分析過
?size_t page_size = os::vm_page_size();
?//目前各個(gè)平臺(tái)最小頁(yè)大小基本都是 4K
?size_t unit = 4*K;
?//使用 StackRedPages 乘以 4K 然后對(duì)虛擬機(jī)頁(yè)大小進(jìn)行對(duì)齊作為紅色區(qū)域大小
?assert(_stack_red_zone_size == 0, "This should be called only once.");
?_stack_red_zone_size = align_up(StackRedPages * unit, page_size);
?//使用 StackYellowPages 乘以 4K 然后對(duì)虛擬機(jī)頁(yè)大小進(jìn)行對(duì)齊作為黃色區(qū)域大小
?assert(_stack_yellow_zone_size == 0, "This should be called only once.");
?_stack_yellow_zone_size = align_up(StackYellowPages * unit, page_size);
?//使用 StackReservedPages 乘以 4K 然后對(duì)虛擬機(jī)頁(yè)大小進(jìn)行對(duì)齊作為保留區(qū)域大小
?assert(_stack_reserved_zone_size == 0, "This should be called only once.");
?_stack_reserved_zone_size = align_up(StackReservedPages * unit, page_size);
?//使用 StackShadowPages 乘以 4K 然后對(duì)虛擬機(jī)頁(yè)大小進(jìn)行對(duì)齊作為保留區(qū)域大小
?assert(_stack_shadow_zone_size == 0, "This should be called only once.");
?_stack_shadow_zone_size = align_up(StackShadowPages * unit, page_size);
}

5.3.1. 解釋執(zhí)行與編譯執(zhí)行時(shí)候的判斷(x86為例)

我們繼續(xù)針對(duì) Java 線程進(jìn)行討論。在前面我們已經(jīng)知道,Java 線程棧的大小是有限制的,如果線程棧使用的內(nèi)存超過了限制,那么就會(huì)拋出?StackOverflowError。但是,JVM 如何知道什么時(shí)候該拋出呢?

首先,對(duì)于解釋執(zhí)行,一般沒有任何優(yōu)化,就是在調(diào)用方法前檢查。不同的環(huán)境下的實(shí)現(xiàn)會(huì)有些差別,我們以 x86 cpu 為例:

https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp

void TemplateInterpreterGenerator::generate_stack_overflow_check(void) {
?//計(jì)算棧幀的一些元數(shù)據(jù)存儲(chǔ)的消耗
?const int entry_size = frame::interpreter_frame_monitor_size() * wordSize;
?const int overhead_size =
? ?-(frame::interpreter_frame_initial_sp_offset * wordSize) + entry_size;
?//讀取虛擬機(jī)頁(yè)大小,第二章我們分析過
?const int page_size = os::vm_page_size();
?//比較當(dāng)前要調(diào)用的方法的元素個(gè)數(shù),判斷與除去元數(shù)據(jù)以外一頁(yè)能容納的元素個(gè)數(shù)誰大誰小
?Label after_frame_check;
?__ cmpl(rdx, (page_size - overhead_size) / Interpreter::stackElementSize);
?__ jcc(Assembler::belowEqual, after_frame_check);
?//大于的才會(huì)進(jìn)行后續(xù)的判斷,因?yàn)樾∮谝豁?yè)的話,絕對(duì)可以被黃色區(qū)域限制住,因?yàn)辄S色區(qū)域要與頁(yè)大小對(duì)齊,因此至少一頁(yè)
?//小于一頁(yè)的棧幀不會(huì)導(dǎo)致跳過黃色區(qū)域,只有大于的須有后續(xù)仔細(xì)判斷

?Label after_frame_check_pop;
?//讀取線程的 stack_overflow_limit_offset
?//_stack_overflow_limit = stack_end() + MAX2(stack_guard_zone_size(), stack_shadow_zone_size());
?//即棧尾 加上 保護(hù)區(qū)域 或者 陰影區(qū)域 的最大值,即有效棧尾地址
?//其實(shí)就是當(dāng)前線程棧容量頂部減去 保護(hù)區(qū)域 或者 陰影區(qū)域 的最大值的地址,即當(dāng)前線程棧只能增長(zhǎng)到這個(gè)地址
?const Address stack_limit(thread, JavaThread::stack_overflow_limit_offset());
?//將前面計(jì)算的棧幀元素個(gè)數(shù)大小保存在 rax
?__ mov(rax, rdx);
?//將棧幀的元素個(gè)數(shù)轉(zhuǎn)換為字節(jié)大小,然后加上棧幀的元數(shù)據(jù)消耗
?__ shlptr(rax, Interpreter::logStackElementSize);
?__ addptr(rax, overhead_size);

?//加上前面計(jì)算的有效棧尾地址
?__ addptr(rax, stack_limit);

?//與當(dāng)前棧頂?shù)刂繁容^,如果當(dāng)前棧頂?shù)刂反笥?rax 當(dāng)前值,證明沒有溢出
?__ cmpptr(rsp, rax);
?__ jcc(Assembler::above, after_frame_check_pop);

?//否則拋出 StackOverflowError 異常
?__ jump(ExternalAddress(StubRoutines::throw_StackOverflowError_entry()));
?__ bind(after_frame_check_pop);
?__ bind(after_frame_check);
}

代碼的步驟大概是(plagiarism和洗稿是惡意抄襲他人勞動(dòng)成果的行為,是對(duì)勞動(dòng)價(jià)值的漠視和踐踏! ):

  1. 首先判斷要分配的棧幀大小,是否大于一頁(yè)。

  2. 如果小于等于一頁(yè),不用檢查,直接結(jié)束。因?yàn)槿绻∮谝豁?yè),那么棧幀的元素個(gè)數(shù)一定小于一頁(yè),棧增長(zhǎng)不會(huì)導(dǎo)致跳過保護(hù)區(qū)域,如果達(dá)到保護(hù)區(qū)域就會(huì)觸發(fā)?SIGSEGV?拋出?StackOverflowError。因?yàn)槊總€(gè)保護(hù)區(qū)域如前面源代碼所示,都是對(duì)虛擬機(jī)頁(yè)大小進(jìn)行對(duì)齊的,因此至少一頁(yè)。

  3. 如果大于一頁(yè),則需要檢查。檢查當(dāng)前已經(jīng)使用的空間,加上棧幀占用的空間,加上保護(hù)區(qū)域與陰影區(qū)域的最大值,占用空間是否大于??臻g限制。如果大于,則拋出?StackOverflowError?異常。為什么是保護(hù)區(qū)域與陰影區(qū)域的最大值?陰影區(qū)域其實(shí)是我們假設(shè)的最大幀大小,最后至少要有這么多空間才一定不會(huì)導(dǎo)致溢出棧頂污染其他內(nèi)存(當(dāng)然,如之前所述,如果你自己實(shí)現(xiàn)一個(gè) Native 調(diào)用并且棧幀很大,則需要修改陰影區(qū)域大?。?。如果本身保護(hù)區(qū)域就比陰影區(qū)域大,那么就用保護(hù)區(qū)域的大小,就也能保證這一點(diǎn)。

可以看出,編譯執(zhí)行,雖然做了一定的優(yōu)化,但是還是很復(fù)雜,就算大部分棧幀應(yīng)該都小于一頁(yè),但是剛開始的判斷指令還是有不小的消耗。我們看看 JIT 編譯后的代碼,還是以 x86 cpu 為例:

https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/share/asm/assembler.cpp

void AbstractAssembler::generate_stack_overflow_check(int frame_size_in_bytes) {
?//讀取虛擬機(jī)頁(yè)大小,第二章我們分析過
?const int page_size = os::vm_page_size();
?//讀取影子區(qū)大小
?int bang_end = (int)StackOverflow::stack_shadow_zone_size();
?
?//如果棧幀大小大于一頁(yè),那么需要將 bang_end 加上棧幀大小,之后檢查每一頁(yè)是否處于保護(hù)區(qū)域
?const int bang_end_safe = bang_end;
?if (frame_size_in_bytes > page_size) {
? ?bang_end += frame_size_in_bytes;
?}

?//檢查每一頁(yè)是否處于保護(hù)區(qū)域
?int bang_offset = bang_end_safe;
?while (bang_offset <= bang_end) {
? ?bang_stack_with_offset(bang_offset);
? ?bang_offset += page_size;
?}
}

https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/macroAssembler_x86.cpp

//檢查是否處于保留區(qū)域,其實(shí)就是將 rsp - offset 的地址上的值寫入 rax 上,
//如果 rsp - offset 保護(hù)區(qū)域,那么就會(huì)觸發(fā) SIGSEGV
void bang_stack_with_offset(int offset) {
? ?movl(Address(rsp, (-offset)), rax);
}

編譯后執(zhí)行的指令就簡(jiǎn)單多了:

  1. 如果棧幀大小小于一頁(yè):只需要考慮 Native 調(diào)用是否會(huì)導(dǎo)致?StackOverflow?即可。檢查當(dāng)前占用位置加上影子區(qū)域大小,之后判斷是否會(huì)進(jìn)入保護(hù)區(qū)域即可,不用考慮當(dāng)前方法棧幀占用大小,因?yàn)榭隙ㄐ∮谝豁?yè)。驗(yàn)證是否進(jìn)入保護(hù)區(qū)域也和之前討論過的?NullPointeException?的處理是類似的,就是將 rsp - offset 的地址上的值寫入 rax 上,如果 rsp - offset 處于保護(hù)區(qū)域,那么就會(huì)觸發(fā)?SIGSEGV。

  2. 如果棧幀大小大于一頁(yè):那么需要將當(dāng)前占用位置,加上棧幀大小,加上影子區(qū)域大小,之后從當(dāng)前棧幀按頁(yè)檢查,是否處于保護(hù)區(qū)域。因?yàn)榇笥谝豁?yè)的話,直接驗(yàn)證最后的位置可能會(huì)溢出到其他東西占用的內(nèi)存(比如其他線程占用的內(nèi)存)。

5.3.2. 一個(gè) Java 線程 Xss 最小能指定多大

這個(gè)和平臺(tái)是相關(guān)的,我們以 linux x86 為例子,假設(shè)沒有大頁(yè)分配,一頁(yè)就是 4K,一個(gè)線程至少要保留如下的空間:

  • 保護(hù)區(qū)域:

    • 黃色區(qū)域:默認(rèn) 2 頁(yè)

    • 紅色區(qū)域:默認(rèn) 1 頁(yè)

    • 保留區(qū)域:默認(rèn) 1 頁(yè)

  • 影子區(qū)域:默認(rèn) 20 頁(yè)

這些加在一起是 24 頁(yè),也就是 96K。

同時(shí),在 JVM 代碼中也限制了,除了這些空間,每種線程的最小大?。?/p>

https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp

size_t os::_compiler_thread_min_stack_allowed = 48 * K;
size_t os::_java_thread_min_stack_allowed = 40 * K;
size_t os::_vm_internal_thread_min_stack_allowed = 64 * K;

所以,對(duì)于 Java 線程,至少需要?40 + 96 = 136K?的空間。我們?cè)囈幌拢?/p>

bash-4.2$ java -Xss1k The Java thread stack size specified is too small. Specify at least 136k Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.

微信搜索“干貨滿滿張哈?!标P(guān)注公眾號(hào),加作者微信,每日一刷,輕松提升技術(shù),斬獲各種offer
我會(huì)經(jīng)常發(fā)一些很好的各種框架的官方社區(qū)的新聞視頻資料并加上個(gè)人翻譯字幕到如下地址(也包括上面的公眾號(hào)),歡迎關(guān)注:


全網(wǎng)最硬核 JVM 內(nèi)存解析 - 13.JVM 線程內(nèi)存設(shè)計(jì)的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
紫阳县| 商河县| 诸城市| 滨海县| 闵行区| 台中县| 和龙市| 高青县| 大安市| 娄烦县| 汽车| 尼勒克县| 景泰县| 北流市| 张家港市| 正蓝旗| 和林格尔县| 湘潭县| 长治市| 三明市| 蒲江县| 尉犁县| 普兰店市| 安宁市| 汪清县| 长乐市| 平原县| 洪湖市| 增城市| 广州市| 栾城县| 新密市| 义乌市| 抚顺市| 临汾市| 开江县| 韶山市| 丁青县| 石门县| 新平| 闽侯县|