【Java多线程与并发】一:Java运行原理分析

写在前面

Hello!好久不见,离开CSDN应该已经有一年的时间了吧,中间因为看到过好的文章想评论一下而登录过几次。也收到了几个评论和几个新的fans,甚是开心。离开CSDN之后转战了github,后来因为种种原因又停止了博客编写,其实最主要的原因还是因为感觉自己没有写出什么有深度的东西,也正是因为这个原因让我又重新回到了这里。接下来的一段时间将继续开启我的学习分享之旅。没有深度慢慢就会有的。
五月份从成都转战北京了,来北京的四个月里,深刻认识到一句话叫比你优秀的人都比你努力。希望自己可以成为那个优秀的人吧。目前就职于帝都某二线互联网银行,有喜欢交朋友或者可以互相内推的可以加VX啦~~博主地地道道的不坑爹的东北人。
VX:479715456
开始正题,感觉原来的博客为了能更通俗易懂的说明一些事情而啰嗦了许多废话,后边尽量避免。

学习多线程与并发之前,先了解Java程序是如何运行的。

1.class文件内容

Java程序的运行是将Java文件编译成.class文件二进制流然后加载到JVM中运行的,了解一下class文件的内容
下图是在项目中随便找的一个.class文件 使用editPlus的16进制查看功能打开查看的内容:
在这里插入图片描述
第一行的CA FE BA BE是一个特殊标记,用来给JVM做验证的,表示这个.class文件是符合虚拟机规范的。其它的内容都是代码中编译之后的内容,其中包括JDK的版本、访问标志(public/private/protected)、常量池、以及类、方法、接口等信息,后续会有具体说明。上边这个格式是给JVM看的,开发人员可以使用其它工具进行查看。

2.JVM运行时数据区

当一个java文件被编译成.class文件之后,就会加载到JVM中,JVM中运行时的数据区有以下几个内容:
在这里插入图片描述
主要分为两大部分线程共享部分和线程独占部分。

  • 线程共享部分: 所有线程都可以访问到这块内存区域,随着虚拟机或者GC而创建销毁。
  • 线程独占部分: 每个线程会有自己的独立空间,独占部分的内容都在每个线程的自己空间内,彼此互不影响,随着线程创建销毁。

接下来了解一下每个区域存储的内容:

  • 方法区: JVM用来存储class文件的信息,class文件中的内容上一小节提到过,这里方法区中还单独开辟了一块运行时常量池,当class文件加载之后,class文件中的常量池中的内容会存在运行时常量池中,它与class文件中的常量池的区别在于JVM并不要求常量一定是编译生成的,也可以时动态加载的,具体在java中的应用就是String.intern()方法。方法区是JVM规划中的逻辑区域,也就是没有明确要求怎么实现,具体实现交给各自版本的虚拟机。
  • 堆内存: 堆内存是JVM中主要存储的数据区,用来存放对象的实例,GC所管理的就是堆内存。堆内存还可以继续细分为新生代、老年代,如果满了就会抛出Out of Memory(OOM)异常。后续深入学习JVM时在详细解读。
  • 虚拟机栈: 用来执行Java方法的区域,每个线程都在这里有自己的独立栈空间,栈由多个栈帧组成,一个线程会执行一个或多个方法,每一个方法对应一个栈帧,每个方法的执行对应的就是栈帧的入栈出栈操作。栈帧中包含局部变量表、操作数栈、动态链接、方法返回地址、附加信息等 ,栈内存的默认大小为1M,超过则会抛出StackOverFlow异常。
  • 本地方法栈: 与虚拟机栈基本相同,区别在于虚拟机栈是用来执行Java方法的区域,而本地方法栈是用来执行Native方法的区域,JVM同样没有规定栈的实现方式,不同版本的虚拟机有各自的实现方式。
  • 程序计数器: 记录当前线程字节码的执行位置,存储字节码指令的地址,如果执行的是Native方法,则存放的是空值。我们都知道程序执行是通过CPU调度的,在线程切换的过程中如何保证还能接着执行,就是通过程序计数器来完成的。

上图下边的执行引擎、本地库接口、本地方法库java开发者不需要关注,java所谓的一处编写到处运行就是通过不同的执行引擎等一系列操作完成的。

3.Java程序运行原理

第一节中查看了16进制的class文件内容,并不能很直观的看到我们所说的版本、类信息等内容,这一节通过一个简单的示例程序查看class文件的内容并分析程序的运行原理。
示例代码:

public class Demo1{
    public static void main(String[] args){
        int x = 500;
        int y = 100;
        int a = x / y;
        int b = 50;
        System.out.println(a + b);
    }
}

在工程路径下找到编译之后的Demo1.class文件,在该文件夹下打开CMD或Powershell窗口,执行如下命令

 javap -v .\Demo1.class  > demo1.txt

通过javap命令解析class文件并将结果导入到demo1.txt中,文件内容如下:

Classfile /Z:/chenxy/Demo1.class
  Last modified 2019-9-3; size 577 bytes
  MD5 checksum a53fdf1ce1d881fc84f37ab86ce2388f
  Compiled from "Demo1.java"
public class Demo1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #27.#28        // java/io/PrintStream.println:(I)V
   #4 = Class              #29            // Demo1
   #5 = Class              #30            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               LDemo1;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               x
  #18 = Utf8               I
  #19 = Utf8               y
  #20 = Utf8               a
  #21 = Utf8               b
  #22 = Utf8               SourceFile
  #23 = Utf8               Demo1.java
  #24 = NameAndType        #6:#7          // "<init>":()V
  #25 = Class              #31            // java/lang/System
  #26 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #27 = Class              #34            // java/io/PrintStream
  #28 = NameAndType        #35:#36        // println:(I)V
  #29 = Utf8               Demo1
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (I)V
{
  public Demo1();
    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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDemo1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: sipush        500
         3: istore_1
         4: bipush        100
         6: istore_2
         7: iload_1
         8: iload_2
         9: idiv
        10: istore_3
        11: bipush        50
        13: istore        4
        15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_3
        19: iload         4
        21: iadd
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: return
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 7
        line 6: 11
        line 7: 15
        line 8: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  args   [Ljava/lang/String;
            4      22     1     x   I
            7      19     2     y   I
           11      15     3     a   I
           15      11     4     b   I
}
SourceFile: "Demo1.java"

我们可以看到版本号和访问控制标志如下:
在这里插入图片描述
52对应jdk1.8
常量池内容如下:
在这里插入图片描述
这里的常量池前边说过是编译时确认的,这里记录了类信息、方法名称、常量值等内容。
构造方法内容如下:
在这里插入图片描述
我们的示例代码中并没有写构造方法,可以看到没有写构造方法的时候,会隐式的提供一个构造方法。
main方法内容如下:
在这里插入图片描述
main方法解析之后前半部分主要描述了访问控制类型、本地变量数量、参数数量等信息。下边从0-25描述的就是方法的执行过程,大概可以看出基本都是push/load/store等指令,对应的就是入栈出栈的操作。class文件中实际存储的是指令码,这里通过javap翻译出来的是操作符,是为了方便我们查看的一种描述形式。具体的指令内容可以查看JVM指令码表。
在这里插入图片描述
可以看到sipush对应的class文件中的指令码为0x11,对应的操作为将int值入栈。
这里简单描述一下上述main方法执行的过程:

  • 首先程序中有5个本地变量,其中第0个对应的是args参数。此时因为args参数为空,所以栈帧中的局部变量表第0个元素存放的就是args。
  • 执行sipush 500,将500压入操作数栈,当前程序计数器计数为0(每一个指令前的数字表示指令对应地址偏移量)
  • 执行istore_1,将500出栈,保存到本地变量表第1个元素中,程序计数器计数3
  • 执行bipush 100,istore_2 与上述相同
  • 执行iload_1,读取本地变量表第一个元素500然后入栈
  • 执行iload_2,第二个元素100入栈
  • 执行idiv,栈中元素做除法,结果入栈
  • …………………………
  • …………………………
  • 执行getstatic #2,获取方法信息 通过查找JVM指令操作表可知getstatic是获取静态类 #2对应的是常量池中的#2 即PrintStream类
  • …………………………
  • 执行invokevirtual #3,执行#3对应的方法,查看常量池可看到#3对应的是println方法

以上就是我们通过解析.class文件获取到的程序运行过程的相关信息。

4.总结

很久没有写文章了,没有什么太好的感觉,可能是因为这一节不是什么特别重点的干货吧,也可能是因为国庆回来太过疲惫。早上五点多爬起来从成都赶高铁来帝都,T^T,后续我会继续努力的!希望大家多多支持鼓励!我写文章的目的不是教学,是想通过记笔记的形式与大家交流,如果有什么问题希望大家不吝赐教。惯例为了后续看起来方便,也对得起总结两个字,概况一下文章中重点的内容吧:java程序的执行过程是类加载器将java文件编译后的.class文件加载到JVM中,由JVM执行,JVM在运行时分为五个数据区,其中有两个线程共享的区域是方法去和堆内存,方法区主要存类信息、常量等内容,堆内存主要存程序创建的类对象,三个线程独占的区域是本地方法栈(执行JAVA方法的区域,每个栈由多个栈帧组成,每个栈帧对应一个方法,栈帧中存有局部变量表、操作数栈、引用信息、返回入口等信息)、虚拟机栈(执行Native方法),程序计数器用来记录当前线程执行到哪个指令了 有关JVM的内容,后续会写专题进行学习。再说个题外话,假期在家除了看了国庆档之外还看了《银河补习班》,看完还是很有感触的,学习不是死记硬背,兴趣是最好的老师,希望现在培养自己独立思考的能力以及兴趣还为时不晚吧。26号还有与技术无关的考试,书还没有看,但愿每天都充实而有收获,共勉。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值