生成字节码
*.java –> *.jasm –> *.class
字节码结构
常量池
方法描述
字节码工具
javac
将java文件编译为classjavap
将class反编译为jasmasmtools
将jasm编译为class;将class反编译为jasmcfr http://www.benf.org/other/cfr/
将class反编译为java
jvm如何加载字节码
加载 –> 链接 –> 初始化
类加载器
双亲委派验证、准备、解析
绕过java语言规范
绑定变量及方法表类的初始化
单例
jvm如何进行方法调用
一个类的多个方法(不管重载还是重写),生成不同的方法描述
Demo.java
1 | public class Demo { |
jvm靠字节码中的方法描述来确定方法(不同的方法名 或 相同方法名但不同参数 都生成了不同的方法描述)
1 | lixl@DESKTOP-0SMOHUS MINGW64 /f/test |
调用的五个指令
java
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-- Customer.java
interface Customer {
boolean isVIP();
}
-- Merchant.java
class Merchant {
public Number actionPrice(double price, Customer customer) {
invokeStaticMethod1();// invokestatic
invokePrivateMethod1();// invokespecial
invokePublicMethod1();// invokevirtual
boolean flag = customer.isVIP();// invokeinterface
return null;
}
private static void invokeStaticMethod1(){
}
private void invokePrivateMethod1(){
}
public void invokePublicMethod1(){
}
}jasm
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83$ javap -v Merchant.class
Classfile /F:/test/Merchant.class
Last modified 2023-11-6; size 565 bytes
MD5 checksum 84ac0cd5304df3835e102360b8325099
Compiled from "Merchant.java"
class Merchant
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #7.#19 // java/lang/Object."<init>":()V
#2 = Methodref #6.#20 // Merchant.invokeStaticMethod1:()V
#3 = Methodref #6.#21 // Merchant.invokePrivateMethod1:()V
#4 = Methodref #6.#22 // Merchant.invokePublicMethod1:()V
#5 = InterfaceMethodref #23.#24 // Customer.isVIP:()Z
#6 = Class #25 // Merchant
#7 = Class #26 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 actionPrice
#13 = Utf8 (DLCustomer;)Ljava/lang/Number;
#14 = Utf8 invokeStaticMethod1
#15 = Utf8 invokePrivateMethod1
#16 = Utf8 invokePublicMethod1
#17 = Utf8 SourceFile
#18 = Utf8 Merchant.java
#19 = NameAndType #8:#9 // "<init>":()V
#20 = NameAndType #14:#9 // invokeStaticMethod1:()V
#21 = NameAndType #15:#9 // invokePrivateMethod1:()V
#22 = NameAndType #16:#9 // invokePublicMethod1:()V
#23 = Class #27 // Customer
#24 = NameAndType #28:#29 // isVIP:()Z
#25 = Utf8 Merchant
#26 = Utf8 java/lang/Object
#27 = Utf8 Customer
#28 = Utf8 isVIP
#29 = Utf8 ()Z
{
Merchant();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public java.lang.Number actionPrice(double, Customer);
descriptor: (DLCustomer;)Ljava/lang/Number;
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=3
0: invokestatic #2 // Method invokeStaticMethod1:()V
3: aload_0
4: invokespecial #3 // Method invokePrivateMethod1:()V
7: aload_0
8: invokevirtual #4 // Method invokePublicMethod1:()V
11: aload_3
12: invokeinterface #5, 1 // InterfaceMethod Customer.isVIP:()Z
17: istore 4
19: aconst_null
20: areturn
LineNumberTable:
line 3: 0
line 4: 3
line 5: 7
line 6: 11
line 8: 19
public void invokePublicMethod1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 21: 0
}
SourceFile: "Merchant.java"jasm 方法调用指令
invokestatic 用于调用静态方法。
invokespecial 用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
invokevirtual 用于调用非私有实例方法。
invokeinterface 用于调用接口方法。
invokedynamic 用于调用动态方法。
1 | 可以被子类覆盖的方法。 |
jvm方法表
类加载机制的链接部分中,类加载的准备阶段,它除了为静态字段分配内存之外,还会构造与该类相关联的方法表。
方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。
这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。
方法表满足两个特质:
其一,子类方法表中包含父类方法表中的所有方法;
其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。
动态绑定与静态绑定
方法调用指令中的符号引用会在执行之前解析成实际引用。
对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。
对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)。
在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。内联缓存
是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。
在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。
如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。
JVM是如何处理异常的
异常处理的两大组成要素是抛出异常和捕获异常
抛出异常可分为显式和隐式两种。
显式抛异常的主体是应用程序,它指的是在程序中使用“throw”关键字,手动将异常实例抛出。
隐式抛异常的主体则是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。
举例来说,Java 虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException)
在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例;
Throwable –> Error Exception
Exception –> RuntimeException
- jvm如何捕获异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public static void main(String[] args) {
try {
mayThrowException();
} catch (Exception e) {
e.printStackTrace();
}
}
// 对应的Java字节码
public static void main(java.lang.String[]);
Code:
0: invokestatic mayThrowException:()V
3: goto 11
6: astore_1
7: aload_1
8: invokevirtual java.lang.Exception.printStackTrace
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception // 异常表条目
编译过后,该方法的异常表拥有一个条目。
其 from 指针和 to 指针分别为 0 和 3,代表它的监控范围从索引为 0 的字节码开始,到索引为 3 的字节码结束(不包括 3)。
该条目的 target 指针是 6,代表这个异常处理器从索引为 6 的字节码开始。
条目的最后一列,代表该异常处理器所捕获的异常类型正是 Exception。
- 带finally的异常
Demo.java
1 | public class Demo { |
jasm
1 | $ javap -v Demo.class |
可以看到,编译结果包含三份 finally 代码块。
其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。
最后一份则作为异常处理器,监控 try 代码块以及 catch 代码块。
它将捕获 try 代码块触发的、未被 catch 代码块捕获的异常,以及 catch 代码块触发的异常。
等效于
1 | public class Demo { |
JVM是如何实现反射的
实现方式
查阅 Method.invoke 的源代码,那么你会发现,它实际上委派给 MethodAccessor 来处理。
MethodAccessor 是一个接口,它有两个已有的具体实现:
一个通过本地方法来实现反射调用,另一个则使用了委派模式。
本地实现
类加载到虚拟机之后,其方法都加载到方法区,可以明确的指向一个内存地址。
反射调用无非就是将传入的参数准备好,然后调用进入目标方法。动态实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// v0版本
import java.lang.reflect.Method;
public class Test {
public static void target(int i) {
new Exception("#" + i).printStackTrace();
}
public static void main(String[] args) throws Exception {
Class<?> klass = Class.forName("Test");
Method method = klass.getMethod("target", int.class);
method.invoke(null, 0);
}
}
# 不同版本的输出略有不同,这里我使用了Java 10。
$ java Test
java.lang.Exception: #0
at Test.target(Test.java:5)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
a t java.base/jdk.internal.reflect.NativeMethodAccessorImpl. .invoke(NativeMethodAccessorImpl.java:62)
t java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.i .invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
t Test.main(Test.java:131
反射调用先是调用了 Method.invoke,然后进入委派实现(DelegatingMethodAccessorImpl),
再然后进入本地实现(NativeMethodAccessorImpl),最后到达目标方法。
这里你可能会疑问,为什么反射调用还要采取委派实现作为中间层?直接交给本地实现不可以么?
其实,Java 的反射调用机制还设立了另一种动态生成字节码的实现(下称动态实现),
直接使用 invoke 指令来调用目标方法。之所以采用委派实现,便是为了能够在本地实现以及动态实现中切换。
动态实现和本地实现相比,其运行效率要快上 20 倍 。
这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换,
但由于生成字节码十分耗时,仅调用一次的话,反而是本地实现要快上 3 到 4 倍 。
开销大的原因
在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。
在调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。
这个动态实现的字节码是自动生成的,它将直接使用 invoke 指令来调用目标方法。
方法的反射调用会带来不少性能开销,原因主要有三个:
变长参数方法导致的 Object 数组,
基本类型的自动装箱、拆箱,
还有最重要的方法内联。
java对象的内存布局
- 对象头
每个对象都有一个对象头,对象头包括两部分,标记信息和类型指针。
标记信息包括哈希值,锁信息,GC信息。类型指针指向这个对象的class。
两个信息分别占用8个字节,所以每个对象的额外内存为16个字节。很消耗内存。
- 压缩指针
为了减少类型指针的内存占用,将64位指针压缩至32位,进而节约内存。之前64位寻址,寻的是字节。现在32位寻址,寻的是变量。再加上内存对齐(补齐为8的倍数),可以每次寻变量都以一定的规则寻找,并且一定可以找得到。
- 内存对齐
内存对齐的另一个好处是,使得CPU缓存行可以更好的实施。保证每个变量都只出现在一条缓存行中,不会出现跨行缓存。提高程序的执行效率。
- 字段重排序
其实就是更好的执行内存对齐标准,会调整字段在内存中的分布,达到方便寻址和节省空间的目的。
- 虚共享
当两个线程分别访问一个对象中的不同volatile字段,理论上是不涉及变量共享和同步要求的。但是如果两个volatile字段处于同一个CPU缓存行中,对其中一个volatile字段的写操作,会导致整个缓存行的写回和读取操作,进而影响到了另一个volatile变量,也就是实际上的共享问题。
- @Contented注解
该注解就是用来解决虚共享问题的,被该注解标识的变量,会独占一个CPU缓存行。但也因此浪费了大量的内存空间。
垃圾回收
引用计数法与可达性分析
引用计数法:每个对象都有一个引用计数器,被引用一次+1;等于0就都是垃圾。
会有循环引用的问题
可达性分析:从GC-root开始标记被引用的对象,没被标记的就被作为垃圾。
误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。Stop-the-world 以及安全点
在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,
那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。
这也就造成了垃圾回收所谓的暂停时间(GC pause)。
Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。
当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,
才允许请求 Stop-the-world 的线程进行独占的工作。
垃圾回收的三种方式
清除:对象新建时记录分配地址,不用的对象加入空闲列表,只清理空闲列表内的对象即可;
会有内存碎片,导致后续大的对象无法被创建。
压缩:把存储的对象压缩放置到连续空间;
但压缩算法本身比较复杂。
复制:把内存分成两份,一半用来创建对象,当这一半内存快满时复制到另一半内存;
比较浪费内存分代模型(新生代(Eden/Surviver)和老年代 )
新生代:新生对象,生存时间短;复制-清理,执行速度快,stw时间短;8:1:1
老年代:对象生成时间长;标记清理问题:
1、TLAB(Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)
解决多个线程同时分配段内存的情况;预先分配,免得冲突
2.Eden/Surviver比例可以调整;Surviver复制次数及Eden占用比例可以调整;
3.卡表(Card Table):为了解决老年代回收垃圾(Minor GC)是需要进行全堆扫描的情况,
Hotspot将整个堆分为一个个512字节大小的卡,卡表内记录每张卡的使用状态。
写屏障(write barrier)、虚共享(false sharing)jvm中的垃圾回收器
针对新生代的垃圾回收器共有三个:Serial,Parallel Scavenge 和 Parallel New (标记 - 复制算法)
针对老年代的垃圾回收器也有三个:刚刚提到的 Serial Old 和 Parallel Old(标记 - 压缩算法),
以及 CMS(标记 - 清除算法,并发)。
G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。
Java 11 引入了 ZGC https://www.zhihu.com/question/287945354/answer/458761494