免费网站主机,西地那非片的功效和副作用,大型网站运维公司,长沙软件开发培训机构前言
最近在工作中遇到频繁FullGC且YoungGC时间有时特别长的情况#xff0c;而自己对JVM的垃圾回收也是一知半解#xff0c;因此需要对JVM做系统的了解#xff0c;为快速解决工作中的问题#xff0c;能有效分析GC日志和业务代码#xff0c;先从G1垃圾回收器开始学习…前言
最近在工作中遇到频繁FullGC且YoungGC时间有时特别长的情况而自己对JVM的垃圾回收也是一知半解因此需要对JVM做系统的了解为快速解决工作中的问题能有效分析GC日志和业务代码先从G1垃圾回收器开始学习工作中系统采用的就是G1垃圾回收器
本文概览 准备阶段
什么是GC
GC全称Garbage Collection意为垃圾收集。在系统不停机运行中应用会不断创建对象也就是不断的在内存中进行空间分配。但系统内存是一定的不可能支持无限制的内存分配因此会针对应用持续运行中不再使用的对象所占用的内存空间进行回收。
GC算法
常见的垃圾收集算法有以下几种
标记-复制标记-清除标记-整理
标记-复制 标记出存活对象后将存活的对象复制到一块准备好的空闲内存区域然后将垃圾回收的区域全部回收掉。 上图以年轻代的回收场景来举例可以看到复制算法有两个比较重要的点对象复制的过程、空间区域的冗余。如果一个区域中存活对象较多的场景不适合采用复制算法来进行垃圾回收。还有就是需要准备额外的空闲区域来接收存活对象的放置因此该算法也有额外空间的损失。
标记-清除 标记出存活对象后将死亡的对象直接被回收掉存活对象不做处理。 上图我们也可以看到这种处理方式很容易造成内存碎片的问题例如如图所示有16M的内存空间通过标记-清除的回收算法回收6M的内存空间但无法分配一个连续的4M大小的对象。
标记-整理 标记出存活对象后将死亡的对象直接回收掉并将存活对象做一步压缩整理将其压缩到头部连续的空间内。 这就解决了上面说的内存碎片问题。
GC回收器
Java虚拟机发展至今共有一下几款垃圾收集器
新生代 Serial基于标记复制算法使用一个收集线程去完成垃圾收集工作并且在垃圾收集过程中必须暂停其他所有工作线程直到收集工作结束Parallel Scavenge基于标记复制算法使用多个线程去并行收集ParaNew基于标记复制算法使用多个线程去并行收集一般与CMS组成JVM分代回收的垃圾回收器 老年代 Serial Old基于标记整理算法使用一个收集线程去完成垃圾收集工作通常用于客户端模式下。在服务端模式下通常作为CMS收集器发生失败时的后备预案。Parallel Old基于标记整理算法使用多个线程去并行收集垃圾回收的过程还是STWCMS基于标记清除算法第一个真正意义上的并发垃圾回收器 不分代 G1基于CSet进行垃圾回收这里不赘述下面做详细解释ZGC它是一个可扩展的低延迟垃圾回收器旨在处理多达几TB的堆内存同时保持暂停时间在10毫秒以内无论堆的大小如何。它使用了一种叫做颜色指针的技术来实现并发的对象整理并极大地减少了STW暂停Shanendoah它是一个低延迟的垃圾回收器设计目标是减少GC的暂停时间而不仅仅关注整体的吞吐量。它通过并发地压缩堆和并发地进行标记-清除达到了最小化STW暂停的效果使其成为响应时间要求严格的应用的一个合适选择
G1出现的背景 Garbage First简称G1收集器是垃圾收集器技术发展历史上的里程碑式的结果它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。摘自《深入理解Java虚拟机》 它主要应用于服务端的垃圾回收场景提供软实时、低延时、可设定目标适用于较大的堆内存对比CMS《深入理解Java虚拟机》作者是这么说的在小内存应用上CMS的表现大概率仍然要会优于G1而在大内存应用上G1则大多能发挥其优势这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。
它的出现也是为了替换JDK 5中的CMS垃圾回收器。在JDK 9发布时将JDK默认的Parallel ScavengeParallel Old组合的垃圾回收器修改为G1收集器而CMS则是被定义为不推荐使用的垃圾回收器。 G1的内存布局
分区与分代
G1将Java堆分成若干个大小一致的区域并且Region的大小可以通过-XX:G1HeapRegionSizeN参数设定取值范围为1MB ~ 32MB且应为2的N次幂。下图是G1的内存分布示意图摘自于引用文章。 从示意图来看G1仍然保留年轻代、老年代的概念但新生代和老年代不再是固定的了也就是同一块Region有可能是年轻代也有可能会被当做是老年代并且他们在物理上也不是连续的了。图中还有Humongous区域这个是用来存储大对象的区域G1认为只要超过了Region大小的一半就是大对象会被直接存储到该区域当中它也属于老年代的一部分。
由于G1将垃圾回收的最小单位定位Region这样就可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1收集器回去跟踪各个Region里面的垃圾堆积的价值回收所获得的空间大小以及回收所需时间的经验值大小然后在后台维护一个优先级列表每次根据用户设定允许的收集停顿时间使用参数 -XX:MaxGCPauseMillis指定默认值200来优先处理回收价值收益最大的那些Region这也是Garbage First名称的由来。
前面说了G1会在逻辑上划分年轻代、老年代其中年轻代又分为Eden、Survivor空间但年轻代并不是固定不变的当年轻代分区占满时JVM会分配新的内存空间加入到年轻代中整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间(默认60%)之间动态变化且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以-XX:G1MaxNewSizePercent及分区的已记忆集合(RSet)计算得到。当然G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn)但同时暂停目标将失去意义。
收集集合
既然空间上不连续那G1是如何回收的呢总不至于去全堆扫描吧肯定不是的。这就引入了Collection Set简称CSet来表示每次GC暂停时回收的一系列目标Region区域。 在G1中共有两类CSet一类为保存年轻代Region的集合另一类为保存年轻代老年代的Region的集合。年轻代收集集合则保存年轻代的分区在GC时会将这些分区中的存活对象移动到空闲分区中然后将原始分区空间全部回收掉。与年轻代集合不同是老年代集合会有准入条件可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置从而拦截那些回收开销巨大的对象同时每次混合收集可以包含候选老年代分区可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限来避免长时间的暂停。
其实G1的收集都是根据CSet进行操作的年轻代收集与混合收集没有明显的不同最大的区别在于两种收集的触发条件。
记忆集与卡表
我们现在知道G1是通过CSet进行垃圾回收的那CSet是如何被构建呢其实针对年轻代Region来讲一般会将所有分区有对象分配的加入到年轻代的CSet中而对于老年代就比较复杂了如果从GC Roots开始一直到叶子对象这一整条引用链都分布在一个Region当中就比较方便在扫描当前Region时就可以将这条引用链标记出来那么当跨Region引用时G1是如何标记的呢肯定不是全堆扫描答案就是G1会维护一个全局卡表他本质上是一个哈希结构Key为每个Region的起始地址Value是一个集合里面存储的元素是卡表的索引号。每个Region都会维护一个记忆集。 上图是一个关于卡表记忆集的直观图比较清晰直接我在学习这块的时候十分困惑所以也在这里记录下我的理解如果有错误还请指正。
G1维护了一个全局卡表这个卡表代表整个堆分成若干个卡也就是分成若干个连续的物理内存区域。当应用线程修改一个对象引用时涉及的卡便会被标记为dirty。而每个Region又维护一个记忆集这个记忆集本质上是一个哈希结构key为别的Region指向自己的指针Value是一个集合记录了卡表的索引号。这里解释下比如对象A指向对象B那么在B的记忆集中就是记录对象A的指针并且将A所对应的卡片索引号集合之所以是一个集合是因为一个对象可能比较大它占用了多个卡或者一个Region中有多个对象都引用当前Region的对象它们设计多个卡的情况
写屏障
讲述完记忆卡与卡表的实现那么他们的数据是如何被更新维护的呢实际上JVM会在编译时注入一小段代码用于记录指针变化object.field reference (putfield) ;当更新指针时会将对应的卡标记为Dirty将Card加入到Dirty Card Queue这个队列会有白/绿/黄/红四个颜色代表JVM处理队列更新记忆集的紧急程度
白天下太平无事发生。绿-XX:G1ConcRefinementGreenZoneNRefinement线程开始被激活开始更新RS黄-XX:G1ConcRefinementYellowZoneN全部Refinement线程开始激活红-XX:G1ConcRefinementRedZoneN产生STW应⽤线程也参与排空队列的⼯作
G1的垃圾回收原理 备注对G1的垃圾回收原理是基于Jdk 8版本。 G1垃圾收集活动周期图 G1首先会先进行若干次young gc每一次gc后会将存活对象提供给PLAB(Promotion Local Allocation Buffer)来转移对象到老年代或Survivor区。当老年代堆内存占用达到阈值后-XX:InitiatingHeapOccupancyPercent默认45%会进入到并发标记周期这个周期G1会先进行一次young gc并且伴随着GC Roots的标记栈中的对象、静态变量、常量、JNI对象等随后进行与应用线程并发的进行存活对象标记。并发标记结束后进行重新标记这个时候标记一下在并发标记过程中产生对象引用更新通过原始快照记忆集快速实现并发标记周期的最后一个操作就是清除阶段该阶段是对RSet梳理、整理堆分区以及识别所有空闲分区并发标记周期后会产生多次mixed gc情况并不会一次性收集完这也是G1的启发式算法根据用户设定的期望时间来优先回收价值最大的一批Region集合。
标记存活对象
通过三色标记法原始快照细节我们单拉一节来具体介绍。
Young GC 通过GC日志来看下young gc各阶段都是干什么的 2023-09-15T16:07:33.4810800: 158368.669: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 318767104 bytes, new threshold 15 (max 15)(to-space exhausted), 0.5207419 secs][Parallel Time: 316.3 ms, GC Workers: 13][GC Worker Start (ms): Min: 158368670.8, Avg: 158368673.3, Max: 158368674.0, Diff: 3.2][Ext Root Scanning (ms): Min: 0.4, Avg: 2.5, Max: 6.1, Diff: 5.7, Sum: 32.4][Update RS (ms): Min: 6.5, Avg: 15.8, Max: 23.6, Diff: 17.1, Sum: 204.8][Processed Buffers: Min: 24, Avg: 93.5, Max: 213, Diff: 189, Sum: 1216][Scan RS (ms): Min: 0.0, Avg: 1.2, Max: 1.7, Diff: 1.7, Sum: 16.1][Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][Object Copy (ms): Min: 282.4, Avg: 291.0, Max: 300.7, Diff: 18.3, Sum: 3783.0][Termination (ms): Min: 0.0, Avg: 3.1, Max: 4.8, Diff: 4.8, Sum: 40.3][Termination Attempts: Min: 1, Avg: 1.7, Max: 5, Diff: 4, Sum: 22][GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.3][GC Worker Total (ms): Min: 313.0, Avg: 313.6, Max: 316.1, Diff: 3.1, Sum: 4077.0][GC Worker End (ms): Min: 158368986.9, Avg: 158368986.9, Max: 158368987.0, Diff: 0.0][Code Root Fixup: 0.0 ms][Code Root Purge: 0.0 ms][Clear CT: 0.5 ms][Other: 204.0 ms][Evacuation Failure: 172.1 ms][Choose CSet: 0.0 ms][Ref Proc: 27.2 ms][Ref Enq: 0.6 ms][Redirty Cards: 1.2 ms][Humongous Register: 0.1 ms][Humongous Reclaim: 0.5 ms][Free CSet: 0.3 ms][Eden: 3704.0M(4808.0M)-0.0B(3024.0M) Survivors: 0.0B-0.0B Heap: 11.2G(12.0G)-7850.2M(12.0G)]
Heap after GC invocations4016 (full 26):garbage-first heap total 12582912K, used 8038625K [0x00000004e0800000, 0x00000004e1003000, 0x00000007e0800000)region size 8192K, 0 young (0K), 0 survivors (0K)Metaspace used 159472K, capacity 162234K, committed 162684K, reserved 1193984Kclass space used 16726K, capacity 17320K, committed 17532K, reserved 1048576K
}[Times: user1.78 sys0.04, real0.52 secs]young gc整个阶段是Stop The World的日志中的并行时间其实说的是GC线程并行的工作应用线程是被挂起的。解释完这个我们再来一块一块说明一下它的各个阶段都做了什么。
并行时间 GC Worker StartGC工作线程启动后面的Diff指标越小越好越小就意味着各个GC工作线程之间开始执行的时间间隔很短或者它们几乎同时开始 这能说明几点更佳的并行性、更佳的响应性、更少的启动延迟、更一致的停顿时间 Ext Root Scanning外部根扫描GC工作线程并行扫描指向Collection Set的外部根例如静态变量、Java线程、JNI引用所耗费的时间Update RS更新记忆集这个阶段主要是处理一些脏卡在程序运行过程中产生的一些引用的变更这个阶段主要是确保跨Region引用在GC过程中被正确处理 Processed Buffers计数显示GC工作线程除了多少个’update buffers’ Scan RS扫描记忆集以查找对某个Region的引用所花费的时间这个时间取决RSet数据结构的粗糙程度G1底层采用的是哈希结构前面描述记忆集已经提到Code Root ScanningCode Cache扫描这个阶段确保了所有直接或间接由机器代码持有的Java对象引用在年轻代GC过程中都被正确处理Object Copy存活对象的移动这个阶段是GC线程并行去处理的它占据了停顿时间的一大部分如果这个时间很长需要分析下为什么会产生这么多的存活对象Termination使得GC工作线程准备进入终止所花费的时间统计GC Worker OtherGC工作线程没有在上面列出的任何并行活动中所花费的时间统计 串行时间 Code Root Fixup更新机器码所引用的对象的地址所耗费的时间Code Root Purge对一些无引用类进行卸载删除无用代码进一步优化内存占用这个操作所耗费的时间Clear CT清除卡表所耗费的时间Other其他阶段的耗时 Evacuation Failure分配失败担保机制当年轻代对象进行移动时找不到连续可用的空间JVM就会做一系列动作其中一个动作就是可能会产生一次全堆单线程、STW式的FullGC操作因此我们要避免出现分配失败的情况。Choose CSet选择要被回收的Region集合所耗费的时间在young gc中一般是将年轻代所有的Region加入进来Ref Proc处理软引用、弱引用、虚引用花费的时间这块的算法比较复杂我们只需要知道这些引用的回收时机即可Ref Enq将上面的引用识别出满足回收条件的并加入到对应的引用处理队列中并进行清理所耗费的时间这两个阶段我学习比较混淆没找到有效资料先记住这两个阶段就是保证这些引用类型能被回收阶段正确处理Redirty Cards重新脏化卡片。排队引用可能会更新RSet所以需要对关联的Card重新脏化(Redirty Cards)Humongous Register、Reclaim 主要是对巨型对象回收的信息youngGC阶段会对RSet中有引用的短命的巨型对象进行回收巨型对象会直接回收而不需要进行转移转移代价巨大也没必要Free CSet释放CSet,其中也会清理CSet中的RSet
并发标记周期 并发标记周期日志如下所示 2023-09-18T14:11:36.8910800: 11.294: [GC pause (Metadata GC Threshold) (young) (initial-mark)
Desired survivor size 243269632 bytes, new threshold 15 (max 15)
, 0.0451481 secs][Parallel Time: 17.9 ms, GC Workers: 13][GC Worker Start (ms): Min: 11294.1, Avg: 11301.5, Max: 11302.2, Diff: 8.1][Ext Root Scanning (ms): Min: 0.3, Avg: 2.2, Max: 8.1, Diff: 7.8, Sum: 28.0][Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0][Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][Code Root Scanning (ms): Min: 0.0, Avg: 1.6, Max: 6.4, Diff: 6.4, Sum: 20.7][Object Copy (ms): Min: 0.5, Avg: 4.7, Max: 9.0, Diff: 8.5, Sum: 60.9][Termination (ms): Min: 0.0, Avg: 1.9, Max: 2.3, Diff: 2.3, Sum: 24.6][Termination Attempts: Min: 1, Avg: 57.7, Max: 157, Diff: 156, Sum: 750][GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2][GC Worker Total (ms): Min: 9.6, Avg: 10.3, Max: 17.7, Diff: 8.1, Sum: 134.4][GC Worker End (ms): Min: 11311.8, Avg: 11311.8, Max: 11311.9, Diff: 0.1][Code Root Fixup: 0.1 ms][Code Root Purge: 0.0 ms][Clear CT: 0.2 ms][Other: 26.9 ms][Choose CSet: 0.0 ms][Ref Proc: 26.4 ms][Ref Enq: 0.1 ms][Redirty Cards: 0.1 ms][Humongous Register: 0.0 ms][Humongous Reclaim: 0.0 ms][Free CSet: 0.1 ms][Eden: 240.0M(3680.0M)-0.0B(3664.0M) Survivors: 0.0B-16.0M Heap: 240.0M(12.0G)-15.0M(12.0G)]
Heap after GC invocations1 (full 0):garbage-first heap total 12582912K, used 15341K [0x00000004e0800000, 0x00000004e1003000, 0x00000007e0800000)region size 8192K, 2 young (16384K), 2 survivors (16384K)Metaspace used 20508K, capacity 21162K, committed 21248K, reserved 1067008Kclass space used 2541K, capacity 2793K, committed 2816K, reserved 1048576K
}[Times: user0.18 sys0.01, real0.05 secs]
2023-09-18T14:11:36.9370800: 11.339: [GC concurrent-root-region-scan-start]
2023-09-18T14:11:36.9370800: 11.339: Total time for which application threads were stopped: 0.0454725 seconds, Stopping threads took: 0.0000165 seconds
2023-09-18T14:11:36.9560800: 11.359: [GC concurrent-root-region-scan-end, 0.0194080 secs]
2023-09-18T14:11:36.9560800: 11.359: [GC concurrent-mark-start]
2023-09-18T14:11:36.9580800: 11.361: [GC concurrent-mark-end, 0.0020355 secs]
2023-09-18T14:11:36.9580800: 11.361: [GC remark 2023-09-18T14:11:36.9580800: 11.361: [Finalize Marking, 0.0194624 secs] 2023-09-18T14:11:36.9780800: 11.381: [GC ref-proc, 0.0199802 secs] 2023-09-18T14:11:36.9980800: 11.401: [Unloading, 0.0062853 secs], 0.0461724 secs][Times: user0.18 sys0.01, real0.05 secs]
2023-09-18T14:11:37.0050800: 11.407: Total time for which application threads were stopped: 0.0465892 seconds, Stopping threads took: 0.0003143 seconds
2023-09-18T14:11:37.0050800: 11.407: [GC cleanup 26M-26M(12G), 0.0022687 secs][Times: user0.01 sys0.00, real0.00 secs]它是借助了一次young gc进行一次根对象的扫描有一定的性能优化对于young gc相关阶段不做赘述这里记录下其他的阶段
initial-mark初始标记标记GC RootsGC concurrent-root-region-scan-start主要的目标是扫描年轻代中的Survivor区域以查找指向老年代的引用。这是并发标记循环的早期部分是在初始标记STW之后发生的。 扫描Survivor区域G1 GC并发地扫描Survivor区域寻找那些指向老年代的引用。标记引用的对象找到这些引用后它们指向的老年代中的对象会被标记为存活。非STW阶段这个阶段并发地与应用程序运行不会导致应用程序停顿。时效性此阶段需要在下一次年轻代GC发生之前完成以确保所有的跨代引用都被正确标记。 GC concurrent-mark-start通过可达性分析算法将根对象引用链上的所有对象标记为存活。GC remark在该阶段中G1需要一个暂停的时间去处理剩下的SATB日志缓冲区和所有更新找出所有未被访问的存活对象同时安全完成存活数据计算
Mixed GC
2023-09-18T14:23:33.1590800: 727.562: [GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 243269632 bytes, new threshold 15 (max 15)
- age 1: 7529224 bytes, 7529224 total(to-space exhausted), 0.5513782 secs][Parallel Time: 354.4 ms, GC Workers: 13][GC Worker Start (ms): Min: 727563.0, Avg: 727564.7, Max: 727565.7, Diff: 2.6][Ext Root Scanning (ms): Min: 0.2, Avg: 1.1, Max: 4.7, Diff: 4.5, Sum: 14.1][Update RS (ms): Min: 10.9, Avg: 13.9, Max: 16.2, Diff: 5.3, Sum: 180.2][Processed Buffers: Min: 20, Avg: 84.1, Max: 133, Diff: 113, Sum: 1093][Scan RS (ms): Min: 1.8, Avg: 4.5, Max: 5.6, Diff: 3.8, Sum: 58.5][Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1][Object Copy (ms): Min: 297.8, Avg: 311.1, Max: 332.0, Diff: 34.3, Sum: 4044.3][Termination (ms): Min: 0.0, Avg: 21.4, Max: 33.1, Diff: 33.1, Sum: 278.6][Termination Attempts: Min: 1, Avg: 1.6, Max: 9, Diff: 8, Sum: 21][GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2][GC Worker Total (ms): Min: 351.0, Avg: 352.0, Max: 353.6, Diff: 2.6, Sum: 4575.9][GC Worker End (ms): Min: 727916.6, Avg: 727916.7, Max: 727917.3, Diff: 0.7][Code Root Fixup: 0.0 ms][Code Root Purge: 0.0 ms][Clear CT: 1.5 ms][Other: 195.4 ms][Evacuation Failure: 172.2 ms][Choose CSet: 0.1 ms][Ref Proc: 20.3 ms][Ref Enq: 0.1 ms][Redirty Cards: 0.5 ms][Humongous Register: 0.0 ms][Humongous Reclaim: 0.4 ms][Free CSet: 0.2 ms][Eden: 2232.0M(3672.0M)-0.0B(3680.0M) Survivors: 8192.0K-0.0B Heap: 11.3G(12.0G)-9434.1M(12.0G)]
Heap after GC invocations38 (full 0):garbage-first heap total 12582912K, used 9660532K [0x00000004e0800000, 0x00000004e1003000, 0x00000007e0800000)region size 8192K, 0 young (0K), 0 survivors (0K)Metaspace used 153978K, capacity 156654K, committed 157000K, reserved 1189888Kclass space used 16554K, capacity 17174K, committed 17224K, reserved 1048576K
}[Times: user1.49 sys0.03, real0.55 secs]mixed gc过程其实是和年轻代gc差不多的区别就在于CSet的选择。young gc会选择年轻代所有的Regionmixed gc会根据期望停顿时间来选择最有收益的一批Region集合。这里不在赘述了。
总结
好了对G1垃圾回收器的学习就暂时到这里了对其有了大概的印象包括G1垃圾回收的活动周期young gc、并发标记周期、多次mixed gc、RSet、CSet以及卡表等知识有了一定的概念也学习了如何分析G1的GC日志。上面的内容大多是来源与网上的资料在一些比较难懂的地方加上自己的理解并绘出直观图和语言注释。
参考
https://pdai.tech/md/java/jvm/java-jvm-gc-g1.html
https://www.infoq.com/articles/G1-one-Garbage-Collector-To-Rule-Them-All/
https://time.geekbang.org/column/intro/100010301
《深入理解Java虚拟机》