JVM教程之一【基础篇】

一: JDK&JRE&JVM关系

  • JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。
  • Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
  • JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。

综上所述:JDK包含JRE,JRE包含JVM,关系如下图所示
在这里插入图片描述

二: Java程序运行过程

  • Java源文件(xxx.java) -->java编译器(javac)–> java字节码文件(xxx.class)
  • java字节码文件(xxx.class)–> 类加载器(CalssLoader)–> 机器码
    在这里插入图片描述不同的操作系统具有不同的JVM,所以java可以跨平台运行

三:java类加载机制

1:类的加载过程

类的加载过程是指将java字节码文件装载到JVM虚拟机内(准确的说是将class文件读入到内存中,将其放在运行时数据区方法区内,然后在堆区创建一个java.lang.Class对象,用来封装在方法区内的数据结构)
类的加载过程主要分为加载–>链接–>初始化三个阶段

1.1:加载

类的加载过程中主要做如下三件事

  • 1:通过一个类的权限定名来获取器定义的二进制文件字节流
  • 2:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 3:在堆中生成一个代表这个类的class对象,作为方法区中这些数据的访问入口

1.2: 链接:

  • 1:验证 – 验证被加载类的正确性

    • 1.1: 文件格式的验证
      验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验
    • 1.2:元数据验证
      主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
    • 1.3:字节码验证
      这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。
    • 1.4:符号引用验证
      它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
  • 2:准备 - 为类的变量分配内存空间并设置初始化默认值

    • 2.1:类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
    • 2.2:这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如
  • 3:解析 - 将常量迟中的符号引用转化为直接引用的过程

    • 符号引用
      以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
    • 直接引用
      直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同

    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

1.3:初始化

在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

①声明类变量时指定初始值

②使用静态代码块为类变量指定初始值

  • JVM初始化步骤

    • 1、假如这个类还没有被加载和连接,则程序先加载并连接该类

    • 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

    • 3、假如类中有初始化语句,则系统依次执行这些初始化语句

  • 类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

    • 创建类的实例,也就是new的方式
    • 访问某个类或接口的静态变量,或者对该静态变量赋值
    • 调用类的静态方法
    • 反射(如 Class.forName(“com.shengsiyuan.Test”))
    • 初始化某个类的子类,则其父类也会被
    • 初始化Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类

2:类加载器

2.1 类加载的类型

  • Java语言系统自带有三个类加载器
加载器名称加载器说明
Bootstrap ClassLoader 【启动类加载器】主要加载核心类库,也就是我们环境变量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
Extention ClassLoader 【扩展类加载器】加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
Appclass Loader【应用程序类加载器】加载当前应用的classpath的所有类
  • 可根据实际情况自定义实现加载类
加载器名称加载器说明
Custom ClassLoader【自定义类加载器】加载应用程序根据自身需要定义的ClassLoader,如tomcat、jboss会根据及j2ee规范自行实现ClassLoader

类加载器示意图如下:
在这里插入图片描述

2.2 :类加载的三种方式

类加载的三种方式。

(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。

(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。

(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。

public class FDDloaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader loader = HelloWorld.class.getClassLoader();
        System.out.println(loader);
    }
}
//一、 使用ClassLoader.loadClass()来加载类,不会执行初始化块 loader.loadClass("Fdd");
//二、 使用Class.forName()来加载类,默认会执行初始化块 Class.forName("Fdd"); 
//三、使用Class.forName()来加载类,指定ClassLoader,初始化时不执行静态块 Class.forName("Fdd", false, loader); } }

2.3:双亲委派原则

  • 问题
    若使用应用程序类加载器,且应用程序类加载器有个java.lang.String类,但rt.jar包中也存在类java.lang.String,此时会造成不同的类加载器加载不同的String对象。
    所以需要采用双亲委派原则保证用不同的类加载器最终得到的都是同样一个Object对象,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载。
    用不同的类加载器最终得到的都是同样一个Object对象
  • 双亲委派的工作流程
    当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

2.4:自定义加载类的实现

  • 自定义加载类主要有两种实现方式
    • 遵守双亲委派模型:继承ClassLoader,重写findClass()方法。

    • 破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。

      //遵守双亲委派原则
       public class MyClassLoader extends ClassLoader {
        private String libPath;
      
       public DiskClassLoader(String path) {
           libPath = path;
       }
      
       @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
         	String fileName = getFileName(name);
          File file = new File(libPath, fileName);
      
             try {
      		   FileInputStream is = new FileInputStream(file);
       		  ByteArrayOutputStream bos = new ByteArrayOutputStream();
        		 int len = 0;
      
        		 try {
          		   while ((len = is.read()) != -1) {
                		 bos.write(len);
             }
         } catch (IOException e) {
             e.printStackTrace();
         }
      
         byte[] data = bos.toByteArray();
         is.close();
         bos.close();
      
         return defineClass(name, data, 0, data.length);
            } catch (IOException e) {
         e.printStackTrace();
           }
      
          return super.findClass(name);
      } //获取要加载 的class文件名
      
         private String getFileName(String name) {
             int index = name.lastIndexOf('.');
      
             if (index == -1) {
                 return name + ".class";
             } else {
                 return name.substring(index + 1) + ".class";
             }
         }
         }
      

