Java不只是一种编程语言,还是一个完整的操作平台。Java之所以可以跨平台,这离不开JVM虚拟机。
JVM是一个软件,在不同的平台上,JVM有不同的版本。Java在编译之后会生成一种.class文件,这种文件成为字节码文件。JVM虚拟机就是将Java编译后的.class文件翻译成特定平台下的机器码,然后运行。也就是说,在不同平台上装上平台对应的JVM虚拟机后,就可以将Java字节码文件转换,然后运行我们的Java程序。
值得注意的是,Java编译后的结果是生成字节码,而不是机器码。字节码是不可以直接运行的,必须通过JVM再次翻译成机器码才可以运行。即使是将Java程序打包成可执行文件,也仍然需要JVM的支持才可以运行。
跨平台的是Java程序,而不是JVM。JVM是用C/C++开发的,不能平台,不同的平台下JVM的版本是不同的。
字节码文件,有什么用?
JVM虚拟机的特点:一处编译,多处运行。
多处运行,靠的是.class 字节码文件。
JVM本身,并不是跨平台的。Java之所以跨平台,是因为JVM本身不夸平台。
二进制的文件,显然不是给人看的。是给机器看的。
从根源了解了之后,返回到语言层次 好多都会豁然开朗。
Java语言规范补充:
JVM虚拟机规范(相对底层的)Java,Groovy,kotlin,Scala。 编译后都是Class文件,所以就都能在JVM虚拟机上运行。
一个Java类,然后进行编译成字节码文件
package com.dawa.jvm.bytecode;
public class MyTest1 {
private int a = 1;
public int getA() { return a; }
public void setA(int a) { this.a = a; }
}
javap
编译后的结果:
➜ main javap com.dawa.jvm.bytecode.MyTest1
Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1 {
public com.dawa.jvm.bytecode.MyTest1();
public int getA();
public void setA(int);
}
Java -c
编译后的结果:
➜ main javap -c com.dawa.jvm.bytecode.MyTest1
Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1 {
public com.dawa.jvm.bytecode.MyTest1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
public int getA();
Code:
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
public void setA(int);
Code:
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
}
javap -verbose
编译后的结果
➜ main javap -verbose com.dawa.jvm.bytecode.MyTest1
Classfile /Users/shangyifeng/work/workspace/jvm_leature/build/classes/java/main/com/dawa/jvm/bytecode/MyTest1.class
Last modified 2020-2-14; size 489 bytes
MD5 checksum 952635139a8b5b42f0142d033929d8c2
Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."":()V
#2 = Fieldref #3.#21 // com/dawa/jvm/bytecode/MyTest1.a:I
#3 = Class #22 // com/dawa/jvm/bytecode/MyTest1
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/dawa/jvm/bytecode/MyTest1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest1.java
#20 = NameAndType #7:#8 // "":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/dawa/jvm/bytecode/MyTest1
#23 = Utf8 java/lang/Object
{
public com.dawa.jvm.bytecode.MyTest1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/dawa/jvm/bytecode/MyTest1;
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/dawa/jvm/bytecode/MyTest1;
public void setA(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
LineNumberTable:
line 11: 0
line 12: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/dawa/jvm/bytecode/MyTest1;
0 6 1 a I
}
SourceFile: "MyTest1.java"
上面一张图说明了字节码所有的事情
借助工具:Hex_fiend(mac)
查看16进制的文件
用工具打开的二进制文件:16进制
4字节展示格式
单字节展示格式
使用javap -verbose 命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。
attribute_length表示 attribute所包含的字节数,不包含 attribute_name_index和 attribute_length字段
max_stack表示这个方法运行的任何时刻所能达到的操作数盏的最大深度
max_locals 表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
code_length表示该方法所包含的字节码的字节数以及具体的指令码
具体字节码即是该方法被调用时,虚拟机所执行的字节码
exception_table,这里存放的是处理异常的信息
每个exception_table表项有start_pc,end_pc,handler_pc,catch_type组成
源文件:
java -p 字节码:
加了synchronized之后的字节码文件比较:只多了一个访问修饰符flag
为什么只有一个访问修饰符呢?我们平时说的同步关键字和同步块,功能都是怎么实现的呢?对于moniterenter 和 moniterexit 这两个指令怎么执行的?是如何通过上述指令完成对锁的锁定和释放呢?
monitorexit的作用:退出对象的监视器
synchronized可以用到的地方:实例方法上 或者 静态方法上,或者在方法内修饰锁对象。
在方法名字上,用的是synchronized关键字。如果是在方法名字上,则只是多了一个标记符
在方法里面,用的是synchronized方法,修饰锁对象,如果修饰在对象上,则字节码有其他体现
如果在静态方法上,则代表给当前方法所在的Class类的对象进行上锁
修饰实例方法的时候,synchronize修饰的是this对象。字节码中只在访问修饰符里面体现。(如果用在方法里面,就能够在字节码中看到具体的二进制代码的实现了。如下图所示,常量池中 13 和3 对应的两个关键词。)其中 13 是正常退出的操作。19 是异常退出的操作。确保锁在出异常的时候能够正常退出。
该方法对应的字节码如下
需要了解的:可重入锁,单线程执行
(自己乱写的一些概念。别人可能看不懂。对上述概念的自我翻译。)当第一个线程使用锁对象的时候,状态有0变为1,在访问锁的同事,可以再次访问这个锁对象中的其他synchronize标记方法。当第二个线程尝试去获取这个锁的时候,是进不去的。因为这个对象就进入等待状态。在等待的时候处于类的自旋状态。老版本的synchronize是比较重量级的,所以之前要求能不用就不用,但是现在已经相对优化了。
如果另外一个线程已经拥有了这个线程锁对象的标记,在状态由1变为0之前,另外一个线程会一直处于等待的阻塞状态。直到那边线程释放,这边再次尝试获得拥有权。
字节码如下所示,当然不会像之前那么详细,是对整体有一个认知。
当然,还是要通过结构表来进行查询操作:
JClasslib对应的信息:
70个常量池,70-1 = 69个常量
从第9个字段定义中,赋值的是5.53 调用的是L类型的Integer。完成了自动装箱操作
默认的访问标识符为 0000。 0009 = public static
方法:6个
初始化变量的赋值,是在构造方法
问题1:如果我自己没有提供无参构造,系统会自动生成构造方法,然后在里面进行初始化。如果我们提供一个无参数构造方法呢?
测试结果:结果一样。如下图所示
为什么会这样呢?原来,因为在编译完之后,JVM会对指令,进行重新的排序。所以会是这样的现象。
问题2:如果有两个构造方法呢?那么变量的赋值操作,是在哪里进行执行的呢?
测试结果:有参构造和无参构造的字节码,完全相同,没有任何的变化。
说明:JVM是把所有赋值,都给放在所有的构造方法中了。(并且是先赋值,然后再执行构造方法自定义的代码片段。)字节码面前,了无秘密。
静态变量和静态代码块的内容都是在cinit()方法中执行的
不管有多少个代码块,都只会生成一个cinit()方法
源文件代码:
Copy
package com.dawa.jvm; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; public class MyTest3 { public void test(){ try { InputStream is = new FileInputStream("test.txt"); ServerSocket serverSocket = new ServerSocket(9999); serverSocket.accept(); } catch (FileNotFoundException ex) { } catch (IOException ex) { } catch (Exception ex) { }finally { System.out.println("finally "); } } }
对应的字节码(java -p)
为什么局部变量是4个?
关于异常表的内容:
关于goto,我的想法:我觉得下面博客中的这种说的不对。 他没有深入理解JVM。 因为goto语句在字节码文件里面,异常处理的时候用到了。(网上有很多种说goto没有用的观点)
Java字节码对于异常的处理方式:
异常处理有两种方式。1.catch 2. throws ,上述的是第一种方式。
如果是throws 抛异常,查看异常的字节码。(方法级别的异常)
注意:
两种异常方式,异常不是在同一个级别的
不论是运行时异常,还是其他异常,都是一样的。都能在这里throws
栈帧(stack frame):
栈:先进后出的数据结构
帧:小的单元
栈帧是有一种用于帮助虚拟机执行方法调用与方法执行的数据结构。
栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息
符号引用,直接引用 (由动态链接,扩展出来的概念)
补充知识:动态链接,C语言的动态链接库。符号引用,直接引用的概念。入栈和出栈的过程。
到这里该学学汇编语言了。
SLOT: 32位。 一个int,长度为32,占用1个Slot。Slot是可以复用的。
对于slot可复用的解释,如上图:如b,c所占用的slot, 可能会被后来的d,e 占用。所以所占用的slot数量不能固定计算出来的。是动态的。
符号引用,如何转换为直接引用
直接引用:可以直接拿到方法或者变量的内存地址。在运行期间是拿不到的。
符号引用,是存在java的常量池中的。
方法调用:通过常量池,将符号引用,转换为直接引用
所以,符号引用的转换的两种方式:
静态解析。
有些符号引用是在类加载阶段或者是第一次使用的时候就会转换为直接引用,这些转换叫做静态解析。
动态解析。
另外一些符号引用则是在 每次运行期转换为直接引用,这种转换叫做动态链接。这体现为Java的多态性。
通过伪代码来解释(下图):只有在处于运行期的时候,这些变量才会被动态的识别。这就是多塔ID一种体现,也是多态性的体现。在字节码中所能看到的,字节码的a所对应的都是Animal类的引用。叫做:invokevirtual:动态调用转发。
方法重载与invokevirtual字节码指令的关系**
- invokeinterface: 助记符:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
- invokestatic:调用静态方法
- invokespecial:调用自己的私有方法、构造方法(
)以及父类的方法 - invokevirtual:调用虚方法,运行期动态查找的过程。
- invokedynamic:动态调用方法。JDK1.7引入的
从代码去理解invokestatic 含义
方法重载,是一种静态的行为。
输出结果是什么?
我的猜想:两个都是Grandpa
原因:在编译阶段,并识别不出子类。
解释:涉及到方法的静态分派
运行结果是什么?
我的猜想:apple, orange,orange
运行结果:(bingo)
从字节码角度去分析结果的原因。
new 关键字的的三个作用
- 开辟空间
- 执行构造方法
- 将对应的引用值给返回
为什么字节码汇中:还是调用的Fruit类的test方法?这和看到的输出结果不符合吧?
因为,这是方法的动态分派。
方法的动态分派涉及到一个重要概念:方法接收者。
invokevirtual字节码指令的多态查找流程(方法重写)
换句话说:查找这个方法是谁调用的
找到操作树栈顶的第一个元素所指向的对象的实际类型。(找到实例方法接收者)
如果找到了方法描述符和方法名称都完全相同的方法,且访问权限也校验通过。则直接返回当前实际类型对象的调用。
如果没有找到,则从子类往父类,从下往上,依次查找。找到,则返回。
如果一直没找到,则抛出异常。
比较方法重载(overload)和方法重写(overwrite),我们可以得到这样的结论:
方法重载是静态的,是编译器行为。
方法重写是动态的,是运行期行为。
java方法:虚方法表与动态分派机制
案例:
运行结果是什么?
我的猜测:animal str / animal date (第二个掺杂了蒙的成分。)
运行结果(猜错了):
第二行是Dog的原因:
根据静态分派和动态分派区分。 (如何区分呢?根据调用者的类型吧,如果调用者调用方法,调用的是当前类的重载的方法,那就是静态分派,如果调用者是重写的方法,则认为是动态分派。)
虚表
图中,粉红色的线,代表父类的方法。子类没有对其重写,所以子类的vtable中,直接指导父类的方法索引。从而提升了查找效率,节省了内存空间。前面提到的类加载的连接阶段中将符号引用转换为直接引用,就是这个操作
在初学多态的时候,初学者经常犯这样的错误。
从字节码角度,看看下面这个程序是不行的? 就是 父类不能调用子类的引用对象。
结论:不行, child调不到子类的test3().
分析:因为方法在编译器会在编译阶段就进行静态静态分派。指向的还是父类的类,父类里面没有test3()方法。
现在JVM在执行Java代码的时候,通常都会将解释执行和编译执行二者结合起来进行。
- 所谓的解释执行,就是通过解释器来读取字节码,遇到相应的指令就去执行该指令。
- 所谓的编译执行,是通过及时编译器(Just In Time,JIT)将字节码转换为机器码来执行。
- 现在JVM会根据代码热点(频率)来生成相应的本地机器码。(两者都有使用)
基于栈的指令集与基于寄存器的指令集之前的关系:
- JVM执行指令所采用的方式是基于栈的指令集。
- 基于栈的指令集主要的操作有入栈与出栈两种。
- 基于栈的指令集的优势在于它可以在不同平台之间移植。而基于寄存器的指令集是与硬件架构密切相关的,无法做到可移植。
- 基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区中进行执行的,速度要快的很多。虽然虚拟机可以采取一些优化手段,但是总体来说,基于栈的指令集的执行速度要慢。
如,要完成一个2-1的操作。
- 栈指令集:
- 寄存器,10-1=1.直接执行机器码。
分析一个计算类
这个myCalculate()方法对应的二进制. 有22个指令需要执行。
stack = 2 ;表示这个栈最多容下2个值
locals = 6 ; 表示最大变量数为6
args_size = 1 ; this 参数
对于这22个指令,进行逐行分析
iconst_1 : bipush :表示将1推到操作数栈当中。
istore_1:index of :把索引1的变量,将栈顶弹出来的值赋值到这个索引。
istore 4 : 不是简写了。 局部变量表的索引:4 ,把栈顶的值,放在这个索引对应的变量值的位置里
iload_0:从局部变量中加载一个int类型的值 1, 2, 3,4
iload_1和iload2执行完之后,左边的栈就有值了,为 2 、 1
iadd: Add int,从操作数栈中弹出两个。 value1+ value2 = value。 然后把value再压回栈顶。
此时这个指令执行完之后,是这样的:
isub: 相减 (同 iadd)
imul:相✖️乘 (同iadd)
全部执行完之后: 就算完了。
结论:JVM是基于操作数栈的实现
Spring的动态代理
动态代理接口
接口的实现类
动态代理对象的实现 (实现了InvocationHandler)
客户端实现类
执行结果:
结论:对Subject的操作,全部由动态代理对象来代理操作。
如果打印subject对象的类,是 com.sun.proxy.$Proxy0,在程序运行期动态生成出来的,那么Proxy的父类为:java.lang.reflect.Proxy
那么:com.sun.proxy.$Proxy0 。动态生成这个类的字节码长什么样?
生成代理类的源码:
跟入:generateProxyClass()方法,是由JVM实现的。 再跟入这个方法
通过设置属性值sun.misc.ProxyGenerator.saveGeneratedFiles
将值保存在磁盘上左边目录里面就已经有了
generateClassFile()生成动态代理类的二进制文件的字节流。
谈谈你对动态代理的理解?(important)
我现在的理解:无非就是在不知道子类是什么的时候,在运行期间,就能够动态的生成Class文件。这就是动态代理。
对字节码有了整体的认知。
关于method里面是否可以不包含任何方法?甚至是init。 是可以的。之前说的关于java的构造方法是必须会有的,那是站在java层次和源码层次去考虑的。字节码规范里面并没有规范必须要有方法。java语言规范和JVM规范并不一是一个东西,不要混为一谈。
Class字节码中有两种数据类型
字节码指令
异常表
动态代码的原理