
课程咨询: 400-996-5531 / 投诉建议: 400-111-8989
认真做教育 专心促就业
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来说明,一个栈帧的工作。首先,我们来看这样的一个函数:
publicintcomp(floatnumber1,floatnumber2){intresult ;if(number1 < number2) result =1;elseresult =2;returnresult; }
其中函数内逻辑对应的字节码,如下:
0: fload_11: fload_22: fcmpg3: ifge 116: iconst_17: istore_38: goto 1311: iconst_212: istore_313: iload_314: 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函数。
publicclassHello{publicvoidsay(){ System.out.println("Hello world!"); }publicintcomp(floatnumber1,floatnumber2){intresult ;if(number1 < number2) result =1;elseresult =2;returnresult; } }
用javac -g:none Hello.java来编译这个类的,然后用javap -c -v Hello.class来解析编译的class。
Classfile /src/main/java/com/demo/Hello.classLast modified2016-10-28; size404bytes MD5 checksum9ac6c800c312d65b568dd2a0718bd2c5publicclasscom.demo.Hello minor version:0major version:52flags: 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{publiccom.demo.Hello(); descriptor: ()V flags: ACC_PUBLIC Code:stack=1, locals=1, args_size=10: aload_01: invokespecial#1// Method java/lang/Object."<init>":()V4:returnpublicvoidsay(); descriptor: ()V flags: ACC_PUBLIC Code:stack=2, locals=1, args_size=10: 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;)V8:returnpublicintcomp(float,float); descriptor: (FF)I flags: ACC_PUBLIC Code:stack=2, locals=4, args_size=30: fload_11: fload_22: fcmpg3: ifge116: iconst_17: istore_38:goto1311: iconst_212: istore_313: iload_314: ireturn StackMapTable: number_of_entries =2frame_type =11/* same */frame_type =252/* append */offset_delta =1locals = [int] }
解释下其中涉及的新的操作码
getstatic:获取镜头变量; invokevirtual:调用函数;return:void函数结束返回;
在public int comp(float, float) code这段代码里面就能看到上面提到的字节码运行的例子。有了个感性认识,其实大体看到class文件里面,除了字节码指令外,还包括了常量pool,访问标志(public等),类的相关信息(属性、函数、常量等)。因为前面用的是-g:node进行编译的,其他模式下还可以有其他扩展、调试信息也包括在class里面。官方给出的class文件格式,详细如下:
ClassFile{u4magic;u2minor_version;u2major_version;u2constant_pool_count;cp_infoconstant_pool[constant_pool_count-1];u2access_flags;u2this_class;u2super_class;u2interfaces_count;u2interfaces[interfaces_count];u2fields_count;field_infofields[fields_count];u2methods_count;method_infomethods[methods_count];u2attributes_count;attribute_infoattributes[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的方法。如下:
publicclassAsmDemo{publicstaticfinalString CLASS_NAME ="Hello";publicstaticfinalAsmDemoLoad load =newAsmDemoLoad();privatestaticclassAsmDemoLoadextendsClassLoader{publicAsmDemoLoad(){super(AsmDemo.class.getClassLoader()); }publicClass<?> defineClassForName(String name,byte[] data) {returnthis.defineClass(name, data,0, data.length); } }publicstaticbyte[] generateSayHello()throwsIOException { ClassWriter classWriter =newClassWriter(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 =newGeneratorAdapter(ACC_PUBLIC, constructorMethod,null,null, classWriter); constructor.loadThis();//每个类都要基础Objectconstructor.invokeConstructor(Type.getType(Object.class), constructorMethod); constructor.returnValue(); constructor.endMethod(); Method mainMethod = Method.getMethod("void say ()"); GeneratorAdapter main =newGeneratorAdapter(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();returnclassWriter.toByteArray(); }publicstaticvoidmain(String[] args)throwsIllegalAccessException, 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字符串解析。