从java到class追踪程序的执行

java/android虚拟机


android开发使用语言便是java,而android虚拟机(Dalvik,art)和普通的hospot等java虚拟机很多东西也是相似的(android虚拟机命令是基于寄存器的),因此可以对比来看(dalvik命令基于栈,看起来会比较紧凑一些)


java或者说android虚拟机可以暂且当做是一个普通的进程,与一般的线程相对应,只不过其中会包含其他一般进程没有的部分(class解释器;JIT:Just In Time,即时编译等)


java和虚拟机并不是绑定的关系,java规范和虚拟机规范是分开的,因此class文件并不只能由java文件来生成,事实上资深工作者可以直接编辑class文件来实现java语法不予许的功能

Dalvik VM并不是一个Java虚拟机,它没有遵循Java虚拟机规范,不能直接执行Java的Class文件,使用的是寄存器架构而不是JVM中常见的栈架构。 —— [ 深入理解Java虚拟机 ][ 周志明 ]

即便如此,也可以通过java虚拟机来查看一般的android代码执行情况,因为dalvik执行的dex其实是class文件转化而来,也是使用java语法编写的应用程序.

一、Java,jdk

1、java说明

java文件是以java语法为依据编译的文件,因为java中对安全限制较高,因此舍去了很多c++中指针等危险操作,对于访问溢出等,在生成底层代码时,会自动加上访问检测等操作,对于内存释放等,也会自动处理(自动添加越界检测等操作),因此java编写很方便;不过在最后执行底层代码时,也会因为这些操作导致运行效率偏低.

2、jdk版本与功能

java编写的依据便是jdk库,同时在jdk有升级时,class文件规范,虚拟机自身也可能会有相应的更改,不同版本jdk可使用功能如下:

  1. 1995年5月23日;版本1.0;解释执行,Java虚拟机,Applet,AWT
  2. 1996年1月23日;版本1.1;JDK 1.1版的技术代表有:JAR文件格式、JDBC、JavaBeans、RMI。Java语法也有了一定的发展,如内部类(Inner Class)和反射(Reflection)都是在这个时候出现的
  3. 1998年12月4日;JDK 1.2;java分为SE,ME,EE;EJB,Java Plug-in,Java IDL,Swing;JIT(Just In Time),java中strictfp关键字,Collections集合等
  4. 2000年5月8日;JDK 1.3;2000年5月8日;添加数字计算,Timer API,JNDI平台级服务等
  5. 2002年2月13日;JDK 1.4;正则表达式,异常链,NIO,日志类,XML解释器和XSLT转换器等
  6. 2004年9月30日;JDK 1.5;自动装箱、泛型、动态注解、枚举、可变长参数、遍历循环(foreach循环)等语法特性都是在JDK 1.5中加入的。在虚拟机和API层面上,这个版本改进了Java的内存模型(Java Memory Model,JMM)、提供了java.util.concurrent并发包等
  7. 2006年12月11日;JDK 1.6;提供动态语言支持(通过内置Mozilla JavaScript Rhino引擎实现)、提供编译API和微型HTTP服务器API等。同时,这个版本对Java虚拟机内部做了大量改进,包括锁与同步、垃圾收集、类加载等方面的算法都有相当多的改动。
  8. 2009年2月19日;JDK 1.7;JDK 1.7的主要改进包括:提供新的G1收集器(G1在发布时依然处于Experimental状态,直至2012年4月的Update 4中才正式“转正”)、加强对非Java语言的调用支持(JSR-292,这项特性到目前为止依然没有完全实现定型)、升级类加载架构等。
  9. 2014年3月19日;JDK 1.8;Lambda表达式,Optional,Stream等

可以看到,随着jdk版本提升,功能也是越来越强

二、了解java到class过程

编写一个简单的java文件,用于查看从java文件到最终字节码的执行过程:

1、java文件

BaseEvent.java文件:

package com.knowledge.mnlin.frame.base;

/**
 * 功能----RxBus传递对象,用于基本的显示toast等信息的传递
 * <p>
 * Created by MNLIN on 2017/9/23.
 */

public class BaseEvent {
    //operateCode 类型,EventBus传递过来时,判断对应的类型
    public int operateCode;

    public Object data;

    public BaseEvent(int operateCode,Object data){
        this.operateCode=operateCode;
        this.data=data;
    }
}

代码很简单,只有构造方法,以及两个成员变量.

