(2.2 运行时数据区域)jvm内存划分为5个区域,简单说就是2个堆(堆和非堆(方法区的别称))、2个栈(虚拟机栈和本地方法栈)、1个程序计数器
(2.2.5 方法区)方法区和永久代的区别在于(JDK 8以前),前者是java虚拟机规范的定义,而永久代是HotSpot虚拟机下特有的实现,且属于方法区的一部分,这么设计的目的就是为了垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的 工作。
(2.3.1 对象的创建)本节讲到:由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成 这两条字节码指令,验证测试代码如下所示:
1
2
3
4
5
6
7
8
9package com.example.demo.test;
public class Test2 {
public static void main(String[] args) {
final Test2 test2 = new Test2();
System.out.println(test2);
}
}执行:
javac Test2.java
,然后执行:javap -v Test2.class
查看字节码文件,输出内容为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67Classfile /mnt/f/Develop/Project/demo/src/main/java/com/example/demo/test/Test2.class
Last modified Dec 14, 2020; size 436 bytes
MD5 checksum b6b5fa11a474d65891c6991ba0d7143a
Compiled from "Test2.java"
public class com.example.demo.test.Test2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // com/example/demo/test/Test2
#3 = Methodref #2.#15 // com/example/demo/test/Test2."<init>":()V
#4 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#6 = Class #21 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test2.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Utf8 com/example/demo/test/Test2
#17 = Class #22 // java/lang/System
#18 = NameAndType #23:#24 // out:Ljava/io/PrintStream;
#19 = Class #25 // java/io/PrintStream
#20 = NameAndType #26:#27 // println:(Ljava/lang/Object;)V
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/System
#23 = Utf8 out
#24 = Utf8 Ljava/io/PrintStream;
#25 = Utf8 java/io/PrintStream
#26 = Utf8 println
#27 = Utf8 (Ljava/lang/Object;)V
{
public com.example.demo.test.Test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/example/demo/test/Test2
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 15
}
SourceFile: "Test2.java"上述输出中,第54行为源码第6行的
final Test2 test2 = new Test2();
,第56行就是文中说的invokespecial。(2.3.2 对象的内存布局)源码地址:jdk8
Mark Word(64 bits) 锁状态 unused:25 | hash:31 | unused:1 | age:4 | biased_lock:0 | lock:01 正常 thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:01 偏向锁 ptr_to_lock_record:62 | lock:00 轻量级锁 ptr_to_heavyweight_monitor:62 | lock:10 重量级索 | lock:11 GC标记 当开启指针压缩(UseCompressdOops)时,正常状态和偏向锁状态下的两个unused字段会变成:cms_free,具体参考:stackoverflow
(2.3.3 对象的访问定位)句柄访问和直接指针访问的本质区别在于:前者是间接引用(相当于中间又增加了一层,从而带来了更好的灵活性),后者是直接引用。
(2.4.2 虚拟机栈和本地方法栈溢出)经测试64位Linux下Xss最小值为:228k,默认值为:1024K(64位Windows 10下,两个值分别为:108K和0)
1
2
3
4
5
6
7
8
9
10
11
12java -Xss128K com.example.demo.test.Test2
The stack size specified is too small, Specify at least 228k
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
intx CompilerThreadStackSize = 0 {pd product}
intx ThreadStackSize = 1024 {pd product}
intx VMThreadStackSize = 1024 {pd product}
openjdk version "1.8.0_275"
OpenJDK Runtime Environment (build 1.8.0_275-8u275-b01-0ubuntu1~18.04-b01)
OpenJDK 64-Bit Server VM (build 25.275-b01, mixed mode)TIPS:
- 如果运行程序出现了OutOfMemoryError异常,发生的区域很小的几率发生在虚拟机栈(主要发生区域是堆),理由是:HotSpot虚拟机不支持栈的动态扩展,所以线程运行期间不会申请内存扩展,从而不会导致OOM,那么只会出现在创建线程申请内存时出现OOM,有3种措施会可以促进这种OOM:1、虚拟机栈空间尽量小;2、每个线程的栈内存尽量大(Xss);3、创建的线程足够多。更详细地说,如果是在32位Windows系统下,由于单个进程最大内存限制是2G,那么排除掉Xmx的堆最大容量、直接内存、jvm本身消耗内存以及程序计数器(可以忽略不计)内存,剩下的内存才是虚拟机栈和本地方法栈的可用内存,如果虚拟机栈内存足够小,而且Xss配置得又偏大,那么,如果创建非常多线程的情况下,是有可能出现创建线程时导致虚拟机栈OOM的。
- 如果允许程序出现了StackOverflowError异常,比较大的可能性是递归调用导致了栈溢出,比较小的可能性是因为调用堆栈过深导致栈溢出,因为对于后者来说,默认Xss的大小为1024k,是很难导致溢出的。
(2.4.3 方法区和运行时常量池溢出)自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中
(2.4.4 本机直接内存溢出)OOM后,dump文件比较小,而且又使用的Direct Memory,那么就要考虑是否是直接内存OOM
(3.2.3 再谈引用)强引用:打死也不能回收;软引用(SoftReference):OOM前回收;弱引用(WeakReference):下次GC时回收;虚引用(PhantomReference):根本不算引用,因为无法获得引用对象,应用场景:为了能在这个对象被收集器回收时收到一个系统通知。
(3.3.1 分代收集理论)
- 垃圾分代收集的本质在于对象的年龄。朝生夕灭的小鲜肉放在一起管理(称之为年轻代),相亲无数(也就是躲过了多次垃圾回收)的大龄青年(称之为老年代)放在一起管理,这可真够现实的。
- 除了CMS收集器,其他都不存在只针对老年代的收集
(3.3.2 标记-清除算法(Mark-Sweep))原理:标记处房间内的垃圾,然后直接清理掉,缺点:房间还是很乱(内存碎片),因为只清理了垃圾,而没有整理房间
(3.3.3 标记-复制算法(Mark-Copy))多用于年轻代垃圾收集器,原理:
- 基础版:房间分成两部分,每次只用其中一部分,每次回收时,清理垃圾,并将物品搬到另一个房间,缺点:浪费空间。
- 进阶版:房间分成三部分,一大两小(8:1:1),每次只用一大和一小,每次回收时,清理垃圾,并将物品搬到另一个房间
(3.3.4 标记-整理算法(Mark-Compact))多用于老年代垃圾收集器,原理:在标记-清除算法的基础上,额外增加整理(更形象的说,是“压缩整理”,从标记-整理算法的英文名:Mark-Compact就能看出来)的功能。
TIPS:
- 对于CMS收集器,平时多数时间都采用(并发)标记-清除算法,这也应对CMS的全称:Concurrent Mark Sweep,当空间碎片过多时采用标记-整理算法。这也对应CMS算法的缺点:1、容易产生内存碎片;2、一旦由于碎片过多触发了标记整理算法,就会导致长时间的STW
(3.4.2 安全点)安全点停顿:几乎所有虚拟机不再采用抢先式中断(Preemptive Suspension)而是采用主动式中断(Voluntary Suspension),也即虚拟机负责设置标记位,各个线程负责轮询检查标记位,然后运行到安全点上(有点类似于游戏服务器的某个全局模块设置标记位,在线玩家心跳逻辑里轮询检测这个标记位)。
(3.4.4 记忆集与卡表)通俗地讲,从垃圾收集区域来看,Remembered Set指的是谁指向了我的集合(如果有人指向了我,那么这个“我”就不能当做垃圾被回收)!这是一个抽象的逻辑概念模型。从具体实现的角度来看,最原始的Remembered Set就是存储对方所有跨带引用对象的集合,但是这是非常低效的,因此可以把这个“我”的区域(也即垃圾收集区域)划分成若干子区域(每个区域称之为卡页:Card Page),然后用一个数组标记该区域是否含有跨代引用指针(这个数组就是卡表:Card Table),在GC时将Card Table中存在跨代引用的Card Page进行扫描,从而减少GC Roots的扫描的范围(这种分组的思想是不是很熟悉,想一下武汉市在医疗资源紧缺时采取的新冠核酸筛的混检模式,就是讲每10人混合成一个样本,筛查效率理论上可以提升到原来的10倍)。
(3.4.5 写屏障)这一节讲了两件事:
卡表的状态维护机制:写屏障,通俗讲就是虚拟机层面的AOP
当多线程写同一块内存时,如果这块内存被同一个缓存行的卡表对应,那么意味着这个位于同一个缓存行的卡表会被多个线程修改,导致性能降低,优化机制为:不要无条件修改卡表,而是加一个判断(额,这个优化真的非常非常初级啊)
(3.4.6 并发的可达性分析)
- 三色标记:白色:还未过安检;黑色:已经过安检,安全;灰色:安检中
- 两种条件同时满足时,会出现白色误判为垃圾:1、扫描期间某黑色新指向了白色对象;2、同时某灰色到该白色的直接或间接引用也恰巧删除。这就导致了该白色对象仅被黑色对象引用,而黑色对象是安全的,不会再次扫描,这就导致错过扫描而被误判为垃圾。举一个形象的例子:三个包过机场的安检仪:黑包–灰包–白包这3个包用绳子相继连着,此时黑包已过安检,灰包安检中,白包等待安检,突然风雨突变,灰包和白包的绳子莫名断掉,同时有一股神秘力量将黑包和小白连起来(来自用户线程),但是这个连线却没穿过安检仪,那这种情况下白包就会错过安检(这个例子真是绝,我真是大聪明!)
- 消除白色误判的两种策略:1、破除上述条件一,也即增量更新(Incremental Update):如果黑色在扫描期间指向了白色,那就让它变灰;2、破除上述条件二,也即原始快照(Snapshot At The Beginning, SATB):先拍个照,当进行扫描时,不管灰色是否删除了白色的指向,都按照这个快照扫描。CMS是基于增量更新,G1、Shenandoah则是用原始快照。
(3.5.6 CMS收集器)
收集过程分成4个阶段:
- 初始标记(CMS initial mark):标记GC Roots能直接关联到的对象,会引发短暂的STW
- 并发标记(CMS concurrent mark):也即三色标记阶段,会导致某些白色被误判垃圾,也会导致漏过某些白色而产生浮动垃圾
- 重新标记(CMS remark):采用增量更新策略,重新标记,会引发STW
- 并发清除(CMS concurrent sweep):这也是CMS名称的由来,并发清除(concurrent sweep)
缺点:
- 并发标记对CPU敏感(默认GC线程数=处理器核心数量 +3)/ 4):CPU核心数大于等于4个时,并发标记线程数不超过25%,当并发线程数小于4个时,有一半运算能力执行GC,因此CPU敏感。
- 由于GC过程中浮动垃圾无法收集(要留到下一次),因此需要为老年代预留出一部分空闲空间(-XX:CMSInitiatingOccupancyFraction,JDK 6以后该默认值从JDK 5的68%提升至92%),防止并发失败(Concurrent Mode Failure)导致临时启用Serial Old。
- CMS正如它的名字所示,属于标记清除算法,因此在GC结束后会导致大量内存碎片。当申请大对象内存时,可能会出现内存碎片过多无法分配,导致提前触发Full GC(有两个参数:-XX:+UseCMSCompactAtFullCollection和-XX:CMSFullGCsBeforeCompaction可以相对改善,具体细节可以使用时查看相关资料)。
TIPS:
- 所有收集器中只有CMS有针对老年代的Old GC
(3.5.7 Garbage First收集器)
JDK 7 Update 4时,G1的”Experimental“的标识被移除,到JDK 8 Update 40,提供类卸载的支持,变成了Oracle官方称为“全功能的垃圾收集 器”(Fully-Featured Garbage Collector)
G1的Remembered Set结构:由于存在多个Region,而每个Region都要维护的Remembered Set都需要存储谁引用了我,因此Remembered本质上是哈希表,key为其他Region的起始地址,value为一个集合,存储的是该Region中卡表的索引号。这存在两个问题:1、Remembered Set是一个双向索引,这维护起来就麻烦了;2、Remembered Set比较多,因此要消耗额外的堆内存,大约在10%~20%之间。
Region中有一部分内存用于GC并发回收中的对象分配,这个区域都位于TAMS(Top at Mark Start)指针之上。G1默认TAMS地址以上的对象都是存活的,不进行GC。
G 1停顿预测模型一句话总结:每个Region进行回收统计,包括:回收耗时、Remember Set中脏卡数量等,G1根据每个Region的统计状态的新旧评价回收价值,并预测哪些Region集合可以在-XX:MaxGCPauseMillis内回收时获得最高收益。
收集过程同样(相对于CMS)分成4个阶段:
- 初始标记(Initial Marking):标记GC Roots能直接关联到的对象,还要额外(相对于CMS)修改TAMS指针的值,会引发短暂的STW
- 并发标记(Concurrent Marking):也即三色标记阶段,还要额外重新处理SATB记录上并发标记期间引用改动的对象。
- 最终标记(Final Marking):短暂的STW,处理上一步中遗留的少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation):1、更新Region的统计数据,并根据统计价值和成本进行排序;2、筛选Region回收集合;3、把回收Region集合中的存活对象移动到空的Region中,然后清空所有回收集合Region全部空间,由于需要移动存活对象,因此会引发STW,但是会多线程并行完成。
从G1开始,垃圾收集器的设计导向发生变化:追求全堆清理转向追赶内存分配速率(Allocation Rate)。我个人把这个新原则称之为遇熊逃命原则,也即不追求跑得最快,只要求不是最慢即可,换到GC角度上,那就是只要应付得了内存分配的速率即可。只要我打扫垃圾的速度比你制造垃圾的速度足够快,足以。这个理念非常牛B啊,所以才说G1是收集器的一个里程碑。
TIPS:
按理来说,G1是全堆回收的,那为什么仍然存在年轻代和老年代?个人推测是因为仍然沿用了HotSopt垃圾分代框架,而书中也多次用了“扮演新生代”或“扮演老年代”的说法,这也可以证明这个推测。
书中没讲到的G1的思想:G1实际上采取的是分治算法的思想,将大问题分解成小问题,逐个击破。从整体上讲,它属于标记-整理算法(根据前文内容,整理算法是让所有存活对象移动到内存一端,如果按照这个标准,那G1整体上并非严格意义上的标记-整理算法),而从局部上讲,又属于标记复制算法(Region A移动到Region B)。
(3.6 低延迟垃圾收集器)
- Shenandoah和G1的3个不同之处:
- 支持并发整理算法,G1在第4个阶段进行筛选回收时,是多线程并行处理,但是不会并发。
- 默认不使用分代收集,不存在新生代和老年代的Region
- 将G1的Remembered Set改为连接矩阵(Connection Matrix):降低维护成本,也降低伪共享的发生概率。
- Shenandoah收集过程:
- 初始标记(Initial Marking),标记GC Roots能直接关联到的对象,会引发短暂的与堆大小无关的STW
- 并发标记(Concurrent Marking):与G1一样,并发且并行
- 最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,引发短暂的STW
- 并发清理(Concurrent Cleanup):清理连一个存活对象都没有找到的Region
- 并发回收(Concurrent Evacuation):这是Shenandoah的核心改进,通过读屏障和Brooks Pointers转发指针实现并发回收。
- 初始引用更新(Initial Update Reference):确保上一个并发回收阶段的线程都已经完成对象移动任务,会引发STW。
- 最终引用更新(Final Update Reference):修正存在于GC Roots 中的引用,最后一次引发STW,停顿时间只与GC Roots数量有关。
- 并发清理(Concurrent Cleanup):清理Immediate Garbage Regions。
TIPS:
- 本节开头提了:内存占用(Footprint)、吞吐量(Throughput)、延迟(Latency)组成了三元悖论,意指三者只能得其二,类似的三元悖论还有很多,例如分布式系统中著名的CAP定理:一致性(Consistency)、可用性(Availability)、分区容忍性(Partition Tolerance)。详情可以参考:周大的另外一本开源书籍或IBM Cloud Learn Hub
- Shenandoah将Remembered Set改为连接矩阵,从数据管理的视角上来看,实际上是把数据分散处理调整为全局集中处理,这也符合数据驱动开发的思想。例如在游戏开发领域,双向游戏好友是比较常见的社交功能,相比于把好友数据存储到每个玩家身上,维护一系列全局的好友关系数据将是更好的选择。因为如果使用前一个方案,那么在分布式环境下,单就数据一致性的这个问题,就会要求架构底层需要很好的支持,功能逻辑的复杂性也会大大增加。
- Brooks Pointer通过CAS操作保证并发时对象访问的正确性,CAS真是并发编程一大神器啊!
- Shenandoah和G1的3个不同之处:
(3.6.2 ZGC收集器):
- Region分成3种:Small Region(2MB,存储<256KB对象)、Medium Region(32MB,存储>=256KB&&<4MB 对象)、Large Region(2MB的整数倍,最小为4MB,存储>=4MB对象)
- Shenandoah收集过程:
- 并发标记(Concurrent Mark):操作与G1、Shenandoah类似,也会引发STW,区别在于标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志 位。
- 并发预备重分配(Concurrent Prepare for Relocate):1、根据特定的查询条件统计得出 本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set);2、类卸载和弱引用处理(从JDK 12开始支持)。
- 并发重分配(Concurrent Relocate):复制Relocation Set中的存活对象到新Region,每个Region维护转发表(Forward Table),记录转发关系。并发期间,如果对象访问,由于染色指针的存在,ZGC知道该对象是否处于Reloaction Set,因此会被内存屏障截获,转发到新Region上,并同时更新引用(指针自愈)。
- 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所 有引用。实际上即使没有这一步,这些对象也可以在之后的访问时,通过指针自愈实现重映射。ZGC将此工作合并到下一次垃圾收集循环的并发标记阶段中,从而更加节省了一次对象图的遍历。
TIPS:
- 在上述第3个“并发重分配”过程,当复制Relocation Set中的存活对象到新Region时,周大没有详述实现细节,推测应该有类似的CAS操作来更新对象指针的染色指针,这是一个小遗憾。
- 根据上述第4个“并发重映射”过程的描述,其过程被合并到下一次GC中,那ZGC岂不是只有3个大的过程?这是一个不太大的疑惑点。
(3.8.5 空间分配担保)在JDK 6 Update 24以前,当老年代最大连续可用空间小于新生代所有对象总和时,两种情况下会触发Full GC:
- -XX:+HandlePromotionFailure允许担保失败,但是老年代最大可用的连续空间是否小于历次晋升到老年代对象容量的平均大小
- -XX:-HandlePromotionFailure不允许担保失败
TIPS:
在JDK 6 Update 24之后,HandlePromotionFailure参数已经被去掉,测试代码:
1
2
3
4
5☺ java -XX:+PrintFlagsFinal -XX:+HandlePromotionFailure -version
Unrecognized VM option 'HandlePromotionFailure'
Did you mean '(+/-)PromotionFailureALot'?
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.上述规则调整为:只要老年代的连续空间大于等于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行Full GC,换言之只有这两个条件同时满足,蔡楚发Full GC。
(4.2.2 jstat:虚拟机统计信息监视工具):远程虚拟机进程的VMID为:[protocol:][//]lvmid[@hostname[:port]/servername]
(7.3.5 初始化)关于代码清单7-6中,字段B的值将会是2而不是1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14static 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);
}书中的解释并不是很彻底,可以使用javac编译,然后借助
javap -v
命令得到更清新的答案,其Parent的javap命令解析如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36javap -v StaticMethod\$Parent master ✗
Warning: Binary file StaticMethod$Parent contains com.example.demo.test.StaticMethod$Parent
Classfile /mnt/f/Develop/Project/demo/src/main/java/com/example/demo/test/StaticMethod$Parent.class
Last modified Dec 24, 2020; size 399 bytes
MD5 checksum 0f4fdfb9857a60ee769dbb6b4a2f3a0d
Compiled from "StaticMethod.java"
class com.example.demo.test.StaticMethod$Parent
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #4.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#15 // com/example/demo/test/StaticMethod$Parent.A:I
#3 = Class #17 // com/example/demo/test/StaticMethod$Parent
省略……
{
省略……
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #2 // Field A:I
4: iconst_2
5: putstatic #2 // Field A:I
8: return
LineNumberTable:
line 6: 0
line 8: 4
line 9: 8
}
SourceFile: "StaticMethod.java"
InnerClasses:
static #18= #3 of #16; //Parent=class com/example/demo/test/StaticMethod$Parent of class com/example/demo/test/StaticMethod通过上述代码可以发现,Line 54-55应对与源码中的类变量的定义:
public static int A = 1;
,而Line 56-57应对与有静态语句块中的赋值:A = 2;
,也就是说:编译后的static静态语句块除了包含程序员的逻辑代码,还额外插入了编译器自动生成的对静态变量初始化的代码,且这些插入代码的位置符合如下原则:如果静态变量定义在static静态语句块上面,那么在程序员逻辑代码上面按照声明顺序插入,否则在程序员的逻辑代码下面按照声明顺序插入。当Parent类完成初始化后,成员变量A的值为2,当Sub类进行初始化时,要调用自己的<clinit>()
,按照文中所述:Java虚拟机会保证在子类的()方法执行前,父类的()方法已经执行 完毕。Sub进行初始化时A的值已经是2,因此赋值给B后就是2了。(7.4.2 双亲委派模型)
“双亲委派”这个术语实在是翻译得败笔,根据知乎上一个比较形象的回答,翻译为“啃老模式”更为形象。也就是说遇到类加载请求时,先让父加载器去加载,父加载器加载不了了自己才去加载。
(8.3.1 解析)静态方法、私有方法、实例构造器、父类以及final方法(由于历史设计的原因,final方法是使用invokevirtual指令来调用)在类加载的时候就可以把符号引 用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-Virtual Method)
(8.3.2 分派)
- 静态分派对应的是方法的重载(Overload),动态分配对应的是类的重写(Override)。
- 当子类声明了与父类同名的字段时,虽然在子类的内存中两 个字段都会存在,但是子类的字段会遮蔽父类的同名字段
(8.4.3 java.lang.invoke包)一句话解释Reflection和MethodHandle的区别:Reflection是重量级,而MethodHandle 是轻量级。
对于普通变量(相对于volatile变量):要求read/load、write/store成对、顺序出现,但是允许不连续出现(也即中间可以插入其他指令)。
对于volatile变量:
- 只有当前一个指令是load时,才允许执行use;且只有后一个动作是use时,才能执行load动作。换句话说:额外要求load和use是连续成对出现的,结合上面第2条,那就是use/load/read一条龙。这条规则要求在工作内存中,每次使用volatile变量前都必须先从主内存刷新最新的值,保证能看见其他线程对该变量的修改。
- 只有当前一个指令是assign时,才允许执行store;且只有后一个动作是store时才能执行assign动作。换句话说:额外要求assign和store是连续成对出现,结合上面第2条,那就是assign/store/write一条龙。这条规则要求在工作内存中,每次修改volatile变量后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对该变量所做的修改。
- 对于任意两条龙A和B,如果A的龙头(assign或use)发生在B的龙头之前,那么A的龙尾要在B的龙尾之前(我这个解释真是太牛了!)
关于互斥量(mutex)和信号量(semaphore)的区别:
- 正如他们的名字一样,前者的使用场景是保护资源的独占性,注重的是竞争关系;而后者的场景是等待“信号”的发生,更倾向于生产者/消费者模型中的消费者,注重的是供需关系。[知乎回答](semaphore和mutex的区别? - 人马座的回答 - 知乎 https://www.zhihu.com/question/47704079/answer/528324049)
- 对于mutex,只有上锁的线程才有资格解锁;而对于semaphore,它可以被任意的线程获取和释放。
简述java的synchronized关键字:
- 它是java中最基本的互斥同步的手段,属于一种块结构的同步语法。
- 被javac编译后,会在同步块的前后分别形成moniterenter和moniterexit两个字节码指令
- 这两个字节码都需要一个reference类型参数指明要锁定的和解锁的对象。如果没有指明,那么就根据synchronized修饰的方法类型决定是锁对象实例还是Class对象。
- 在执行monitorenter指令时,首先要去尝试获取对象的锁,并且锁计数器加一,执行 monitorexit指令时会将锁计数器的值减一。计数器清零时,锁被释放。并且可以推论出:对于同一个线程来说,synchronized是可重入的。
简述java的并发机制(或者同步机制)
- 同步的定义:同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些, 当使用信号量的时候)线程使用。
- 基于同步的定义或者目的,有两种实现思路:1、互斥同步,其基本思想是对于共享资源的获取是悲观的(也即悲观锁),因此在访问资源时,都进行加锁操作,例如java语言层面提供的synchronized关键字。2、非阻塞同步,其基本思想是对于共享资源的获取时保持乐观的。不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数 据的确被争用,产生了冲突,那再进行其他的补偿措施。
关于JDK 6引入的自适应自旋锁的机制
- 自旋时间不再固定,而是由同一把锁前一次的自旋时间以及锁拥有者的状态决定。
- 假如刚刚自旋等待获取过同一把锁,并且持有锁的线程正在运行中,虚拟机会认为很可能再次成功,就会增加自旋次数
- 假如很少成功获得过锁,那么可能直接省略掉自旋,避免浪费CPU资源。