IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> JVM_03 类文件结构与字节码指令篇 -> 正文阅读

[Java知识库]JVM_03 类文件结构与字节码指令篇

类文件结构与字节码指令

、

1、类文件结构

一个简单的 HelloWorld.java 程序:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world!");
    }
}

接下来执行:javac -parameters -d . HelloWorld.java 命令编译.java文件为.class文件:

其中-parameters表示将源文件转换为字节码文件的时候保留参数信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Mow1KBM-1638096094659)(JVM笔记(黑马).assets/image-20211128095810820.png)]

获得二进制字节码文件后怎么读呢?有2种方式:

  • 方式一:JDK自带的反编译工具:javap -verbose XXX.class

根据JVM规范类文件的结构如下:

、

其中u跟数字n,表示占用n个字节

1.1 魔数(u4 magic)

魔数(u4 magic):对应字节码文件的0~3个字节,表示文件的特定类型,不同文件有自己不同的魔数信息,例如java的二进制.class文件的

魔数类型就是如下:

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 091

对于这个cafebabe的由来就不再说了!(每一行的前8位属于编号)

1.2 版本(u2 minor_version、u2 major_version)

版本:对应字节码文件的4~7个字节,表示类的版本 :我们的小版本号在这里不显示,所以前2个字节为00 00

0000000 ca fe ba be00 00 00 3400 23 0a 00 06 00 15 09

这里的十六进制 34H(00 34) 表示十进制的 52,代表的就是JDK8,依次类推:51 就是 JDK 7,53 就是JDK 9。

扩展:2AF5换算成10进制 : 5 * 16^0 + F * 16^1 + A * 16^2 + 2 * 16^3 = 10997

1.3 常量池

实在太麻烦:参考文章

Constant Type 常量类型Value 常量对应的序号(十进制)Value 常量对应的序号(十六进制)
CONSTANT_Class77
CONSTANT_Fieldref99
CONSTANT_Methodref10a
CONSTANT_InterfaceMethodref11b
CONSTANT_String88
CONSTANT_Integer33
CONSTANT_Float44
CONSTANT_Long55
CONSTANT_Double66
CONSTANT_NameAndType12c
CONSTANT_Utf811
CONSTANT_MethodHandle15f
CONSTANT_MethodType1610
CONSTANT_InvokeDynamic1812

案例分析

8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意#0 项不计入,也没有值,注意00表示引用

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • #1项 0a (十六进制) 对应的十进制是10,查找常量池表得知为CONSTANT_Methodref,即方法引用(方法信息),00 06 (6) 和 00

    15(21) 表示它引用了常量池中 #6#21 项来获得这个方法的【所属类】和【方法名】。

    0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • #2项 09 查找常量池表得知,表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和# 23 项来获得这个成员变量的【所属类】和【成员变量名】

    0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

    0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  • #3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中#24 项

    0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  • #4项 0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26
    项来获得这个方法的【所属类】和【方法名】

    0000020 00 16 00 17 08 00 18 0a 00 19 00 1a07 00 1b 07

  • #5项 07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项

    0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  • #6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项

    0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

    0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

  • #7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【< init > 】

    0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

  • #8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【() V】其实就是表示无参、无返回值

    0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

    0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

  • #9项 01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65 是【Code】

    0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

  • #10项 01 表示一个 utf8 串,00 0f(15) 表示长度,4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65
    是【LineNumberTable】

    0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

    0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63

实在写不下去了,需要知道的是常量池一共34项,每项表示各不相同!

1.4 访问标识与继承信息

21 表示该 class 是一个类,公共的类,查下面表中第得到 (0x0001 + 0x0020)ACC_PUBLIC + ACC_SUPER

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 05

表示根据常量池中 #5 找到本类全限定名:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 06

表示根据常量池中 #6 找到父类全限定名:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

表示接口的数量,本类为 0:

0 0000660 29 56 00 21 00 05 00 0600 00 00 00 00 02 00 01

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BLH9aA1-1638096094660)(JVM笔记(黑马).assets/image-20211128105825253.png)]

