JVM 之 运行原理及例子剖析

前言

上一节中,我们学习了JVM的基本结构个内存结构(特指运行时数据区结构),本节我们讲学习一下JVM的运行流程,并通过一个实际例子来剖析一下在运行时JVM是如何分配内存结构中各个组成部分工作的。


1、JVM运行原理

  • JVM 运行原理
    这里写图片描述
    说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。

    总而言之 JVM 运行原理就是:Java代码编译和执行的整个过程。

  • Java代码编译和执行过程
    正如前面所说,Java代码的编译和执行的整个过程大概是:开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。

    • Java代码编译是由Java源码编译器来完成,也就是Java代码到JVM字节码(.class文件)的过程。 流程图如下所示:
      这里写图片描述
    • Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:
      这里写图片描述

2、JVM运行过程

下图是JVM运行的主要过程:
这里写图片描述


Java代码编译和执行的整个过程包含了以下三个重要的机制:Java源码编译机制,类加载机制,类执行机制。

  • 2.1、Java源码编译机制

    • Java 源码编译由以下三个过程组成:

      • 分析和输入到符号表
      • 注解处理
      • 语义分析和生成class文件

      流程图如下所示:
      这里写图片描述
      最后生成的class文件由以下部分组成:

      • 结构信息:包括class文件格式版本号及各部分的数量与大小的信息。
      • 元数据:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池。
      • 方法信息:对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息。
  • 2.2、类加载机制

    首先在学习类加载器加载class文件之前我们先学习一下类加载器。顾名思义类加载器是专门用来加载class文件的。

    • 2.2.1 确定类的层次关系和加载顺序
      这里写图片描述

      • Bootstrap ClassLoader
        负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。
      • Extension ClassLoader
        负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。
      • App ClassLoader
        负责记载classpath中指定的jar包及目录中class。
      • Custom ClassLoader
        属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

        这里写图片描述
        加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

        • 类加载顺序次图
          这里写图片描述
    • 2.2.2 检查类是否已经被加载
      检查类是否已经被加载,从底层往上层依次检查各个加载器已经加载的类,顺序是系统应用类加载器、扩展加载器、根加载器,一旦发现被某个加载器加载过,则马上使用该类。如果一直找到最顶层的根加载器,发现类还没有被加载进JVM运行数据区的方法区,则接下来就要加载该类。

    • 2.2.3 类加载过程
      加载过程和检查过程顺序相反,从上层往下层的顺序进行加载。从加载器检查自己的加载路径,找要加载的类,一旦找到类就进行加载。
  • 2.3、类执行机制
    类在被加载之后,接下来进行连接、初始化,然后才是使用,最后卸载。

    • 2.3.1 连接。连接(linking)包括三个部分:

      • 验证verifying:验证类符合Java规范和JVM规范,和编译阶段的语法语义分析不同。
      • 准备preparing:为类的静态变量分配内存,初始化为系统的初始值。(不初始化静态代码块)。对于final static修饰的变量,直接赋值为用户的定义值。
      • 解析resolving:将符号引用(字面量描述)转为直接引用(对象和实例的地址指针、实例变量和方法的偏移量)
    • 2.3.2 类初始化
      初始化类的静态变量和静态代码块为用户自定义的值。非静态类在实例化类,在Java堆中创建对象的时候,才会进行初始化。初始化的顺序,和Java源码的从上到下顺序一致。注意:什么时候触发初始化?在类被Java程序“第一次主动使用”的时候,才会触发初始化操作(如果还没有加载,则会顺势触发类的加载过程)。这里写图片描述

    • 2.3.3 内存分配
      启动JVM后,操作系统就给JVM分配了内存空间,JVM自己由把得到的内存分为几块:
      这里写图片描述
      JVM是基于栈结构的体系结构来执行class字节码的,不同于windows和Linux基于寄存器结构。类的执行机制,主要是在Java栈上面完成。当一个线程被创建后,Java栈和PC寄存器就会被创建。Java栈由栈帧组成,调用一个方法,就会生成一个栈帧(可以理解为表示调用一个方法)。栈帧又由局部变量表、操作数栈和常量池引用组成。这里写图片描述
      执行的时候,每个线程都有一个Java栈,当前执行的栈称为当前栈。一个Java栈调用多个方法,则会push很多个栈帧,当前活动的栈帧称为当前栈帧。当前栈帧执行完毕之后,会把执行结果(如果有)压入到调用它的那个栈帧的操作数栈中,作为上一个栈帧的一个中间处理结果被调用,然后就会被pop出去。当所有调用的方法执行结束后,栈帧也就都pop掉没有了。
      例如:执行代码
      这里写图片描述

    • 3.3.4 类具体的执行过程
      本步骤由执行引擎Execute Engine来完成。执行引擎把字节码转为机器码,然后操作系统才可以真正调用,在硬件环境上执行代码。执行引擎的通过Java字节码解释器(一行一行解释字节码)和JIT(Just In Time)即时编译器(对热代码整段编译)来完成机器码的翻译工作。

      • JIT编译器的工作流程为:
        JVM字节码 -> 机器无关优化 -> 中间代码 -> 机器相关优化 -> 中间代码 -> 寄存器分配器 -> 中间代码 -> 目标机器码生成器 -> 目标机器码
    • 3.3.5 类的卸载