2、class文件(经反编译后显示的内容)

BaseEvent.class文件:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.knowledge.mnlin.frame.base;

public class BaseEvent {
    public int operateCode;
    public Object data;

    public BaseEvent(int operateCode, Object data) {
        this.operateCode = operateCode;
        this.data = data;
    }
}

看起来经过编译后,内容逻辑并没有什么变化,事实上,生成class文件时除了一些必要的处理(比如一些if不可能执行的代码会被丢弃,case string会进行变化等),其他都不会有太大变化.可以看一些一些简单的代码的转换:

①、if语句省略

一些if语句不可能到达的位置,代码会被自动忽略,是不会进入class文件中的:

java文件:

if(true){
    Log.d("tag","true");
}else{
    Log.d("tag","true");
}

class文件:

Log.d("tag", "true");

可以看到,if语句中false部分因为不可能执行,所以在编译时就已经被去掉了,只剩下了恒为true的代码;

②、case语句

用于测试java文件case语句:

String str="a";
switch (str){
    case "a":{
        break;
    }
    case "b":{
        break;
    }
}

在生成class文件后反编译,可以看到具体的逻辑:

String str = "a";
byte var5 = -1;
switch(str.hashCode()) {
case 97:
    if(str.equals("a")) {
        var5 = 0;
    }
    break;
case 98:
    if(str.equals("b")) {
        var5 = 1;
    }
}

switch(var5) {
case 0:
case 1:
default:
}

在java7之前,case也是不能使用string来判断的;事实上语法自身也是不支持string来判断的(实际是通过哈希码和equal方法来判断的),这里相当于java的一个语法糖,将多余的操作交给编辑器来完成,达到了简化代码的效果。

③、其他部分

除此之外,还有其他的一些内容,例如自动拆装箱泛型擦除增强for循环变长参数枚举类型等;

  • 自动拆装箱:基本类型与对象之间编辑转换,如Integer与int等
  • 泛型擦除:如List<String>与List<Integer>,在java中,被认为是同一个类(事实上,在生成class时,已经记录了泛型类型,因此java中通过反射才可以获取的到泛型参数)
  • 增强for循环:增强for循环自身是通过iterator接口来实现的,因此必须实现了iterator接口的对象才可以使用增强for
  • 变长参数:在java1.5时,添加了变长参数功能,但实际上,只是一个数组而已。
  • 枚举类型:枚举类型在实际使用时,只是Enum类的子类,因此与一般类并没有太大区别(java编译后会自身生成[class name]$[enum name]类)

除此之外,还有常见的数组类型,其实是在虚拟机内部自动生成了类。当然class文件是看不到这部分内容的。

内部类的话,则分为两种:

I、一般内部类

一般的内部类其实逻辑时分成两步:

  1. 创建对象;在java -> class时,会生成一个 [class name]$[\d]类,然后调用new创建对象;
  2. 使用生成的对象参与方法调用

java源码:

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.e("tag","inner class");
    }
});

生成了额外的class文件:[class name]$[\d]

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.acchain.community.base;

import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;

class BaseEvent$1 implements OnClickListener {
    BaseEvent$1(BaseEvent this$0) {
        this.this$0 = this$0;
    }

    public void onClick(View v) {
        Log.e("tag", "inner class");
    }
}

可以看到,其实是编译器自动生成了一个其他的class类。

II、lambda表示

java8以后,jdk支持lambda表达式,对于上面那段代码,如果使用lambda简写的的话,是如下形式:

view.setOnClickListener(v -> Log.e("tag","lambda"));

这种写法非常方便,但如果查看编译后的class文件的话,可以发现并没有额外的class文件生成,这是就需要查看未反编译的class文件之间的差别

III、两种方式对比

一般形式内部类class文件对应字节码(截取其中一部分):

151: aload         5
153: new           #15                 // class com/acchain/community/base/BaseEvent$1
156: dup
157: aload_0
158: invokespecial #16                 // Method com/acchain/community/base/BaseEvent$1."<init>":(Lcom/acchain/community/base/BaseEvent;)V
161: invokevirtual #17                 // Method android/view/View.setOnClickListener:(Landroid/view/View$OnClickListen

lambda形式class文件对应字节码(截取一部分):

166: invokedynamic #18,  0             // InvokeDynamic #0:onClick:()Landroid/view/View$OnClickListener;
171: invokevirtual #17                 // Method android/view/View.setOnClickListener:(Landroid/view/View$OnClickListen