1.5 Field 信息

表示成员变量数量,本类为 0

0 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sB9RlQxH-1638096094660)(JVM笔记(黑马).assets/image-20211128110115995.png)]

这么分析也太麻烦了,所以Oracle提供了Javap工具,反编译.class文件

2、字节码指令

2.1、javap 工具

Java 中提供了 javap 工具来反编译 class 文件

javap -v D:Demo.class

2.2、图解运行流程

执行的代码

public class Demo3_1 {    
	public static void main(String[] args) {        
		int a = 10;        
		int b = Short.MAX_VALUE + 1;        
		int c = a + b;        
		System.out.println(c);   
    } 
}

常量池也属于方法区,只不过这里单独提出来了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jwmzRp2V-1638096094661)(JVM笔记(黑马).assets/image-20211128112107382.png)]

方法字节码载入方法区

由于:(stack=2,locals=4) 对应操作数栈有 2 个空间(每个空间 4 个字节),局部变量表中有 4 个槽位。

  • stack:最大栈深度,是我们的同时最多能操作多少变量
  • locals:我们一共有多少个变量

栈帧:局部变量表、操作数栈、动态链接、方法出口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yjMWugOw-1638096094662)(JVM笔记(黑马).assets/image-20211128112932861.png)]

执行引擎开始执行字节码

bipush 10:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有

  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
  • ldc 将一个 int 压入操作数栈
  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
  • 这里小的数字都是和字节码指令存在一起,超过 short 范围(-127~128)的数字存入了常量池

istore 1

将操作数栈栈顶元素弹出,放入局部变量表的 slot 1 中

对应代码中的 a = 10,为a变量赋值完成[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOu4Dssy-1638096094662)(JVM笔记(黑马).assets/image-20211128114107700.png)]

执行Idc #3将运行时 常量池中的#3压入操作数栈帧

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kX86ElXn-1638096094663)(JVM笔记(黑马).assets/image-20211128114452230.png)]

istore2将操作数栈中的元素弹出,放到局部变量表的 2 号位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X48WagHc-1638096094663)(JVM笔记(黑马).assets/image-20211128114631273.png)]

iload1 iload2
将局部变量表中 1 号位置和 2 号位置的元素放入操作数栈中。因为只能在操作数栈中执行运算操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gSkytMyR-1638096094664)(JVM笔记(黑马).assets/image-20211128114833803.png)]

iadd
将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lcLa1gHz-1638096094664)(JVM笔记(黑马).assets/image-20211128115023720.png)]

istore_3

将我们操作数栈中的计算好的数据,放到局部变量表3的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WUvZVILI-1638096094667)(JVM笔记(黑马).assets/image-20211128114927966.png)]

getstatic #4

在运行时常量池中找到 #4 ,发现是一个对象,在堆内存中找到该对象,并将其引用放入操作数栈中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jaf0pPOC-1638096094669)(JVM笔记(黑马).assets/image-20211128115816371.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEseKj5u-1638096094671)(JVM笔记(黑马).assets/image-20211128115743388.png)]

iload #3

将我们局部变量中的3号位置的数据放入我们的操作数栈中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlHJxWna-1638096094672)(JVM笔记(黑马).assets/image-20211128120102137.png)]

invokevirtual #5

  • 找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 生成新的栈帧(分配 locals、stack等)
  • 传递参数,执行新栈帧中的字节码

(一个方法一个栈帧)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dE0QNSFK-1638096094674)(JVM笔记(黑马).assets/image-20211128120139722.png)]

  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9qwmhitN-1638096094676)(JVM笔记(黑马).assets/image-20211128120803584.png)]

return

完成 main 方法调用,弹出 main 栈帧,程序结束

2.3、相关练习

练习 a++ 相关问题

/***
 * 从字节码的角度分析 a++ 相关题目
 */
public class ByteCodeTest {
    public static void main(String[] args) {
        int a = 10 ;
        int b = a++ +  ++a  + a-- ;
        System.out.println(a); //11
        System.out.println(b); //34  : 10 + 12 + 12
    }
}

参考:

、

练习

public class ByteCodeTest {
    public static void main(String[] args) {
      	int i = 0 ;
      	int x = 0 ;				//变量表中的两个变量分别为 0 ,0
      	while(i < 10){
          	x = x++ ;		iload 先将x=0取出到操作数栈中,然后对变量表中x的数据++,然后将操作数栈中的数据itore写回给x
          	i++ ;			因此导致无效add
       }
      System.out.println(x); // 输出0
    }
}

2.4、构造方法

cinit() V ,构造方法,无返回值

public class Code_12_CinitTest {
	static int i = 10;				//静态成员变量
		
	static {						//静态代码块
	  i = 20;
	}
    
	static {
	  i = 30;
	}

	public static void main(String[] args) {
		System.out.println(i); // 30
	}
}

编译器会按从上至下的顺序,收集所有 static 静态代码块静态成员赋值的代码,合并为一个特殊的方法 cinit()V :

stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #3                  // Field i:I
         5: bipush        20
         7: putstatic     #3                  // Field i:I
        10: bipush        30
        12: putstatic     #3                  // Field i:I
        15: return

init()V

public class Code_13_InitTest {

    private String a = "s1";    //成员变量

    {
        b = 20;					//代码块
    }

    private int b = 10;

    {
        a = "s2";
    }

    public Code_13_InitTest(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Code_13_InitTest d = new Code_13_InitTest("s3", 30);  //在init()V之后执行
        System.out.println(d.a);
        System.out.println(d.b);
    }
}

编译器会按=从上至下的顺序,收集所有 {} 代码块成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后.

注意:如果是静态代码块和静态成员变量,优先级高于{}代码块和成员变量

2.5、方法的调用

public class Code_14_MethodTest {

    public Code_14_MethodTest() {

    }

    private void test1() {			//私有方法 invokespecial

    }

    private final void test2() {	//final方法 invokespecial

    }

    public void test3() {			//普通成员方法 invokevirtual

    }

    public static void test4() {   //静态方法 invokestatic 

    }

    public static void main(String[] args) {
        Code_14_MethodTest obj = new Code_14_MethodTest();
        obj.test1();
        obj.test2();
        obj.test3();
        obj.test4(); //使用对象调用静态方法
        Code_14_MethodTest.test4();
    }
}

不同方法在调用时,对应的虚拟机指令有所区别

  • 私有、构造、被 final 修饰的方法,在调用时都使用 invokespecial 指令
  • 普通成员方法在调用时,使用 invokevirtual 指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
  • 静态方法在调用时使用 invokestatic 指令
Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  //
         3: dup // 复制一份对象地址压入操作数栈中
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokespecial #4                  // Method test1:()V
        12: aload_1
        13: invokespecial #5                  // Method test2:()V
        15: invokevirtual #6                  // Method test3:()V
        16: aload_1
        17: pop   							
        18: invokestatic  #7 				 //通过对象调用静态方法test4,会浪费2个指令aload1和pop,因此最好不要使用
        20: invokestatic  #8                 // Method test4:()V
        23: return

2.6、多态的原理

因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令

在执行 invokevirtual 指令时,经历了以下几个步骤

  • 先通过栈帧中对象的引用找到对象
  • 分析对象头,找到对象实际的 Class
  • Class 结构中有 vtable
  • 查询 vtable 找到方法的具体地址
  • 执行方法的字节码

2.7、异常处理

try - catch

public class Code_15_TryCatchTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (Exception e) {
            i = 20;
        }
    }
}

对应字节码指令 :

Code:
     stack=1, locals=3, args_size=1
        0: iconst_0				
        1: istore_1				//为i赋值
        2: bipush        10		
        4: istore_1				//将10写入变量表中的i
        5: goto          12		//结束(不抛出异常到此结束,goto直接到12行)
        8: astore_2				//将我们的异常存入异常表
        9: bipush        20		//20放入操作数栈
       11: istore_1				//20写入变量表中的i
       12: return
     //多出来一个异常表
     Exception table:
        from    to  target type  
            2     5     8   Class java/lang/Exception  当我们的代码在[2,5)中出现异常,直接跳转到8行执行
  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号位置(为 e )