例子

  • 案例分析一
    这里写图片描述

  • 案例分析二
    这里写图片描述
    上面main方法中运行的程序过程如下:

    • (1)用户创建了一个Student对象,运行时JVM首先会去方法区寻找该对象的类型信息,没有则使用类加载器classloader将Student.class字节码文件加载至内存中的方法区,并将Student类的类型信息存放至方法区。
    • (2)接着JVM在堆中为新的Student实例分配内存空间,这个实例持有着指向方法区的Student类型信息的引用,引用指的是类型信息在方法区中的内存地址。
    • (3)在此运行的JVM进程中,会首先起一个线程跑该用户程序,而创建线程的同时也创建了一个虚拟机栈,虚拟机栈用来跟踪线程运行中的一系列方法调用的过程,每调用一个方法就会创建并往栈中压入一个栈帧,栈帧用来存储方法的参数,局部变量和运算过程的临时数据。上面程序中的stu是对Student的引用,就存放于栈中,并持有指向堆中Student实例的内存地址。
    • (4)JVM根据stu引用持有的堆中对象的内存地址,定位到堆中的Student实例,由于堆中实例持有指向方法区的Student类型信息的引用,从而获得add()方法的字节码信息,接着执行add()方法包含的指令。
  • 案例分析三

    public class Demo01 {
        public static void main(String[] args) {
            A a = new A();
            System.out.println(a.width);
        }
    }
    
    class A{
        public static int width=100; //静态变量,静态域 field
        static{
            System.out.println("静态初始化类A");
            width = 300 ;
        }
        public A() {
            System.out.println("创建A类的对象");
        }
    }

    分析:
    这里写图片描述
    说明:
    内存中存在栈、堆(放创建好的对象)、方法区(实际也是一种特殊堆)。

    • 1、JVM加载Demo01时候,首先在方法区中形成Demo01类对应静态数据(类变量、类方法、代码…),同时在堆里面也会形成java.lang.Class对象(反射对象),代表Demo01类,通过对象可以访问到类二进制结构。然后加载变量A类信息,同时也会在堆里面形成a对象,代表A类。

    • 2、main方法执行时会在栈里面形成main方法栈帧,一个方法对应一个栈帧。如果main方法调用了别的方法,会在栈里面挨个往里压,main方法里面有个局部变量A类型的a,一开始a值为null,通过new调用类A的构造器,栈里面生成A()方法同时堆里面生成A对象,然后把A对象地址付给栈中的a,此时a拥有A对象地址。

    • 3、当调用A.width时,调用方法区数据。
  • 案例四
    这里写图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值