两者之间,字节码是不一样的,invokedynamic指令是在java1.7版本之后新加的内容,主要用于动态调用,具体功能可以参考书籍:《深入理解Java虚拟机》

好了,扯远了,接下来还是以BaseEvent类来说明生成的字节码

3、class文件(字节形式解析)

现在对BaseEvent.class文件,不进行反编译,直接查看字节部分,这部分内容需要充分理解class文件的组成结构,现在直接贴上解析部分:

//根据java虚拟机规定,class文件格式采用了类似c语言结构体的伪结构,只包含两种数据类型:无符号数,表
//无符号数为基本数据类型,以u1,u2,u4,u8来分别表示1,2,4,8个字节,无符号数用来描述数字,索引,引用,数量,或暗中啊utf-8编码的字符串值
//表 时有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表习惯性的以  _info  结尾;因此整个class文件可以看作一个 表(有时看来就像view布局,里面由view和group两种构成,多个view放到一起看作是一个group)

//前四字节为魔数值,表示是否为可以被虚拟机接受的class文件,ascii码含义为:J~>:
cafe babe  

//第五第六字节为次版本号(Minor Version),这里表示为0x0000(class文件默认格式为大端存储,高字节在低位,低字节在高位)
0000 

//七八字节为主版本号(Major Version),这里表示0x0034,即52,java版本号从45开始,jdk1.0对应45,jdk8.0对应52.0;jdk每个大版本更新都加1
0034 

//常量池容量(偏移地址:0x00000008)为十六进制数0x0019,为25,常量池坐标从1开始,即索引1-24(0不不使用)共24个常量
//常量池存放两大类常量:字面量Literal,符号引用Symbolic References
//字面量包括:文本字符串,声明为final的常量值等
//符号引用包括:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符
0019 

//常量池容量已经确定,一次接下来会有24个常量(常量类型一共有14种,因此这24个常量类型不同的字节码类不定)

//每一个常量为一个表结构,每种常量类型表开始第一位为u1,也就是一个字节,标志位;这里为0x0a,即10,表示为CONSTANT-Methodref_info:类中方法的符号引用;接下来查询该表类型结构字段
/*
type  |  descriptor  |  remark
u1    |  tag         |  CONSTANT_Methodref(10)
u2    |  class_index |  constant_pool中的索引,CONSTANT_Class_info类型。记录定义该方法的类。
u2    |  name_and_type_index  |  constant_pool中的索引,CONSTANT_NameAndType_info类型。指定类中扽方法名(name)和方法描述符(descriptor)。
*/
//因此接下来的1+2+2个字节都为该类型表的数据
0a
//class_index字段为0x0005,索引5,指向另一种表类型:CONSTANT_Class_info
0005
//name_and_type_index字段为0x0013,索引19,指向另一种表类型:CONSTANT_NameAndType_info
0013

//按照常量池格式,可以依据解析剩余23个表结构
09  
0004 0014 0900 0400 1507 0016 0700 1701
000b 6f70 6572 6174 6543 6f64 6501 0001
4901 0004 6461 7461 0100 124c 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 3b01 0006
3c69 6e69 743e 0100 1628 494c 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 3b29 5601
0004 436f 6465 0100 0f4c 696e 654e 756d
6265 7254 6162 6c65 0100 124c 6f63 616c
5661 7269 6162 6c65 5461 626c 6501 0004
7468 6973 0100 2a4c 636f 6d2f 6b6e 6f77
6c65 6467 652f 6d6e 6c69 6e2f 6672 616d
652f 6261 7365 2f42 6173 6545 7665 6e74
3b01 000a 536f 7572 6365 4669 6c65 0100
0e42 6173 6545 7665 6e74 2e6a 6176 610c
000a 0018 0c00 0600 070c 0008 0009 0100
2863 6f6d 2f6b 6e6f 776c 6564 6765 2f6d
6e6c 696e 2f66 7261 6d65 2f62 6173 652f
4261 7365 4576 656e 7401 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0100 0328
2956


//常量池结束后,紧跟两个字节访问标志,0x0021表示为public类型的类,并且属于jdk1.0.2以后编译的字节码;注:jdk8时候,16个bit位都已经用完
0021

//this class字段,本类索引,指向常量池的第4个常量(表结构)

//super class字段,父类索引,指向常量池第5个常量(表结构)
0005 

