赞
踩
作者:周志明
整理者GitHub:https://github.com/starjuly/UnderstandingTheJVM
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机的进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图2-1所示。
这一小节将对 JVM 对 Java 堆中的对象的创建、布局和访问的全过程进行讲解。
遇到一条New指令,虚拟机的步骤:
在第二步中,为对象分配内存,就是在内存划分一块确定大小的空闲内存,但存在两个问题:
如何划分空闲内存和已被使用的内存?
如何处理多线程下,内存分配问题?
对象头(Header):
实例数据(Instance Data):
对齐填充(Padding):
Java程序通过栈上的reference数据来操作堆上具体对象,主流的访问方式主要有以下两种:
--XX:MaxDirectMemorySize
设置,如不指定,默认与Java堆最大值(-Xmx指定)一致。垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还"存活"着,哪些已经"死去"("死去"即不可能再被任何途径使用的对象)了。
/** * testGC()方法执行后,ObjA和ObjB会不会被GC呢? */ public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; // 假设在这行发生GC,objA和objB是否能被回收? System.gc(); } public static void main(String[] args) { testGC(); } }
运行结果:
[GC (System.gc()) [PSYoungGen: 8034K->624K(76288K)] 8034K->632K(251392K), 0.0014423 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 624K->0K(76288K)] [ParOldGen: 8K->394K(175104K)] 632K->394K(251392K), [Metaspace: 3161K->3161K(1056768K)], 0.0045513 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 76288K, used 1966K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
eden space 65536K, 3% used [0x000000076ab00000,0x000000076aceb9e0,0x000000076eb00000)
from space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
to space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
ParOldGen total 175104K, used 394K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
object space 175104K, 0% used [0x00000006c0000000,0x00000006c00629d0,0x00000006cab00000)
Metaspace used 3173K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 349K, capacity 388K, committed 512K, reserved 1048576K
从运行结果可以清楚看到内存回收日志中包含"8034K->624K",意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。
图3-1 利用可达性分析算法判定对象是否可回收
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
/** * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个 对象的finalize()方法最多只会被系统自动调用一次 */ public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("Yes, I am still alive!"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method is exceted!"); // 重新建立关联,避免被回收 SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscapeGC(); // 对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因为Finalizer方法优先级很低,暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("No, I am dead :("); } // 下面这段代码和上面的完全相同,但是这次自我拯救失败了 SAVE_HOOK = null; System.gc(); // 因为Finalizer方法优先级很低,暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("No, I am dead :("); } } }
运行结果:
finalize method is exceted!
Yes, I am still alive!
No, I am dead :(
方法区的垃圾收集主要回收两个部分内容:废弃的常量和不再使用的类型。
从如何判定对象消亡的角度出发,垃圾收集算法可以划分为"引用计数式垃圾收集"(Reference Counting GC)和"追踪式垃圾收集"(Tracing GC)两大类,这两类也常备称作"直接垃圾收集"和"间接垃圾收集"。本节所有算法均属于追踪式垃圾收集的范畴。
当前商业虚拟机的垃圾收集器,大多数都遵循了"分代收集"(Generational Collection)的理论进行设计,它建立在两个分代假说之上:
注意
对于不同分代的名词定义:
标记-复制算法常被称为复制算法,为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。该算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。
标记-整理算法标记的过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
标记-清理算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。
CARD_TABLE [this address >> 9] = 0;
void oop_field_store(oop* field, oop new_value) {
// 引用字段赋值操作
*field = new_value;
// 写后屏障,在这里完成卡表状态更新
post_write_barrier(field, new_value);
}
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
表3-1 并发出现“对象”消失问题的示意
注意
-Xlog[:[selector][:[output][:[decorators][:output-options]]]]
java -Xlog:gc GCTest.java
[0.014s][info][gc] Using G1
[0.602s][info][gc] GC(0) Pause Full (System.gc()) 14M->2M(20M) 8.272ms
[0.609s][info][gc] GC(1) Pause Full (System.gc()) 2M->2M(14M) 7.350ms
[0.619s][info][gc] GC(2) Pause Full (System.gc()) 2M->2M(10M) 9.763ms
java -XX:+PrintGCDetails GCTest.java [0.002s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead. [0.009s][info ][gc,heap] Heap region size: 1M [0.013s][info ][gc ] Using G1 [0.013s][info ][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 [0.589s][info ][gc,task ] GC(0) Using 6 workers of 10 for full compaction [0.589s][info ][gc,start ] GC(0) Pause Full (System.gc()) [0.589s][info ][gc,phases,start] GC(0) Phase 1: Mark live objects [0.591s][info ][gc,stringtable ] GC(0) Cleaned string and symbol table, strings: 5459 processed, 12 removed, symbols: 48571 processed, 12 removed [0.591s][info ][gc,phases ] GC(0) Phase 1: Mark live objects 1.942ms [0.591s][info ][gc,phases,start] GC(0) Phase 2: Prepare for compaction [0.592s][info ][gc,phases ] GC(0) Phase 2: Prepare for compaction 0.610ms [0.592s][info ][gc,phases,start] GC(0) Phase 3: Adjust pointers [0.593s][info ][gc,phases ] GC(0) Phase 3: Adjust pointers 0.941ms [0.593s][info ][gc,phases,start] GC(0) Phase 4: Compact heap [0.594s][info ][gc,phases ] GC(0) Phase 4: Compact heap 1.076ms [0.596s][info ][gc,heap ] GC(0) Eden regions: 15->0(6) [0.596s][info ][gc,heap ] GC(0) Survivor regions: 0->0(0) [0.596s][info ][gc,heap ] GC(0) Old regions: 0->6 [0.596s][info ][gc,heap ] GC(0) Humongous regions: 0->0 [0.596s][info ][gc,metaspace ] GC(0) Metaspace: 14200K->14200K(1062912K) [0.596s][info ][gc ] GC(0) Pause Full (System.gc()) 14M->2M(20M) 7.147ms [0.596s][info ][gc,cpu ] GC(0) User=0.02s Sys=0.01s Real=0.00s [0.596s][info ][gc,task ] GC(1) Using 2 workers of 10 for full compaction [0.596s][info ][gc,start ] GC(1) Pause Full (System.gc()) [0.596s][info ][gc,phases,start] GC(1) Phase 1: Mark live objects [0.599s][info ][gc,stringtable ] GC(1) Cleaned string and symbol table, strings: 5447 processed, 0 removed, symbols: 48559 processed, 0 removed [0.599s][info ][gc,phases ] GC(1) Phase 1: Mark live objects 2.560ms [0.599s][info ][gc,phases,start] GC(1) Phase 2: Prepare for compaction [0.599s][info ][gc,phases ] GC(1) Phase 2: Prepare for compaction 0.442ms [0.599s][info ][gc,phases,start] GC(1) Phase 3: Adjust pointers [0.601s][info ][gc,phases ] GC(1) Phase 3: Adjust pointers 1.238ms [0.601s][info ][gc,phases,start] GC(1) Phase 4: Compact heap [0.602s][info ][gc,phases ] GC(1) Phase 4: Compact heap 0.948ms [0.602s][info ][gc,heap ] GC(1) Eden regions: 0->0(3) [0.602s][info ][gc,heap ] GC(1) Survivor regions: 0->0(0) [0.602s][info ][gc,heap ] GC(1) Old regions: 6->3 [0.602s][info ][gc,heap ] GC(1) Humongous regions: 0->0 [0.602s][info ][gc,metaspace ] GC(1) Metaspace: 14200K->14200K(1062912K) [0.602s][info ][gc ] GC(1) Pause Full (System.gc()) 2M->2M(10M) 6.079ms [0.603s][info ][gc,cpu ] GC(1) User=0.01s Sys=0.00s Real=0.01s [0.603s][info ][gc,task ] GC(2) Using 1 workers of 10 for full compaction [0.603s][info ][gc,start ] GC(2) Pause Full (System.gc()) [0.603s][info ][gc,phases,start] GC(2) Phase 1: Mark live objects [0.608s][info ][gc,stringtable ] GC(2) Cleaned string and symbol table, strings: 5447 processed, 0 removed, symbols: 48559 processed, 0 removed [0.608s][info ][gc,phases ] GC(2) Phase 1: Mark live objects 5.014ms [0.608s][info ][gc,phases,start] GC(2) Phase 2: Prepare for compaction [0.608s][info ][gc,phases ] GC(2) Phase 2: Prepare for compaction 0.754ms [0.608s][info ][gc,phases,start] GC(2) Phase 3: Adjust pointers [0.611s][info ][gc,phases ] GC(2) Phase 3: Adjust pointers 2.747ms [0.611s][info ][gc,phases,start] GC(2) Phase 4: Compact heap [0.613s][info ][gc,phases ] GC(2) Phase 4: Compact heap 2.177ms [0.617s][info ][gc,heap ] GC(2) Eden regions: 0->0(3) [0.617s][info ][gc,heap ] GC(2) Survivor regions: 0->0(0) [0.617s][info ][gc,heap ] GC(2) Old regions: 3->3 [0.617s][info ][gc,heap ] GC(2) Humongous regions: 0->0 [0.617s][info ][gc,metaspace ] GC(2) Metaspace: 14200K->14200K(1062912K) [0.617s][info ][gc ] GC(2) Pause Full (System.gc()) 2M->2M(10M) 14.832ms [0.618s][info ][gc,cpu ] GC(2) User=0.03s Sys=0.00s Real=0.02s [0.619s][info ][gc,heap,exit ] Heap [0.619s][info ][gc,heap,exit ] garbage-first heap total 10240K, used 2282K [0x0000000700000000, 0x0000000800000000) [0.619s][info ][gc,heap,exit ] region size 1024K, 1 young (1024K), 0 survivors (0K) [0.619s][info ][gc,heap,exit ] Metaspace used 14206K, capacity 14530K, committed 14720K, reserved 1062912K [0.619s][info ][gc,heap,exit ] class space used 1528K, capacity 1655K, committed 1664K, reserved 1048576K
java -Xlog:gc+heap=debug GCTest.java [0.011s][info][gc,heap] Heap region size: 1M [0.011s][debug][gc,heap] Minimum heap 8388608 Initial heap 268435456 Maximum heap 4294967296 [0.652s][debug][gc,heap] GC(0) Heap before GC invocations=0 (full 0): garbage-first heap total 262144K, used 14336K [0x0000000700000000, 0x0000000800000000) [0.652s][debug][gc,heap] GC(0) region size 1024K, 15 young (15360K), 0 survivors (0K) [0.652s][debug][gc,heap] GC(0) Metaspace used 14186K, capacity 14498K, committed 14720K, reserved 1062912K [0.652s][debug][gc,heap] GC(0) class space used 1526K, capacity 1623K, committed 1664K, reserved 1048576K [0.657s][info ][gc,heap] GC(0) Eden regions: 15->0(6) [0.657s][info ][gc,heap] GC(0) Survivor regions: 0->0(0) [0.657s][info ][gc,heap] GC(0) Old regions: 0->6 [0.657s][info ][gc,heap] GC(0) Humongous regions: 0->0 [0.657s][debug][gc,heap] GC(0) Heap after GC invocations=1 (full 1): garbage-first heap total 20480K, used 2285K [0x0000000700000000, 0x0000000800000000) [0.657s][debug][gc,heap] GC(0) region size 1024K, 0 young (0K), 0 survivors (0K) [0.657s][debug][gc,heap] GC(0) Metaspace used 14186K, capacity 14498K, committed 14720K, reserved 1062912K [0.657s][debug][gc,heap] GC(0) class space used 1526K, capacity 1623K, committed 1664K, reserved 1048576K [0.657s][debug][gc,heap] GC(1) Heap before GC invocations=1 (full 1): garbage-first heap total 20480K, used 2285K [0x0000000700000000, 0x0000000800000000) [0.657s][debug][gc,heap] GC(1) region size 1024K, 0 young (0K), 0 survivors (0K) [0.657s][debug][gc,heap] GC(1) Metaspace used 14186K, capacity 14498K, committed 14720K, reserved 1062912K [0.657s][debug][gc,heap] GC(1) class space used 1526K, capacity 1623K, committed 1664K, reserved 1048576K [0.664s][info ][gc,heap] GC(1) Eden regions: 0->0(3) [0.664s][info ][gc,heap] GC(1) Survivor regions: 0->0(0) [0.664s][info ][gc,heap] GC(1) Old regions: 6->3 [0.664s][info ][gc,heap] GC(1) Humongous regions: 0->0 [0.664s][debug][gc,heap] GC(1) Heap after GC invocations=2 (full 2): garbage-first heap total 10240K, used 2285K [0x0000000700000000, 0x0000000800000000) [0.664s][debug][gc,heap] GC(1) region size 1024K, 0 young (0K), 0 survivors (0K) [0.664s][debug][gc,heap] GC(1) Metaspace used 14186K, capacity 14498K, committed 14720K, reserved 1062912K [0.664s][debug][gc,heap] GC(1) class space used 1526K, capacity 1623K, committed 1664K, reserved 1048576K [0.664s][debug][gc,heap] GC(2) Heap before GC invocations=2 (full 2): garbage-first heap total 10240K, used 2285K [0x0000000700000000, 0x0000000800000000) [0.664s][debug][gc,heap] GC(2) region size 1024K, 0 young (0K), 0 survivors (0K) [0.664s][debug][gc,heap] GC(2) Metaspace used 14186K, capacity 14498K, committed 14720K, reserved 1062912K [0.664s][debug][gc,heap] GC(2) class space used 1526K, capacity 1623K, committed 1664K, reserved 1048576K [0.674s][info ][gc,heap] GC(2) Eden regions: 0->0(3) [0.674s][info ][gc,heap] GC(2) Survivor regions: 0->0(0) [0.674s][info ][gc,heap] GC(2) Old regions: 3->3 [0.674s][info ][gc,heap] GC(2) Humongous regions: 0->0 [0.674s][debug][gc,heap] GC(2) Heap after GC invocations=3 (full 3): garbage-first heap total 10240K, used 2285K [0x0000000700000000, 0x0000000800000000) [0.674s][debug][gc,heap] GC(2) region size 1024K, 0 young (0K), 0 survivors (0K) [0.674s][debug][gc,heap] GC(2) Metaspace used 14186K, capacity 14498K, committed 14720K, reserved 1062912K [0.674s][debug][gc,heap] GC(2) class space used 1526K, capacity 1623K, committed 1664K, reserved 1048576K
java -Xlog:safepoint GCTest.java [0.131s][info][safepoint] Entering safepoint region: EnableBiasedLocking [0.131s][info][safepoint] Leaving safepoint region [0.131s][info][safepoint] Total time for which application threads were stopped: 0.0000875 seconds, Stopping threads took: 0.0000249 seconds [0.495s][info][safepoint] Application time: 0.3610630 seconds [0.495s][info][safepoint] Entering safepoint region: Deoptimize [0.495s][info][safepoint] Leaving safepoint region [0.495s][info][safepoint] Total time for which application threads were stopped: 0.0001419 seconds, Stopping threads took: 0.0000055 seconds [0.643s][info][safepoint] Application time: 0.1483016 seconds [0.643s][info][safepoint] Entering safepoint region: G1CollectFull [0.649s][info][safepoint] Leaving safepoint region [0.649s][info][safepoint] Total time for which application threads were stopped: 0.0057503 seconds, Stopping threads took: 0.0000048 seconds [0.649s][info][safepoint] Application time: 0.0000766 seconds [0.649s][info][safepoint] Entering safepoint region: G1CollectFull [0.656s][info][safepoint] Leaving safepoint region [0.656s][info][safepoint] Total time for which application threads were stopped: 0.0066743 seconds, Stopping threads took: 0.0000049 seconds [0.656s][info][safepoint] Application time: 0.0000392 seconds [0.656s][info][safepoint] Entering safepoint region: G1CollectFull [0.666s][info][safepoint] Leaving safepoint region [0.666s][info][safepoint] Total time for which application threads were stopped: 0.0097514 seconds, Stopping threads took: 0.0000049 seconds [0.667s][info][safepoint] Application time: 0.0012239 seconds [0.667s][info][safepoint] Entering safepoint region: Halt
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
// 出现一次Minor GC
allocation4 = new byte[4 * _1MB];
}
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold() {
byte[] allocation;
// 直接分配在老年代中
allocation = new byte[4 * _1MB];
}
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
*/
public static void testTenuringThreshold() {
byte[] allocation1, allocation2, allocation3;
// 什么时候进入老年代决定于XX:MaxTenuringThreshold设置
allocation1 = new byte[4 * _1MB];
allocation2 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
private static final int _1MB = 1024 * 1024; /** * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 * -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution */ @SuppressWarnings("unused") public static void testTenuringThreshold2() { byte[] allocation1, allocation2, allocation3, allocation4; // allocation1+allocation2大于survivor空间一半 allocation1 = new byte[_1MB / 4]; allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; }
private static final int _1MB = 1024 * 1024; /** * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 * -XX:HandlePromotionFailure */ @SuppressWarnings("unused") public static void testHandlePromotion() { byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7; // allocation1+allocation2大于survivor空间一半 allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation1 = null; allocation4 = new byte[2 * _1MB]; allocation5 = new byte[2 * _1MB]; allocation6 = new byte[2 * _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 * _1MB]; }
jps [ options ] [ hostid ]
jps执行样例
jps -l
jps -l
1177 org.jetbrains.idea.maven.server.RemoteMavenServer36
1481 jdk.jcmd/sun.tools.jps.Jps
1147
jstat [ option vmid [interval[s|ms] [count]] ]
[protocol:][//]lvmid[@hostname[:port]/servername]
jstat -gc 2764 250 20
jstat -gcutil 7304
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 44.06 39.52 70.35 93.62 88.93 103 0.644 12 0.322 - - 0.966
jinfo [ option ] pid
jinfo -flag CMSInitiatingOccupancyFraction 7304
-XX:CMSInitiatingOccupancyFraction=-1
jmap [ option ] vmid
jmap -dump:format=b,file=idea.bin 7304 [2d5h7m] ✹ ✭
Heap dump file created
jhat idea.bin
Reading from idea.bin...
Dump file created Mon Mar 23 23:39:03 CST 2020
Snapshot read, resolving...
Resolving 2817601 objects...
Chasing references, expect 563 dots......
Eliminating duplicate references.......
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
jstack [ option ] vmid
jstack -l 6275 ⏎ 2020-03-26 22:43:04 Full thread dump OpenJDK 64-Bit Server VM (25.152-b39 mixed mode): "rebel-notifications-queue-1" #57 daemon prio=5 os_prio=31 tid=0x00007ff24a70a000 nid=0x959f waiting on condition [0x0000700009845000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000007a59095c0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1093) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Locked ownable synchronizers: - None
<%@ page import="java.util.Map"%> <html> <head> <title>服务器线程信息</title> </head> <body> <pre> <% for (Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()) { Thread thread = (Thread) stackTrace.getKey(); StackTraceElement[] stack = stackTrace.getValue(); if (thread.equals(Thread.currentThread())) { continue; } out.print("\n线程:" + thread.getName() + "\n"); for (StackTraceElement element : stack) { out.print("\t" + element + "\n"); } } %> </pre> </body> </html>
/** * staticObj、instanceObj、localObj存放在哪里? */ public class JHSDB_TestCase { static class Test { static ObjectHolder staticObj = new ObjectHolder(); ObjectHolder instanceObj = new ObjectHolder(); void foo() { ObjectHolder localObj = new ObjectHolder(); System.out.println("done"); // 设置一个断点 } } private static class ObjectHolder {} public static void main(String[] args) { Test test = new JHSDB_TestCase.Test(); test.foo(); } }
-Xmx10m -XX:+UseSerialGC -XX:-UseCompressedOops
jps -l
2032 jdk.jcmd/sun.tools.jps.Jps
1492 org.jetbrains.idea.maven.server.RemoteMavenServer36
2022 org.jetbrains.jps.cmdline.Launcher
2023 ch3.JHSDB_TestCase
1471
jhsdb hsdb --pid 2023
命令打开的JHSDB的界面如图4-4所示。
图4-4 JHSDB的界面
阅读代码清单4-6可知,运行至断点位置一共会创建三个ObjectHolder对象的实例,只要是对象实例必然会在Java堆中分配,从这三个对象开始着手,
先把它们从Java堆中找出来。
首先点击菜单中的Tools -> Heap Parameters,结果如图4-5所示,因为运行参数中指定了使用的是Serial收集器,图中我们看到了典型的Serial的分代内存布局,
Heap Parameters窗口中清楚列出了新生代的Eden、S1、S2和老年代的容量(单位为字节)以及它们的虚拟内存地址的起止范围。
图4-5 Serial收集器的堆布局
注意图中各个区域的内容地址范围,后面还要用到它们。打开Windows -> Console 窗口,
使用scanoops命令在Java堆的新生代(从Eden起始地址到To Survivor结束地址)范围内查找ObjectHolder的实例,结果如下所示:
hsdb>scanoops 0x0000000103c00000 0x0000000103f50000 JHSDB_TestCase$ObjectHolder
0x0000000103e97bb8 ch3/JHSDB_TestCase$ObjectHolder
0x0000000103e97be0 ch3/JHSDB_TestCase$ObjectHolder
0x0000000103e97bf0 ch3/JHSDB_TestCase$ObjectHolder
果然找到了三个实例的地址,而且它们的地址都落到了Eden的范围之内,算是顺带验证了一般情况下新对象在Eden中创建的分配规则。
再使用Tools -> Inspector功能确认一下这三个地址中存放的对象,结果如图4-6所示。
图4-6 查看对象实例数据
Inspector展示了对象头和指向对象元数据的指针,里面包括了Java类型的名字、继承关系、实现接口关系,字段信息、方法信息、运行时常量池的指针、
内嵌的虚方法表(vtable)以及接口方法表(itable)等。
接下来要根据堆中对象实例地址找出引用它们的指针,使用如下命令:
hsdb> revptrs 0x0000000103e97bb8
null
Oop for java/lang/Class @ 0x0000000103e96388
找到了一个引用该对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址,通过Inspector查看该对象实例,
可以清楚看到这确实是一个java.lang.Class类型的对象实例,里面有一个名为staticObj的实例字段,如图4-7所示。
图4-7 Class对象
接下来继续查找第二个对象实例:
hsdb> revptrs 0x0000000103e97be0
Oop for JHSDB_TestCase$Test @ 0x0000000103e97bc8
这次找到一个类型为JHSDB_TestCase$Test的对象实例,在Inspector中该对象实例显示如图4-8所示。
图4-8 JHSDB_TestCase$Test对象
这个结果完全符合预期,第二个ObjectHolder的指针是在Java堆中JHSDB_TestCase$Test对象的instanceObj字段上。
但是采用相同方法查找第三个ObjectHolder实例时,JHSDB返回了一个null,表示未查找到任何结果。
hsdb> revptrs 0x0000000103e97bf0
null
看来revptrs命令并不支持查找栈上的指针引用,不过因为测试代码足够简洁,可以人工完成这件事情。
在Java Thread窗口中main线程后点击Stack Memory按钮查看该线程的内存,如图4-9所示。
图4-9 main线程的栈内存
这个线程只有两个方法栈帧,尽管没有查找功能,但通过肉眼观察在地址 上的值正好就是0x0000000103e97bf0,而且JHSDB在旁边已经自动生成注释,
说明这里确实是引用了一个来自新生代的JHSDB_TestCase$ObjectHolder对象。
通过JDK/bin目录下的jconsole.exe启动JConsole后,会自动搜索出本机运行的所有虚拟机进程,而不需要自己使用jps来查询,如图4-10所示。
双击选择其中一个程序便可进入主界面开始监控。JMX支持跨服务器的管理,也可以使用下面的“远程进程”功能来连接远程服务器,对远程虚拟机进行监控。
图4-10 JConsole连接页面
图4-10看到有三个本地虚拟机进程。双击MonitoringTest进入JConsole主界面,如图4-11所示。
图4-11 JConsole主界面
-Xms100m -Xmx100m -XX:+UseSerialGC
/** * 内存占位符对象,一个OOMObject大约占64KB */ public class MonitoringTest { static class OOMObject { public byte[] placeholder = new byte[64 * 1024]; } public static void fillHeap(int num) throws InterruptedException { List<OOMObject> list = new ArrayList<>(); for (int i = 0; i < num; i++) { // 稍作延迟,令监视器的变化更加明显 Thread.sleep(50); list.add(new OOMObject()); } System.gc(); } public static void main(String[] args) throws Exception { fillHeap(1000); } }
这段代码的作用是以64KB/50ms的速度向Java堆中填充数据,一共填充1000次,使用JConsole的“内存”页签进行监视,观察曲线和柱状指示图的变化。
程序运行后,在“内存”页签中可以看到内存池Eden区的运行趋势呈现折线状,如图4-12所示。监视范围扩大到整个堆后,会发现曲线是一直平滑增长的。
从柱状图可以看到,在1000次循环执行结束,运行了System.gc后,虽然整个新生代Eden区基本被清空了,但是代表老年代的柱状图仍然保持峰值状态,
说明被填充进堆中的数据在System.gc()方法执行之后仍然存活。
图4-12 Eden区内存变化状况
/** * 线程死循环演示 */ public static void createBusyThread() { Thread thread = new Thread(new Runnable() { @Override public void run() { while (true) { ; } } }, "testBusyThread"); thread.start(); } /** * 线程锁等待演示 */ public static void createLockThread(final Object lock) { Thread thread = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }, "testLockThread"); thread.start(); } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); br.readLine(); createBusyThread(); br.readLine(); Object obj = new Object(); createLockThread(obj); }
程序运行后,首先在"线程"页签中选择main线程,如图4-13所示。堆栈追踪显示BufferedReader的readBytes()方法正在等待System.in的键盘输入,
这时候线程为Runnable状态,Runnable状态的线程仍会被分配运行时间,但readBytes()方法检查到流没有更新就会立刻归还执行令牌给操作系统,
这种等待只消耗很小的处理器资源。
图4-13 main线程
接着监控testBusyThread线程,如图4-14所示。testBusyThread线程一直在执行空循环,从堆栈追踪中看到一直在MonitoringTest.java代码的41行停留,
41行的代码为while(true)。这时候线程为Runnable状态,而且没有归还线程执行令牌的动作,所以会在空循环耗尽操作系统分配给它的执行时间,
直到线程切换为止。
图4-14 testBysyThread线程
图4-15显示testLockThread线程在等待lock对象的notify()或notifyAll()方法的出现,线程这时候处于WAITING状态,
在重新唤醒前不会被分配执行时间。
图4-14 testLockThread线程
testLockThread线程正处于正常的活锁等待中,只要lock对象的notify()或notifyAll()方法被调用,这个线程便能激活继续执行。
代码清单4-9演示了一个无法再被激活的死锁等待。
/** * 线程死锁等待演示 */ static class SynAddRunable implements Runnable { int a, b; public SynAddRunable(int a, int b) { this.a = a; this.b = b; } @Override public void run() { synchronized (Integer.valueOf(a)) { synchronized (Integer.valueOf(b)) { System.out.println(a + b); } } } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(new SynAddRunable(1, 2)).start(); new Thread(new SynAddRunable(2, 1)).start(); } }
这段代码运行后会遇到线程死锁。造成死锁的根本原因是Integer.valueOf()方法出于减少对象创建次数和节省内存的考虑,
会对数值为-128 ~ 127之间的Integer对象进行缓存,如果valueOf()方法传入的参数在这个方位之内,就直接返回缓存中的对象。
也就是说代码中尽管调用了200次Integer.valueOf()方法,但一共只返回了两个不同的Integer对选哪个。
假如某个线程的两个synchronized快之间发生了一次线程切换,那就会出现线程A在等待线程B持有的Integer.valueOf(),
线程B又在等待被线程A持有的Integer.valueOf(),结果大家都跑不下去的情况。
出现线程死锁之后,点击JConsole线程面板的“检测死锁”按钮,将出现一个新的“死锁”页签,如图4-16所示。
图4-16 线程死锁
VisualVM基于NetBeans平台开发工具,所以一开始它就具备了通过插件扩展功能的能力,有了插件扩展支持,VisualVM可以做到:
VisualVM的插件可以手工进行安装,在网站上下载nbm包后,点击"工具->插件->已下载"菜单,然后再弹出对话框中指定nbm包路径便可完成安装。
VisualVM的自动安装已可找到大多数所需的插件,在有网络连接的环境下,点击“工具->插件菜单”,弹出如图4-17所示的插件页签,
在页签的“可用插件”及“已安装”中列举了当前版本VisualVM可以使用的全部插件,选中插件后在右边窗口会显示这个插件的基本信息,
如开发者、版本、功能描述等。
图4-17 线程死锁
读者可根据自己的工作需要和兴趣选择合适的插件,然后点击“安装”按钮,弹出如图4-18所示的下载进度窗口,
跟着提示操作即可完成安装。
图4-18 VisualVM插件安装过程
选择一个需要监视的程序就可以进入程序的主界面了,如图4-19所示。由于VisualVM的版本以及选择安装插件数量的不同,
页签可能有所差别。
图4-19 VisualVM主界面
在VisualVM中生成堆转储快照文件有两种方式,可以执行下列任一操作:
生成堆转储快照文件之后,应用程序页签会在该堆的应用程序下增加一个以[heap-dump]开头的子节点,并且在主页签中打开该转储快照,
如图4-20所示。如果需要把堆转储快照保存或发送出去,就应在heapdump节点上右键选择”另存为“菜单,否则当VisualVM关闭时,
生成的堆转储快照文件会被当做临时文件自动清理掉。要打开一个由已经存在的堆转储快照文件,通过文件菜单的”装入“功能,选择硬盘上的文件即可。
图4-20 浏览dump文件
堆页签中的”摘要“面板可以看到应用程序dump时的运行参数、System.getProperties()的内容、线程堆栈等信息:”类“面板则是以类为统计口径统计类的实例数量、
容量信息;”实例“面板不能直接使用,因为VisualVM在此时还无法确定用户想查看哪个类的实例,所以需要通过”类“面板进入,在”类“中选择一个需要查看的类,
然后双击即可在”实例“里面看到此类的其中500个实例的具体属性信息;“OOL控制台”面板则是运行OOL查询语句的,同jhat中介绍的OOL功能一样。
在Profiler页签中,VisualVM提供了程序运行期间方法级的处理器执行时间分析以及内存分析。
要开始性能分析,先选择“CPU”和“内存”按钮中的一个,然后切换到应用程序中对程序进行操作,VisualVM会记录这段时间中应用程序执行过的所有方法。
如果是进行处理器执行时间分析,将会统计每个方法的执行次数、执行耗时;如果是内存分析,则会统计每个方法关联的对象以及这些对象所占的空间。
等要分析的操作执行结束后,点击“停止”按钮结束监控过程、如图4-21所示。
图4-21 对应用程序进行CPU执行时间分析
BTrace是一个很神奇的VisualVM插件,它本身也是一个可运行的独立程序。BTrace的作用是在不中断目标程序运行的前提下,
通过HotSpot虚拟机的Instrument功能动态加入原本不存在的调试代码。这项功能对实际中的程序很有意义:如当程序出现问题时,
排查错误的一些必要信息时(譬如方法参数、返回值等),在开发时并没有打印日志之中以至于不得不停掉服务时,都可以通过调试增量来加入日志代码一解决问题。
在VisualVM中安装了BTrace插件后,在应用程序面板中右击要调试的程序,会出现“Trace Application…”菜单,点击将进入BTrace面板。
这个面板看起来就像一个简单的Java程序开发环境,里面甚至已经有了一小段Javad代码,如图4-22所示。
图4-22 BTrace动态追踪
现有一段简单的Java代码来演示BTrace的功能:产生两个1000以内的随机整数,输出这两个数字相加的结果,如代码清单4-10所示。
代码清单4-10 BTrace跟踪演示
public class BTraceTest { public int add(int a, int b) { return a + b; } public static void main(String[] args) throws IOException { BTraceTest test = new BTraceTest(); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); for (int i = 0; i < 10; i++) { reader.readLine(); int a = (int) Math.round(Math.random() * 1000); int b = (int) Math.round(Math.random() * 1000); System.out.println(test.add(a, b)); } } }
/* BTrace Script Template */ import com.sun.btrace.annotations.*; import static com.sun.btrace.BTraceUtils.*; @BTrace public class TracingScript { @OnMethod( clazz="ch3.BTraceTest", method="add", location=@Location(Kind.RETURN) ) public static void func(@Self ch3.BTraceTest instance, int a, int b, @Return int result) { println("调用堆栈:"); jstack(); println(strcat("方法参数A:", str(a))); println(strcat("方法参数B:", str(b))); println(strcat("方法参数结果:", str(result))); } }
点击Start按钮后稍等片刻,编译完成后,Output面板中会出现“BTrace code successfully deployed”的字样。
当程序运行时将会在Output面板输出如图4-23所示的调试信息。
图4-23 BTrace跟踪结果
BTrace的用途很广发,打印调用堆栈、参数、返回值只是它最基础的使用形式,在它的网站上有使用BTrace进行性能监视、
定位连接泄漏、解决多线程竞争问题等的使用案例。
BTrace能够实现动态修改程序行为,是因为它是基于Java虚拟机的Instrument开发的。Instrument是Java虚拟机工具接口的重要组件,
提供了一套代理(Agent)机制,使得第三方工具程序可以以代理的方式访问和修改Java虚拟机的内部的数据。
阿里巴巴开源的诊断工具Arthas也通过Instrument实现了与BTrace类似的功能。
持续收集的JFR(Java Flight Recorder)是一套内建在HotSpot虚拟机里面的监控和基于事件的信息搜集框架,与其他的监控工具(如JProfiling)相比,
Oracle特别强调它”可持续在线“的特性。JFR在JFR在生产环境中对吞吐量一般不会高于1%,而且JFR监控过程的开始、停止都是完全可动态的,
即不需要重启应用。JFR的监控对应用也是完全透明的,即不需要对应用程序的源码做任何修改,或者基于特定的代理来运行。
JMC与虚拟机之间同样采取JMX协议进行通信,JMC一方面作为JMX控制台,显示来自虚拟机MBean提供的数据;另一方面作为JFR的分析工具,
展示来自JFR的数据。启动后JMC的主界面如图4-24所示。
图4-24 JMC主界面
在左侧的”JVM浏览器“面板中自动显示了通过JDP协议(Java Discovery Protocol)找到的本机正在运行的HotSpot虚拟机进程,
如果需要监控其他服务器上的虚拟机,可在”文件->连接”菜单中创建远程连接,如图4-25所示。
图4-25 JMC建立连接界面
这里要填写信息应该在被监控虚拟机进程启动的时候以虚拟机参数的形式指定,以下是一份被监控端的启动参数样例:
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=192.168.31.4
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
本地虚拟机与远程虚拟机进程的差别只限于创建连接这个步骤,连接成功创建以后的操作就是完全一样的了。把”JVM浏览器”面板中的进程展开后,
可以看到每个进程的数据都有MBean和JFR两个数据源。
双击“飞行记录器”,将会出现“启动飞行记录”窗口,如图4-26所示。
-
图4-26 启用飞行记录仪
点击“完成”按钮后马上就会开始记录,记录时间结束以后会生成飞行记录报告,如图4-27所示。
图4-27 飞行记录仪报告
飞行记录报告包含以下几类信息:
JFR的基本工作逻辑是开启一系列的录制动作,当某个事件发生时,这个事件的所有上下文数据将会以循环日志的形式被保存至内存或者指定的某个文件当中,
循环日志相当于数据流被保留在一个环形缓存中,所以只有最近发生的事件的数据才是可用的。JMC从虚拟机内存或者文件中读取并展示这些事件数据,
并通过这些数据进行性能分析。
public class Bar {
int a = 1;
static int b = 2;
public int sum(int c) {
return a + b + c;
}
public static void main(String[] args) {
new Bar().sum(3);
}
}
java -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline, *Bar.sum -XX:CompileCommand=compileonly,*Bar.sum test.Bar
其中,参数-Xcomp是让虚拟机以编译模式执行代码,这样不需要执行足够次数来预热就能触发即使编译。
两个-XX:CompileCommand的意思是让编译器不要内联sum()并且只编译sum(),-XX:+PrintAssembly就是输出反汇编内容。
JITWatch是HSIDS经常搭配使用的可视化的编译日志分析工具,为便于在JITWatch中读取,
读者可使用以下参数把日志输出到logfile文件:
-XX:+UnlockDiagnosticVMOptions
-XX:+TraceClassLoading
-XX:+LogCompilation
-XX:LogFile=/tmp/logfile.log
-XX:+PrintAssembly
-XX:+TraceClassLoading
在JITWatch中加载日志后,就可以看到执行期间使用过的各种类型和对应调用过的方法了,界面如图4-28所示。
图4-28 JITWatch主界面
选择想要查看的类和方法,即可查看对应的Java源代码、字节码和即时编译器生成的汇编代码,如图4-29所示。
图4-29 查看方法代码
一个很久前的案例,但今天仍然具有代表性。一个15万PV/日左右的在线文档类型网站最近更新了硬件系统,
服务器的硬件为四路志强处理器、16GB物理内存,操作系统为64位CentOS 5.4,Resin作为Web服务器。
整个服务器暂时没有部署别的应用,所有硬件资源都可以提供给这访问量并不算太大的文档网站使用。
软件版本选用的是64位的JDK5,管理员启用了一个虚拟机实例,使用-Xmx和-Xms参数将Java堆大小固定在12GB。
使用一段时间后发现服务器的运行效果十分不理想,网站经常不定期出现长时间失去响应。
监控服务器运行状况后发现网站失去响应是由垃圾收集停顿所导致的,在该系统软硬件条件下,HotSpot虚拟机是以服务端模式运行,
默认使用的是吞吐量优先收集器,回收12GB的Java堆,一次Full GC的停顿时间就高达14秒。由于程序设计的原因,
访问文档时会把文档从磁盘提取到内存中,导致内存中出现很多文档序列化产生的大对象,这些大对象大多数在分配时就直接进入了老年代,
没有在Minor GC中被清理掉。这种情况下即使有12GB的堆,内存也会很快被消耗殆尽,由此导致每隔几分钟出现十几秒的停顿就,
令网站开发、管理员都对使用Java技术开发网站感到很失望。
分析此案例的情况,程序代码问题这里不延伸讨论,程序部署上的主要问题显然是过大的内存进行回收时带来的长时间的停顿。
经调查,更早之前的硬件使用的是32位操作系统,给HotSpot虚拟机只分配了1.5G的堆内存,当时用户确实感觉使用网站比较缓慢,
但还不至于发生长达十几秒的明显停顿,后来将硬件升级到64位系统、16GB内存希望能提升程序效能,却反而出现了停顿问题,
尝试过将Java堆分配的内存重新缩小到1.5GB或者2GB,这样的确可以避免长时间停顿,但是在硬件上的投资就显得非常浪费。
目前单体应用在较大内存的硬件上主要的部署方式有两种:
此案例中的管理员采用了第一种部署方式。对于用户交互性强、对停顿时间敏感、内容又较大的系统,
并不是一定要使用Shenandoah、ZGC这些明确以控制延迟为目标的垃圾收集器才能解决问题,
使用Parallel Scavenge/Old收集器,并且给Java虚拟机分配较大的堆内存也是有很多运行成功的案例的,
一个类型从被加载到虚拟机内存中开始,到卸载初内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,
其中验证、准备、解析三个部分统称为连接。这七个阶段的发生顺序如图7-1所示。
对于初始化阶段,《Java虚拟机规范》严格规定了有且只有六种情况必须对类进行“初始化”:
以上六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会出发初始化,称为被动引用。以下三点说明何为被动引用。
public static int value = 123;
public static final int value = 123;
public class Test {
static {
i = 0; // 给变量复制可以正常编译通过
System.out.println(i); // 这句编译会提示"非法向前引用"
}
static int i = 1;
}
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
public class DeadLoopClass { static { // 如果不加上这个 if 语句,编译器提示"Initializer must be able to complete normally"并拒绝编译 if (true) { System.out.println(Thread.currentThread() + "init DeadLoopClass"); while (true) { } } } } class DeadLoopClassTest { public static void main(String[] args) { Runnable script = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread() + "start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + "run over"); } }; Thread thread1 = new Thread(script); Thread thread2 = new Thread(script); thread1.start(); thread2.start(); } }
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]init DeadLoopClass
/** * 类加载器与 instanceof 关键字演示 */ public class ClassLoaderTest { public static void main(String[] args) throws Exception { ClassLoader myLoader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } }; Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance(); System.out.println(obj.getClass()); System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest); } }
class org.fenixsoft.classloading.ClassLoaderTest
false
本节内容将针对JDK8及之前版本的Java来介绍什么事三层类加载器,以及什么是双亲委派模型。对于这个时期的Java应用,
绝大多数Java程序都会使用到以下3个系统提供的类加载器进行加载。
图7-2 类加载器双亲委派模型
图7-2中展示的各种类加载器之间的层次关系被称为类加载器的"双亲委派模型"。双亲委派模型要求除了顶层的启动类加载器外,
其余的类加载器都应该有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,
每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,
子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载进行加载,
因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,
如果用户自己也编写了一个名为java.lang.Object的类,并且放在程序中的ClassPath中,那系统中就会出现多个不同的Object类,
Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
双亲委派模型对于保证Java程序中的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委派的代码只有短短十余行,
全部集中在java.lang.ClassLoader的loadClass()方法之中,如代码清单7-10所示。
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先,检查请求的类是否已经被加载过了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父类加载器抛出 ClassNotFoundException // 说明父类加载器无法完成加载请求 } if (c == null) { // 在父类加载器无法加载时 // 再调用本身的 findClass 方法来进行类加载 long t1 = System.nanoTime(); c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
为了保证兼容性,JDK9并没有从根本上动摇三层类加载器架构以及双亲委派模型。但为了模块化系统的顺利施行,
模块化下的类加载器仍然发生了一些应该被注意到的变动,主要包括以下几个方面。
扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代。
平台类加载器和应用程序类加载器不再派生自java.net.URLClassLoader。
JDK9中虽然仍然维持着三层类加载器和双亲委派的架构,但类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,
在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载,
也许这可以算是对双亲委派的第四次破坏。在JDK9以后的三层类加载器的架构如图7-7所示,可以对照图7-2进行比较。
图7-7 JDK9后的类加载器委派关系
代码的编译结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
Java虚拟机以方法作为最基本的执行单元,"栈帧"则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,
它也是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包含了局部变量表、操作数栈、动态连接、方法返回地址和一些额外附加信息。在编译Java程序源码的时候,
栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,并且写入到方法表的Code属性之中。换言之,一个栈帧需要分配多少内存,
并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。
一个线程中的方法调用链可能会很长,以Java程序的角度来看,同一时刻,同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。
而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈顶才是生效的,其被称为"当前栈帧",
与这个栈帧所关联的方法被称为"当前方法"。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作,在概念模型上,典型的栈帧结构如图8-1所示。
图8-1 栈帧的概念结构
图12-1 处理器、高速缓存、主内存间的交互关系
Java内存模型对于64位的数据类型(long和double)定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,
即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的“long和double的非原子性协定“(Non-Atomic Treatment of double and long Variables)。
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度。
目前线程是Java里面进行处理器资源调度的最基本单位,不过如果日后Loom项目能够成为Java引入纤程(Fiber)的话,可能就会改变这一点。
实现线程主要有三种方式:使用内核线程实现(1:1实现),使用用户线程实现(1:N实现),使用用户线程加轻量级进程混合实现(N:M实现)。
1.内核线程实现
使用内核线程实现的方式也被称为1:1实现。内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,
内核通过操作调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,
这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为多线程内核(Multi-Threads Kernel)。
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都有一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。
这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型,如图12-3所示。
图12-3 轻量级线程与内核线程之间1:1的关系
由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中某一个轻量级进程在系统调用中被阻塞了,也不会影响整个进程继续工作。轻量级进程也具有它的局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。
而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。
2.用户线程实现
图12-4 进程与用户线程之间1:N的关系
3.混合实现
4.Java线程的实现
图12-6 线程状态转换关系
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。