• java 程序运行的基础知识【Java bytecode】


    JVM 线程栈 到 函数运行

    每一个JVM线程来说启动的时候都会创建一个私有的线程栈。一个jvm线程栈用来存储栈帧,jvm线程栈和C语言中的栈很类似,它负责管理局部变量、部分运算结果,同时也参与到函数调用和函数返回的工作中。JVM规范中运行线程栈的大小可以是固定的或者是动态分配的,也可以是根据一定规则计算的。不同jvm对栈的实现会不同,一些可能提供给开发人员自己控制jvm线程栈初始大小的方式;对于动态分配来说也可能提供对jvm最大和最小值的设置。

    当计算一个线程需要的分配的大小超出了固定值、或者设置的最大值,jvm会抛出StackOverflowError。而对于动态分配栈来说,如果内存不能够提供足够的空间来满足最小值、或者需要的值JVM会抛出 OutOfMemoryError

    栈帧,可以理解成一个函数执行的环境,它管理参数、局部变量、返回值等等。

    每个栈帧都包括一个管理局部变量的数组( local variables),这个数组的单元数量在编译成字节码的时候就能确定了。对于32-bit 一个单位能够存放 boolean, byte, char, short, int, float, reference,returnAddress;连续两个单位就能够用来存储long 、double。局部变量数组的下标是从0开始,一般而言0位置存储的是this,后面接着是函数的参数,再是函数中出现的局部变量。

    每个栈帧也都包括一个(LIFO)操作栈的数据结构(operand stack),它的大小同样也可以在编译的时候确定,创建的时候会是个空栈。举个简单的例子,来描述它公用,对于int a+b来说,先把push a 进入栈中,再朴实 b 进入入栈中,然后 同时pop 两个值执行iadd 指令,再将其加后的结果push入栈中完成指令。

    除开以上两个关键的结构,每个栈帧还有常量池( run-time constant pool)、异常抛出管理等结构。在此就不一一详细说来了,可以参考其他资料。

    再来通过一个简单的 Demo 来说明,一个栈帧的工作。首先,我们来看这样的一个函数:

    public int comp(float number1, float number2){
    		int result ;
    		if(number1 < number2)
    			result = 1;
    		else
    			result = 2;
    		return result;
    	}
    

    其中函数内逻辑对应的字节码,如下:

     0: fload_1
     1: fload_2
     2: fcmpg
     3: ifge          11
     6: iconst_1
     7: istore_3
     8: goto          13
    11: iconst_2
    12: istore_3
    13: iload_3
    14: ireturn
    
    

    对于这几个字节码指令稍微说明下:

    fload_x:取局部变量数组中第x个,类型fload,push 入栈;
    fcmpg:比较两个单精度浮点数。如果两数大于结果为1,相等则结果为0,小于的话结果为-1;
    ifge:跳转指令;
    iconst_x:push 常量x入栈;
    istore_x:pop栈存入局部变量数组第x个;
    iload_x:读取局部变量数组第x个,入栈;
    ireturn:函数结束返回int型;
    
    

    细心点观察可以发现i开头指代int,f开头指代fload,load代表载入,if代表跳转等等,其中字节码的操作码定义也是有一定意义的,详情可以翻译jvm字节码相关标准。再来看看,jvm如何在栈帧结构上执行情况,以具体调用comp(1.02,2.02)为例:

    效果图

    Java 的 Class

    说字节码,一定少不了.class。不妨,以一个demo类 来具体看class 的内容,类非常简单,两个函数一个say,另外一个就是上面的cmp函数。

    public class Hello {
    
    	public void say(){
    		System.out.println("Hello world!");
    	}
    
    	public int comp(float number1, float number2){
    		int result ;
    		if(number1 < number2)
    			result = 1;
    		else
    			result = 2;
    		return result;
    	}
    } 
    

    用 javac -g:none Hello.java 来编译这个类的,然后用 javap -c -v Hello.class 来解析编译的class。

    Classfile /src/main/java/com/demo/Hello.class
      Last modified 2016-10-28; size 404 bytes
      MD5 checksum 9ac6c800c312d65b568dd2a0718bd2c5
    public class com.demo.Hello
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #6.#14         // java/lang/Object."<init>":()V
       #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #17            // Hello world!
       #4 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #20            // com/demo/Hello
       #6 = Class              #21            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               say
      #11 = Utf8               comp
      #12 = Utf8               (FF)I
      #13 = Utf8               StackMapTable
      #14 = NameAndType        #7:#8          // "<init>":()V
      #15 = Class              #22            // java/lang/System
      #16 = NameAndType        #23:#24        // out:Ljava/io/PrintStream;
      #17 = Utf8               Hello world!
      #18 = Class              #25            // java/io/PrintStream
      #19 = NameAndType        #26:#27        // println:(Ljava/lang/String;)V
      #20 = Utf8               com/demo/Hello
      #21 = Utf8               java/lang/Object
      #22 = Utf8               java/lang/System
      #23 = Utf8               out
      #24 = Utf8               Ljava/io/PrintStream;
      #25 = Utf8               java/io/PrintStream
      #26 = Utf8               println
      #27 = Utf8               (Ljava/lang/String;)V
    {
      public com.demo.Hello();
        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
    
      public void say();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String Hello world!
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
    
      public int comp(float, float);
        descriptor: (FF)I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=3
             0: fload_1
             1: fload_2
             2: fcmpg
             3: ifge          11
             6: iconst_1
             7: istore_3
             8: goto          13
            11: iconst_2
            12: istore_3
            13: iload_3
            14: ireturn
          StackMapTable: number_of_entries = 2
            frame_type = 11 /* same */
            frame_type = 252 /* append */
              offset_delta = 1
              locals = [ int ]
    }
    
    

    解释下其中涉及的新的操作码

    getstatic:获取镜头变量;
    invokevirtual:调用函数;
    return:void 函数结束返回;
    

    在 public int comp(float, float) code 这段代码里面就能看到上面提到的字节码运行的例子。有了个感性认识,其实大体看到class文件里面,除了字节码指令外,还包括了常量pool,访问标志(public 等),类的相关信息(属性、函数、常量等)。因为前面用的是 -g:node进行编译的,其他模式下还可以有其他扩展、调试信息也包括在class里面。官方给出的class文件格式,详细如下:

    ClassFile {
        u4             magic;
        u2             minor_version;
        u2             major_version;
        u2             constant_pool_count;
        cp_info        constant_pool[constant_pool_count-1];
        u2             access_flags;
        u2             this_class;
        u2             super_class;
        u2             interfaces_count;
        u2             interfaces[interfaces_count];
        u2             fields_count;
        field_info     fields[fields_count];
        u2             methods_count;
        method_info    methods[methods_count];
        u2             attributes_count;
        attribute_info attributes[attributes_count];
    }
    
    

    magic: 就是非常有名的 0xCAFEBABE ,一个标识class文件;

    minor_version 、major_version :指的是java class 文件的版本,一般说class文件的版本是 XX.xx 其中XX 就是major,xx是minor,比如上面demo中的版本是52.0 代表就是 minor 0,major 51.

    constant_pool_count:就是常量池元素个数,cp_info constant_pool[constant_pool_count-1] 就是相关的详细信息了。

    access_flags:指的是访问标识例如ACC_PUBLIC、ACC_FINAL、ACC_INTERFACE、ACC_SUPER 写过java的相信看名字应该知道啥意思,ACC是access的缩写。

    其他具体的,就不一一介绍了详细可以直接参考官方文档。

    动态生成java字节码

    当然,你可以直接按照官方的class文件格式来直接写 byte[],然后自定义个 class load 载入编写的byte[]来实现动态生成class。不过,这个要求可能也有点高,必须的非常熟悉class文件格式才能做到。这里demo还是借助 ASM 这个类库来简单演示下,就编写下 上面的Hello 不过里面只实现say的方法。如下:

    public class AsmDemo {
    
    	public static final String CLASS_NAME = "Hello";	
    	public static final AsmDemoLoad load = new AsmDemoLoad();
    
    	private static class AsmDemoLoad extends ClassLoader {
    
    		public AsmDemoLoad() {
    
    			super(AsmDemo.class.getClassLoader());
    		}
    
    		public Class<?> defineClassForName(String name, byte[] data) {
    			return this.defineClass(name, data, 0, data.length);
    		}
    	}
    
    	public static byte[] generateSayHello() throws IOException {
    
    		ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    		classWriter.visit(V1_7, ACC_PUBLIC + ACC_SUPER, CLASS_NAME, null, getInternalName(Object.class), null);
    	
    		//默认初始化函数
    		Method constructorMethod = Method.getMethod("void <init> ()");
    		GeneratorAdapter constructor = new GeneratorAdapter(ACC_PUBLIC, constructorMethod, null, null, classWriter);
    		constructor.loadThis();
    		//每个类都要基础Object
    		constructor.invokeConstructor(Type.getType(Object.class), constructorMethod);
    		constructor.returnValue();
    		constructor.endMethod();
    
    		Method mainMethod = Method.getMethod("void say ()");
    		GeneratorAdapter main = new GeneratorAdapter(ACC_PUBLIC, mainMethod, null, null, classWriter);
    		main.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
    		main.push("Hello world!");
    		main.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println (String)"));
    		main.returnValue();
    		main.endMethod();
    
    		return classWriter.toByteArray();
    	}
    
    	public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException,
    			InvocationTargetException, InstantiationException, NoSuchMethodException, SecurityException, IOException {
    
    		byte[] code = AsmDemo.generateSayHello();
    
    		//反射构建 hello 类,调用hello方法。
    		Class<?> hello = load.defineClassForName(CLASS_NAME, code);
    		hello.getMethod("say", null).invoke(hello.newInstance(), null);
    	}
    }
    

    关于动态生成字节码用途,一定场景下是可以提升效率与性能,因为动态生成的类和普通的载入类并无太大区别。手工优化后的字节码执行可能比编译的要优,可以替代反射使用的许多场景 同时避免反射的性能消耗。很著名的一个例子,fastJSON 就是使用内嵌 ASM 框架动态生成字节码类,来进行序列和反序列化工作,是目前公认最快的json字符串解析。

  • 相关阅读:
    P1067 多项式输出(模拟水题)
    A. The Fair Nut and Elevator(暴力)
    A. The Fair Nut and Elevator(暴力)
    Knight Tournament (set)
    jquery怎么添加多个类名
    jquery对类的操作,添加,删除,点击添加,再点击删除
    jquery操作css样式的方法
    jquery浅复制和深复制区别
    TS 三种函数的定义方式
    ES7及ES8新特性
  • 原文地址:https://www.cnblogs.com/jzhlin/p/6008568.html
Copyright © 2020-2023  润新知