//接口个数为0,表示没有实现任何接口
0000 

//若有实现的接口,则接下来应该是 2*接口个数 个字节的接口索引结合索引,因为每个接口索引为 u2 类型


//然后是u2结构的字段,表示fields_count,这里为0x0002表示有两个field(类成员),因此该u2后会有两个field_info表类型
0002 

//field_info表类型包含三个u2基本类型数据:access_flags,name_index,descriptor_index;
//第一个field_info,operateCode
//access_flags为0x0001,表示为public访问权限
0001 
//name_index索引,引用常量池6,表示该字段名字
0006
//descriptor_index索引,表示该字段的类型描述,指向常量7(常量池中常量7是个utf-8的字符串,为I,表示int类型);
0007
//在最后还有一个属性表集合计数器用于存储额外的信息(暂不考虑)
0000 

//第二个field_info,data
0001 0008 0009 0000

//方法表和成员表结构雷同
//有一个方法
0001 
//access_flags,public访问权限
0001
//name_index,常量池中10
000a 
//descriptor_index索引,方法描述
000b 
//属性表集合计数器,此时不为0(上面的field_info中最后属性表集合计数器是0,因此不做考虑);表示此方法的属性表集合有一项属性
0001 

//attribute_name_index,属性值名称索引;该字段表明属性表集合的那项属性指向常量池0x000c,也就是12索引,(12索引为utf-8字符串:"Code",Code时虚拟机规范预定义的属性,表示java代码编译成的字节码指令);
000c 
//attribute_length;属性值长度89
0000 0059
//max_stack;操作数栈深度最大值;
0002 
//max_locals;代表了局部变量表需要的存储空间(slot为虚拟机为局部变量分配内存最小的使用单位);
0003
//code_length;java源程序编译层的字节码指令(的长度);
0000 000f 
//code;字节码
2a //aload_0,将第1个reference(引用)类型的变量推送到栈顶(this值)
b7 0001 //invokespecial,将栈顶reference类型数据指向对象作为方法接收者,调用此对象的构造器方法,dprivate方法等;具体调用的方法所对应常量池的地址为:0x0001
2a
1b 
b5 0002 //赋值操作2
2a 
2c
b5 0003 //赋值操作,索引:常量池3
b1 //return;表示执行结束
//exception_table_length;
0000
//exception_table;exception_table_length为0,因此这里没有异常信息 

    //有两个Code的从属 属性
    0002

    //attribute_name_index;索引,指向字符串"LineNumberTable"
    000d
    //attributes_length;属性值共18个字节
    0000 0012
    //line_number_table_length;一共4行
    0004
    //接下来为四行的line对照表
    //第一行:字节码的第0x0000行对应源码的0x000f行
    0000 000f
    //第二到四行
    0004 0010
    0009 0011 
    000e 0012

    //同LineNumberTable属性结构一样,接下来是LocalVariableTable属性结构
    //attribute_name_index;索引,指向字符串"LocalVariableTable"
    000e
    //attributes_length;
    0000 0020
    //local_variable_table_length
    0003
    //3个local_variable_info结构
    //第一个local_variable_info
    //start_pc,length,name_index,descriptor_index,index这五个字段定义了一个局部变量的声明周期,类型,名称等信息
    0000 000f 000f 0010 0000  
    //后两个local_variable_info
    0000 000f 0006 0007 0001 
    0000 000f 0008 0009 0002 



//从此处开始,就不再是<init>方法的属性了,而是整个类的属性介绍;
//整个文件的属性值个数
0001 

//这个文件只有以下一个属性
//sourcefile属性:attribute_name_index(u2),attribut_length(u4),sourcefile_index(u2)
//指向常量池17:"SourceFile"
0011 
//长度,2
0000 0002 
//文件的名字索引,指向常量池中18:"BaseEvent.java"
0012

这部分如果依靠人为解析,其实会很麻烦,这里盗用别人的图,用来表示class文件的结构:

这里写图片描述

class文件格式采用类似于c语言结构体的伪结构来存储数据,只有两种基本结构:无符号数,表
其中u1,u2,u4,u8这些表示占用1、2、4、8个字节的无符号数,其他类型就是表结构;
具体的表结构可以参考百度(类型太多了);

4、class文件(javap解析过的显示内容)