四: JVM虚拟机内存模型(一)

综合示意图
在这里插入图片描述
JVM的整体内存模型结构如下图所示

在这里插入图片描述JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区
域【JAVA 堆、方法区】、直接内存

1:类加载子系统

负责加载文件存入运行时数据, 详见本文第三部分

2:运行时数据

负责存储运行时的数据的地方

在这里插入图片描述

2.1:方法区

方法区存储类的构造信息+常量+静态变量+即时编译后的代码数据

  • 方法区在逻辑上属于堆的一部分,一般称为非堆

在这里插入图片描述

2.2:虚拟机栈

Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表(局部变量表需要的内存在编译期间就确定了所在方法运行期间不会改变大小)、操作数栈、动态链接、方法出口等信息,每一个方法从调用至出栈的过程,就对应着栈帧在虚拟机中从入栈到出栈的过程。
虚拟机栈通过压/出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上
在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定
栈帧在整个JVM体系中的地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等

栈帧结构:

在这里插入图片描述
在这里插入图片描述

  • 局部变量表
    保存方法中用到的所有局部变量,包括基本数据类型和引用类型

    • 1.局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
    • 2.局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址)
  • 操作数栈

    • 操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程。

    • 同局部变量表一样,操作数栈的最大深度也是Java 程序编译成 Class 文件时被写入到 Class 文件格式属性表的 Code 属性的 max_stacks 数据项中。

    • 操作数栈的每一个元素可以是任意的 Java 数据类型,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2,在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值(指的是进入操作数栈的 “同一批操作” 的数据类型的栈容量的和)。

    • 当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中,也提供一些指令向操作数栈中写入和提取值,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。例如,整数加法的字节码指令 iadd(使用 iadd 指令时,相加的两个元素也必须是 int 型) 在运行的时候将操作数栈中最接近栈顶的两个 int 数值元素出栈相加,然后将相加结果入栈。

      // java 代码
      public void test() {
           byte a = 1;
           short b = 1;
           int c = 1;
           long d = 1L;
           float e = 1F;
           double f = 1D;
           char g = 'a';
           boolean h = true;
      }
      
      // 字节码指令
      0: iconst_1   // 把 a 压入操作数栈栈顶
      1: istore_1   // 将栈顶的 a 存入局部变量表索引为1的 Slot
      2: iconst_1  // 把 b 压入操作数栈栈顶
      3: istore_2   // 将栈顶的 b 存入局部变量表索引为2的 Slot
      4: iconst_1   // 把 c 压入操作数栈栈顶
      5: istore_3    // 将栈顶的 c 存入局部变量表索引为3的 Slot
      6: lconst_1   // 把 d 压入操作数栈栈顶
      7: lstore        4   // 将栈顶的 d 存入局部变量表索引为4的 Slot,由于 long 是64位,所以占2个 Slot
      9: fconst_1   // 把 e 压入操作数栈栈顶
      10: fstore        6   // 将栈顶的 e 存入局部变量表索引为6的 Slot
      12: dconst_1   // 把 f 压入操作数栈栈顶
      13: dstore        7   // 将栈顶的 f 存入局部变量表索引为4的 Slot,由于 double 是64位,所以占2个 Slot
      15: bipush        97   // 把 g 压入操作数栈栈顶
      17: istore        9   // 将栈顶的 g 存入局部变量表索引为9的 Slot
      19: iconst_1   // 把 h 压入操作数栈栈顶
      20: istore        10   // 将栈顶的 h 存入局部变量表索引为10的 Slot
      
  • 动态链接

  • 方法出口
    当一个方法开始执行后,只有2种方式可以退出这个方法 :

    • 方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
    • 异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
      无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
  • java字节码指令

1:如下一段java代码程序、执行javac命令生成class文件

javac C:\Users\admin\Desktop\Demo.java

在这里插入图片描述
2:使用javap命令反解析class文件生成汇编代码如下图所示

 javap -c C:\Users\admin\Desktop\Demo.class

在这里插入图片描述3:部分指令说明

指令说明
aload_0
invokespecial
bipush
putfield
ldc常量池中的常量 zhangsan 入栈
  • 变量指向堆
    在这里插入图片描述
  • 方法区指向堆
    在这里插入图片描述
  • 堆指向方法区
    在这里插入图片描述

2.3:本地方法栈

加载本地方法的数据区

  • 本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出
    StackOverflowError和OutOfMemoryError异常。
  • 不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。
  • HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。

2.4:程序计数器

记录当前字节码指令执行到的地址

  • 一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有” 的内存。
  • 正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址) 。如果还是 Native 方法,则为空。
  • 这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域

2.5:堆 --后续章节介绍

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

班婕妤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值