多个 single-catch

public class Code_16_MultipleCatchTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (ArithmeticException e) {  //同理muitlcatch(ArithmeticException | Exception e){ }
            i = 20;
        }catch (Exception e) {
            i = 30;
        }
    }
}

对应的字节码

Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          19
        8: astore_2
        9: bipush        20
       11: istore_1
       12: goto          19
       15: astore_2
       16: bipush        30
       18: istore_1
       19: return
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/ArithmeticException  
            2     5    15   Class java/lang/Exception
  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
  • multicatch同理只是Exception table中是一样的,同样是一个槽保存异常信息

finally

public class Code_17_FinallyTest {
    
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {		//为什么finally语句块中的代码一定会执行?我们看一眼对应的字节码文件即可发现!
            i = 30;
        }
    }
}

字节码文件

Code:
     stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
        // try块
        2: bipush        10
        4: istore_1
        // try块执行完后,会执行finally    
        5: bipush        30
        7: istore_1
        8: goto          27
       // catch块     
       11: astore_2 // 异常信息放入局部变量表的2号槽位
       12: bipush        20
       14: istore_1
       // catch块执行完后,会执行finally        
       15: bipush        30
       17: istore_1
       18: goto          27
       // 出现异常,但未被 Exception 捕获,会抛出其他异常,这时也需要执行 finally 块中的代码   
       21: astore_3
       22: bipush        30
       24: istore_1
       25: aload_3
       26: athrow  // 抛出异常
       27: return
     Exception table:
        from    to  target type
            2     5    11   Class java/lang/Exception
            2     5    21   any
           11    15    21   any

可以看到 ?nally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次

finally相关面试题

//我们的结果会是多少呢? 20
public class finallyTest {
    public static void main(String[] args) {
        int result = test() ;
        System.out.println(result);  
    }
    public static int test(){
        try {
            return 10 ;
        }finally {
            return 20 ; 
        }
    }
}

结论:当我们的try语句块中遇到return的时候,会先将return的值保存一下(不会被更改),然后执行finally语句块!

注意:finally块中return导致覆盖了我们的原有的return,所以finally尽量不要用,会吞了我们自己的return

2.8、Synchronized

public class Code_19_SyncTest {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}

对应字节码

Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
         8: aload_1 // 加载到操作数栈
         9: dup // 复制一份,放到操作数栈,用于加锁时消耗
        10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 2 号槽位。这时操作数栈中有一份对象的引用
        11: monitorenter // 加锁
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2 // 加载对象到栈顶
        21: monitorexit // 释放锁
        22: goto          30
        // 异常情况的解决方案 释放锁!
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
        // 异常表!
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
  2     5    21   any
       11    15    21   any

可以看到 ?nally 中的代码被==复制了 3 份==,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

**注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次**



> finally相关面试题

```java
//我们的结果会是多少呢? 20
public class finallyTest {
    public static void main(String[] args) {
        int result = test() ;
        System.out.println(result);  
    }
    public static int test(){
        try {
            return 10 ;
        }finally {
            return 20 ; 
        }
    }
}

结论:当我们的try语句块中遇到return的时候,会先将return的值保存一下(不会被更改),然后执行finally语句块!

注意:finally块中return导致覆盖了我们的原有的return,所以finally尽量不要用,会吞了我们自己的return

2.8、Synchronized

public class Code_19_SyncTest {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}

对应字节码

Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
         8: aload_1 // 加载到操作数栈
         9: dup // 复制一份,放到操作数栈,用于加锁时消耗
        10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 2 号槽位。这时操作数栈中有一份对象的引用
        11: monitorenter // 加锁
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2 // 加载对象到栈顶
        21: monitorexit // 释放锁
        22: goto          30
        // 异常情况的解决方案 释放锁!
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
        // 异常表!
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any

结论 : 无论如何,我们通过synchronized加锁的对象最后一定会释放锁!

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-29 16:10:49  更:2021-11-29 16:11:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:18:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码