这里通过javap可以看到方法具体的字节码内容,通过javap生成基于栈的字节码指令,因为这种指令比较紧凑,也方便查看,javap查看的内容如下:

Classfile /D:/personal/github/FrameKnowledge/app/build/intermediates/classes/debug/com/knowledge/mnlin/frame/base/BaseEvent.class
  Last modified Dec 19, 2017; size 463 bytes
  MD5 checksum 25f902a85e0f04b251a24e7004d82012
  Compiled from "BaseEvent.java"
public class com.knowledge.mnlin.frame.base.BaseEvent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#20         // com/knowledge/mnlin/frame/base/BaseEvent.operateCode:I
   #3 = Fieldref           #4.#21         // com/knowledge/mnlin/frame/base/BaseEvent.data:Ljava/lang/Object;
   #4 = Class              #22            // com/knowledge/mnlin/frame/base/BaseEvent
   #5 = Class              #23            // java/lang/Object
   #6 = Utf8               operateCode
   #7 = Utf8               I
   #8 = Utf8               data
   #9 = Utf8               Ljava/lang/Object;
  #10 = Utf8               <init>
  #11 = Utf8               (ILjava/lang/Object;)V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/knowledge/mnlin/frame/base/BaseEvent;
  #17 = Utf8               SourceFile
  #18 = Utf8               BaseEvent.java
  #19 = NameAndType        #10:#24        // "<init>":()V
  #20 = NameAndType        #6:#7          // operateCode:I
  #21 = NameAndType        #8:#9          // data:Ljava/lang/Object;
  #22 = Utf8               com/knowledge/mnlin/frame/base/BaseEvent
  #23 = Utf8               java/lang/Object
  #24 = Utf8               ()V
{
  public int operateCode;
    descriptor: I
    flags: ACC_PUBLIC

  public java.lang.Object data;
    descriptor: Ljava/lang/Object;
    flags: ACC_PUBLIC

  public com.knowledge.mnlin.frame.base.BaseEvent(int, java.lang.Object);
    descriptor: (ILjava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield      #2                  // Field operateCode:I
         9: aload_0
        10: aload_2
        11: putfield      #3                  // Field data:Ljava/lang/Object;
        14: return
      LineNumberTable:
        line 15: 0
        line 16: 4
        line 17: 9
        line 18: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/knowledge/mnlin/frame/base/BaseEvent;
            0      15     1 operateCode   I
            0      15     2  data   Ljava/lang/Object;
}
SourceFile: "BaseEvent.java"
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 -Duser.language=en -Duser.country=US

参考class文件结构可以很明显的对比各个部分。
这里只看构造函数中内容:

  1. 开始部分标明了方法访问属性,方法签名(参数与返回值类型);
  2. 然后表明了该方法执行需要栈的深度为2,局部变量表容量为3,参数个数为3
  3. 然后是方法中代码对应的字节码
  4. 接着是指令与源码中行号的对应关系,这也是java在异常时可以抛出代码行数的原因
  5. 最后就是局部变量表的内容,可以看到参数容量为3,类型和对应的值也标注的很清楚

最核心的地方便是方法中代码执行的逻辑部分,源码很简单,只是简单的赋值操作:

this.operateCode=operateCode;
this.data=data;

对应的字节码可以进行说明 :

//aload_0表示将局部变量表中0位置(BaseEvent)的值压入栈中,此时栈高度为1,
0: aload_0

// 调用父类Object的构造方法
1: invokespecial #1                  // Method java/lang/Object."<init>":()V

// aload_0表示将局部变量表中0位置(BaseEvent)的值压入栈中,此时栈高度为1
4: aload_0

// iload_1表示将局部变量表中1位置(int类型,operateCode)的值压入栈中,此时栈高度为2
5: iload_1

// 成员field赋值,此时栈高度变为0
6: putfield      #2                  // Field operateCode:I

// aload_0表示将局部变量表中0位置(BaseEvent)的值压入栈中,此时栈高度为1
9: aload_0

// aload_2表示将局部变量表中2位置(Object)的值压入栈中,此时栈高度为2
10: aload_2

// 成员field赋值,此时栈高度变为0
11: putfield      #3                  // Field data:Ljava/lang/Object;

//方法结束
14: return

可以看到,基于栈的指令不需要考虑寄存器等硬件设备,因此兼容性较高,但以为需要频繁的移动栈中数据以及赋值,代码需要更多的执行时间。

三、扩展:从虚拟机看程序

class文件生成字节码是无法直接运行的,因此需要在某个时刻翻译为机器语言,根据虚拟机的不同,这部分处理会有些差异。

对于最早期的java虚拟机来说,采用存解释的方式执行,如此一来,虚拟机实现时就需要内置一个解释器,在运行时将字节码转换为机器码,然后运行,这种方式很明显效率会有些低,但胜在占用内存较小。

安卓前期使用Dalvik虚拟机则内置了JIT(Just In Time,前面也有提及),虚拟机可以根据统计方法调用次数回边次数(可简单的理解为方法内部循环体执行次数),将一些调用频率较高的代码(方法),编译为机器代码,这样可以显著提高程序的执行效率

而在安卓4.4之后的ART(Android Running Time)则采用了AOT(Ahead Of Time)方式,在apk安装时就将dex文件转为机器代码,这样虽然占用的空间较大,安装时间较长,但在每次使用时会非常快。

1、java内存结构概况

总的来说,虚拟机最后执行的只是机器代码,而执行代码时,肯定需要对局部变量,对象等进行存储,用图来简略的说明虚拟机内的存储结构:

这里写图片描述

2、功能说明

  • 方法区是进程持有的,所有的线程都可以访问。
  • 虚拟机栈也可以成为线程栈,里面存储有方法调用链表
  • 本地方法栈时当初虚拟机设计时为其他语言(如C语言)调用提供的接口
  • 程序计数器则记录了当前线程中程序执行到了哪一步。

以java文件来说,每个类文件在加载后生成的class对象都保存在方法区中,如果类加载器相同,则全局保持单例;一些虚拟机中常量池也保存在方法区内。

堆中则保持所有线程通过new或其他方法创建的实例对象,为所有线程共有。

至于方法中的局部变量,则只是在运行时保存与虚拟机栈中,通过上面的程序分析可以看出,如果虚拟机架构基于栈的话,那么局部变量名称都不会有记录;事实上,虚拟机栈中的结构非常复杂,甚至可能有些实例对象创建后不保存于堆中,而是直接放入到虚拟机栈中进行使用。

3、虚拟机针对代码的“优化”处理

在字节码转化为机器码的过程中,为了提高运行效率,往往还会进行其他的一些代码排序工作;这就会导致多线程环境下的同步问题;

一般来说,多线程环境下,如果有多个线程访问修改同一数据,就需要加锁,加锁的本质其实是为了保证多线程代码执行时三个条件:原子性,可见性,有序性

  • 原子性:某些操作虽然代码只有一行,却需要虚拟机执行多条指令,如果不能保证原子性,则在多线程环境下数据访问会出现错误。
  • 可见性:一个线程中对某个变量的修改对其他线程可见
  • 有序性:后面的代码与前面的执行顺序是无法确切肯定的(在前后代码没有关联的情况下),例如:

    int i=0;
    int j=1;

    在真正由虚拟机执行时,是无法保证两者执行的顺序的,这就是指令重排序,虽然在单个线程内,这种顺序不会影响最终的结果,但在多线程情况下,很可能会导致其他错误。

在这里因为考虑的是虚拟机对代码执行的影响,因此不会深入说明如何保持线程同步;这里只说明虚拟机对程序执行可能产生的影响;
由于乱序的可能,我们生成的字节码指令就没有那么可靠了,因此在多数情况下,需要加锁来保证程序的顺利执行。

4、程序执行的“最小单位”

之前说多线程状态下,一行代码可能在执行一半时,其他线程就对其进行了修改,这里简单说明一下程序的原子性操作。
顾名思义,原子性操作便是指那些不可在分的操作,操作共八种:

  1. read : 从主内存中读取
  2. load : 将read内容放入工作内存
  3. store :从工作内容中存储
  4. write : 将store内容写回主内存
  5. assign :赋值,将执行引擎中的值赋值给工作内存中变量
  6. use :使用工作内存中变量
  7. lock :锁定
  8. unlock :解锁

具体的操作规则可以参考其他书籍或百度,只要知道只有 如上的操作才能保证在处理过程中不会有其他数据影响。
而最终代码的执行也时通过这些存储使用操作来完成的。

工作内存和主内存是相对于线程进程而言的,每个线程都有自己的工作内存,所有的取值赋值操作等只能控制自己所在线程的工作内容中的数据,不能执行操纵主内存(同一进程主内存相同)中的内容;
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值