JVM | Java程序如何执行

  • 类文件结构基础

  • Class文件是一组以8位字节为基础的单位的二进制流,各个数据项目按照顺序紧凑地排列在Class文件之中,中间没有任何分隔符。
    Class文件存储结构中只有两种数据类型:无符号数和表(表又是由多个无符号数或者其他表构成)。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数是Class类文件的基石。

    • 字节码指令基础

    参考:http://blog.51cto.com/damon188/2131035

    • 回顾JVM运行时数据区域

    • 方法区:

    线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。

    • 堆:

    线程共享,存储对象实例。

    • 程序计数器:

    线程私有,存储当前线程正在执行的虚拟机字节码指令的偏移地址(指令行号)。

    • 虚拟机栈:

    线程私有,描述Java方法执行的内存模型。

    • 方法执行的内存模型

    • 局部变量表:

    变量存储空间,存放方法参数、方法内定义的局部变量。

    • 操作数栈:

    执行操作的空间,执行过程中,会有各种字节码指令网操作数栈写入和提取内容,入栈、出栈操作。

    • 动态连接:

    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。
    在每一次运行期间转化为直接引用的符号引用。
    相对的在类加载阶段或者第一次使用的时候就转化为直接引用的符号引用,转化过程称为静态解析。

    • 返回地址:

    方法被调用的位置。

    • 局部变量表slot槽复用问题:

      public class SlotReuse {
      public static void methodA(){
      byte[] placeholder = new byte[64 1024 1024];
      System.gc();
      }
      public static void methodB(){
      {
      byte[] placeholder = new byte[64 1024 1024];
      }
      System.gc();
      }
      public static void methodC(){
      {
      byte[] placeholder = new byte[64 1024 1024];
      }
      int a = 0;
      System.gc();
      }
      public static void main(String[] args) {
      // methodA();
      // methodB();
      // methodC();
      }
      }

    虚拟机运行参数加上“-verbose:gc”,查看垃圾收集过程。

    • 执行methodA();
    [GC 69499K->66048K(251392K), 0.0010970 secs]
    [Full GC 66048K->65866K(251392K), 0.0098170 secs]

    处于变量placeholder的作用域,不敢回收placeholder的内存。

    • 执行methodB();
    [GC 69499K->66048K(251392K), 0.0011820 secs]
    [Full GC 66048K->65866K(251392K), 0.0112650 secs]

    逻辑上出了placeholder的作用域,placeholder的内存仍然没有被回收,原因就是离开了placeholder的作用域后,没有任何对局部变量表的读写操作,placeholder原本所占用的Slot槽还没有被其他变量复用,作为GC Roots一部分的局部变量表仍然保持着对它的关联。

    • 执行methodC();
    [GC 69499K->66000K(251392K), 0.0012780 secs]
    [Full GC 66000K->330K(251392K), 0.0099390 secs]

    placeholder原本所占用的Slot槽被其他变量复用,切断了GC Roots关联,正常垃圾回收。

    • 如何找到正确的方法

    确定被调用方法的版本(即调用哪一个方法)是方法调用阶段的唯一任务。

    • 5种方法调用字节码指令:

    1)invokestatic:调用静态方法。
    2)invokespecial:调用实例构造器<init>方法、私有方法和父类方法。
    3)invokevirtual:调用所有的虚方法。非虚方法以外的都是虚方法,非虚方法包括使用invokestatic、invokespecial调用的方法和被final修饰的方法。
    4)invokeinterface:调用接口方法,运行时再确定一个实现此接口的对象。
    5)invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
    ireturn(返回值是boolean、byte、char、short、int)、lreturn、freturn、dreturn、areturn:方法返回指令。

    • 静态解析

    方法在程序真正运行之前就有一个可确定的调用版本,并且这个调用版本在运行期不可变。
    只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类。

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

    字节码:

    public static void main(java.lang.String[]);
             Signature: ([Ljava/lang/String;)V
             flags: ACC_PUBLIC, ACC_STATIC
             Code:
                 stack=0, locals=1, args_size=1
                 0: invokestatic  #5                  // Method sayHello:()V
                 3: return
             LineNumberTable:
                 line 14: 0
                 line 15: 3
    • 静态分派

    所有依赖静态类型来定位方法执行版本的分派动作。
    分派时机:编译阶段
    典型应用:方法重载
    e.g.

        public class StaticDispatch {
            public static void sayHello(short arg){
                    System.out.println("hello short");
            }
            public static void sayHello(byte arg){
                    System.out.println("hello byte");
            }
            public static void sayHello(Object arg){
                    System.out.println("hello Object");
            }
            public static void sayHello(int arg){
                    System.out.println("hello int");
            }
            public static void sayHello(long arg){
                    System.out.println("hello long");
            }
            public static void sayHello(char arg){
                    System.out.println("hello char");
            }
            public static void sayHello(char... args){
                    System.out.println("hello char...");
            }
            public static void sayHello(Serializable arg){
                    System.out.println("hello Serializable");
            }
            public static void main(String[] args) {
                    sayHello(‘a‘);
            }
    }

    当前main方法编译字节码:

    public static void main(java.lang.String[]);
    Signature: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush        97
         2: invokestatic  #12                 // Method sayHello:(C)V
         5: return
      LineNumberTable:
        line 46: 0
        line 47: 5

    注释public static void sayHello(char arg)构造方法后编译字节码:

    public static void main(java.lang.String[]);
    Signature: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush        97
         2: invokestatic  #11                 // Method sayHello:(I)V
         5: return
      LineNumberTable:
        line 46: 0
        line 47: 5

    自动转型顺序:char --> int --> long --> float --> double --> Serializable --> Object --> char...(可变长字符)
    可以宽化,不可以窄化。

    • 动态分派

    运行期根据实际类型确定方法执行版本的分派动作。
    分派时机:运行阶段
    典型应用:方法重写
    e.g.

        public class DynamicDispatch {
                static abstract class Human {
                        protected abstract void sayHello();
                }
                static class Man extends Human {
                        @Override
                        protected void sayHello() {
                                System.out.println("man say hello");
                        }
                }
                static class Woman extends Human {
                        @Override
                        protected void sayHello() {
                                System.out.println("woman say hello");
                        }
                }
                public static void main(String[] args) {
                        Human man = new Man();
                        Human woman = new Woman();
                        man.sayHello();
                        woman.sayHello();
                        man = new Woman();
                        man.sayHello();
                }
        }

    字节码:

        public static void main(java.lang.String[]);
            Signature: ([Ljava/lang/String;)V
            flags: ACC_PUBLIC, ACC_STATIC
            Code:
                stack=2, locals=3, args_size=1
                     0: new           #2                  // class edu/atlas/demo/java/jvm/DynamicDispatch$Man
                     3: dup
                     4: invokespecial #3                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Man."<init>":()V
                     7: astore_1
                     8: new           #4                  // class edu/atlas/demo/java/jvm/DynamicDispatch$Woman
                    11: dup
                    12: invokespecial #5                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Woman."<init>":()V
                    15: astore_2
                    16: aload_1
                    17: invokevirtual #6                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
                    20: aload_2
                    21: invokevirtual #6                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
                    24: new           #4                  // class edu/atlas/demo/java/jvm/DynamicDispatch$Woman
                    27: dup
                    28: invokespecial #5                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Woman."<init>":()V
                    31: astore_1
                    32: aload_1
                    33: invokevirtual #6                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
                    36: return
                LineNumberTable:
                    line 32: 0
                    line 33: 8
                    line 34: 16
                    line 35: 20
                    line 36: 24
                    line 37: 32
                    line 38: 36
    • invokevirtual指令的运行时解析过程:
      1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
      2)如果在类型C中找到与常量中的描述符和简单名称都相等的方法,则进行访问权限校验,
      如果通过则返回这个方法的直接引用,查找过程结束;
      如果不通过,则返回java.lang.IllegalAccessError异常。
      3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
      4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

    由于invokevirtual指令的第一步就是在运行期间确定接收者的世界类型,所以两次调用的invokevirtual指令把常量池中类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。

    • 虚拟机动态分派的实现

    上面介绍了动态分派的过程,然而由于动态分派是非常频繁的动作,因此虚拟机实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜索。常用的稳定优化手段就是为类在方法区建立一个虚方法表(vtable),接口方法建立接口方法表(itable)。
    虚方法表存放着各个方法的实际入口地址。
    如果某个方法在子类中没有被重写,那么子类的虚方法里面的地址和父类相同方法的地址入口是一致的,都指向父类方的实现入口。
    如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
    方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

    • 如何执行方法内的字节码

    • 基于栈的解释器执行过程
      e.g.
    public static int methodA(int a, int b){
        return a + b;
    }
    public static int methodB(int a, int b){
        return a - b;
    }
    public static int methodC(int a, int b){
        return a * b;
    }
    public static int heavyMethod(){
        int a = 200;
        int b = 100;
        int c = methodC(methodA(a, b), methodB(a, b));
        return c;
    }

    字节码:

        public static int methodA(int, int);
            Signature: (II)I
            flags: ACC_PUBLIC, ACC_STATIC
            Code:
                stack=2, locals=2, args_size=2
                     0: iload_0
                     1: iload_1
                     2: iadd
                     3: ireturn
                LineNumberTable:
                    line 18: 0
    
        public static int methodB(int, int);
            Signature: (II)I
            flags: ACC_PUBLIC, ACC_STATIC
            Code:
                stack=2, locals=2, args_size=2
                     0: iload_0
                     1: iload_1
                     2: isub
                     3: ireturn
                LineNumberTable:
                    line 22: 0
    
        public static int methodC(int, int);
            Signature: (II)I
            flags: ACC_PUBLIC, ACC_STATIC
            Code:
                stack=2, locals=2, args_size=2
                     0: iload_0
                     1: iload_1
                     2: imul
                     3: ireturn
                LineNumberTable:
                    line 26: 0
    
        public static int heavyMethod();
            Signature: ()I
            flags: ACC_PUBLIC, ACC_STATIC
            Code:
                stack=3, locals=3, args_size=0
                     0: sipush        200
                     3: istore_0
                     4: bipush        100
                     6: istore_1
                     7: iload_0
                     8: iload_1
                     9: invokestatic  #17                 // Method methodA:(II)I
                    12: iload_0
                    13: iload_1
                    14: invokestatic  #18                 // Method methodB:(II)I
                    17: invokestatic  #19                 // Method methodC:(II)I
                    20: istore_2
                    21: iload_2
                    22: ireturn
                LineNumberTable:
                    line 128: 0
                    line 129: 4
                    line 130: 7
                    line 140: 21

    图例分析:

    原文地址:http://blog.51cto.com/damon188/2131414

    时间: 2024-11-10 17:26:01

    JVM | Java程序如何执行的相关文章

    java程序的执行

    Java平台和语言最开始只是SUN公司在1990年12月开始研究的一个内部项目.SUN公司的一个叫做帕特里克·诺顿的工程师被自己开发的C和C语言编译器搞得焦头烂额,因为其中的API极其难用.帕特里克决定改用NeXT,同时他也获得了研究公司的一个叫做"Stealth 计划"的项目的机会. ??"Stealth 计划"后来改名为"Green计划",JGosling(詹姆斯·高斯林)和麦克·舍林丹也加入了帕特里克的工作小组.他们和其他几个工程师一起在加

    linux下crontab定时执行java程序,java程序不执行的问题

    crontab的语法 在以上各个字段中,还可以使用以下特殊字符: 星号(*):代表所有可能的值,例如month字段如果是星号,则表示在满足其它字段的制约条件后每月都执行该命令操作. 逗号(,):可以用逗号隔开的值指定一个列表范围,例如,"1,2,5,7,8,9" 中杠(-):可以用整数之间的中杠表示一个整数范围,例如"2-6"表示"2,3,4,5,6" 正斜线(/):可以用正斜线指定时间的间隔频率,例如"0-23/2"表示每两

    一个Java程序的执行过程(转)

    我们手工执行java程序是这样的: 1.在记事本中或者是UE的文本编辑器中,写好源程序: 2.使用javac命令把源程序编译成.class文件: 编译后的.class(类字节码)文件中会包含以下内容: ConstantPool:符号表: FieldInfo:类中的成员变量信息: MethodInfo:类中的方法描述: Attribute:可选的附加节点. FieldInfo节点包含成员变量的名称,诸如public,private,static等的标志.ConstantValue属性用来存储静态的

    Eclipse下导出java程序可执行的jar包图片无法显示问题的一种解决方法

    说明:在eclipse中运行java程序的时候一切正常,可是当把jar包导出的时候却发现图片没法显示,这估计是java程序的各种配置和路径问题所导致,后来找到一种解决方法,供遇到这方面问题的学习java程序的鞋同参考: Java项目下的目录结构如下: 其中class类放在包:package accpedu; (即实际是在如上bin/accpedu文件夹下面) 通过下面的方法来引用图片时,在eclipse里面执行是可以正常显示图片的: ImageIcon image1 = new ImageIco

    java程序中执行HiveQL

    这里是指java中执行hive或者hiveQL. 注意:而不是经常说的通过JDBC的方式连接Hiveserver2来执行查询.是在部署了hiveserver的服务器上执行hive命令.这样就可以将分析得结果写到文件中,不用一定在hive的命令行client下执行. String sql="show tables; select * from test_tb limit 10"; List<String> command = new ArrayList<String&g

    1.3.2 java程序的运行机制和jvm

    java语言比较特殊,由java语言编写的程序需要经过编译步骤,但这个编译步骤并不会生成特定平台的机器码,而是生成一种与平台无关的字节码(也就是*.class文件).当然,这种字节码不是可执行性的,必须使用java解释器来解释执行.因此,我们可以认为:java语言既是编译型语言,也是解释型语言.或者说,java语言既不是纯粹的编译型语言,也不是纯粹的解释型语言.java程序的执行过程必须经过先编译,后解释两个步骤.如图1.1所示. java语言里负责解释执行字节码文件的是java虚拟机,既 JV

    Java程序的运行机制和JVM

    1. Java语言比较特殊, 由Java编写的程序需要经过编译步骤,但这个编译步骤不会产生特定平台的机器码,而是生成一种与平台无关的字节码(也就是.class文件).这种字节码不是可执行性的,必须使用Java解释器来解释执行.因此可以认为: Java语言既是编译型语言,也是解释型语言. 或者说,Java语言既不是纯粹的编译型语言,也不是纯粹的解释型语言. Java程序的执行过程,必须经过先编译,后解释两个步骤. 2. Java语言里,负责解释执行字节码文件的是Java虚拟机,即JVM(Java

    在命令符模式下编译并执行Java程序

    对于Java初学者,建议使用纯文本文件来编写Java程序,并在命令符模式下使用工具程序编译和执行Java程序.使用javac工具编译.java,使用java工具执行.class.(推荐sublime编辑器) 怎么在命令符模式下编译并执行Java程序?弄了好久查了不少资料终于解决.记录下来,便于总结.分享.查阅. 首先根据自己计算机的操作系统下载和安装sun公司的JDK,http://www.oracle.com/technetwork/cn/java /javase/downloads/inde

    Linux下执行Java程序

    在linux下编译java程序,执行javac编译生成class文件时,在centos7终端输入如,javac hello.java    会提示未找到指令,但用java -verison测试环境变量是没问题的 百度了好久,说的很复杂,重新再linux配置环境变量,输入 vi /etc/profile进入,添加以下代码: export JAVA_HOME=/usr/local/jdk1.8.0_144 export PATH=$JAVA_HOME/bin:$PATH export CLASSPA