微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

jvm探究(1)——类加载子系统

文章目录

前言

少有java工程师可以抵挡探究jvm的诱惑,这个道理非常简单,人们总是喜欢学会怎么用,才去想知道为什么。本次的篇章将集中精力探究下大名鼎鼎的java虚拟机。jvm篇所有的内容几乎都是以hotSpot虚拟机展开的,下面我们开始。 ————————————————————————————————————————————— 本次章节重点不在于林林总总地敞开谈java virtual machine。而是将所有最关键的需要理解和记忆的地方进行梳理。大家看得话也是一样,把握重点!

jvm整体把握

1、jvm整体结构

在这里插入图片描述

上面这个图是jvm整体结构的一个草图。这个图有个大概记忆就可,后面咱们还有更加详细的图需要大家能手绘那种。

有两点需要注意: 1.方法区和堆是多个线程共享的,但是java栈、本地方法栈、程序计数器是每个线程都单独享有一份的. 2.此处的执行引擎你可以认为是和多线程那块的执行引擎是一致的概念,它最重要的工作就是将高级语言转换为机器指令。

2、java代码的执行流程

在这里插入图片描述

Java源代码通过编译器编译为.class字节码文件,然后交给java虚拟机执行

两次编译:
文件——>字节码文件——>机器指令

注意java编译器和执行引擎中的编译器的区别:java编译器称为前端编译器、执行引擎中的编译器称为后端编译器 ————————————————————————————————————————————— 注意虚拟机中的解释执行和jit编译执行的关系和区别:解释执行是为了保证程序执行的响应时间,逐行解释执行字节码文件,jit是针对重复出现的热点代码进行优化的,重点关注程序性能。但是两者可不是互相补充的关系,他们每一个离开另外一个仍可以独立支撑工作(将字节码转换为机器指令)。

3、Jvm的生命周期

jvm的生命周期分为以下几个部分:

虚拟机启动:

java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)完成的,这个类是由虚拟机的具体实现完成的。

虚拟机执行:
  • 一个运行中的java虚拟机有一个清晰的任务,执行java程序。
  • 程序开始执行虚拟机就运行,程序结束虚拟机就停止。
  • 执行一个所谓的java程序的时候,真正在执行的是一个叫做java虚拟机的进程。
虚拟机退出: 如下几种情况会导致虚拟机的退出.
  • 程序正常执行结束
  • 程序在执行过程中遇到了错误或者异常而终止。
  • 操作系统出现错误导致了java虚拟机进程终止。
  • 有线程调用Runtime类或者System类的exit方法,或Runtime类的halt方法,并且java安全管理器允许这次exit或者halt操作。
  • 暂未想到的其它情形。

类加载子系统

在这里插入图片描述

从jvm的整体结构图中不难看出,大家编写的程序想要被虚拟机进行一系列的后续处理。必须要将. class文件进行加载,这个扮演加载角色的就是咱们的类加载子系统。我们将用比较长的时间来剖析。

1、类加载子系统作用

在这里插入图片描述

  • 类加载子系统负责从文件系统或者网络中加载class文件,class文件在开头有特定的文件标识。
  • classLoader只负责class文件的加载,至于它是否可以运行,则有执行引擎决定。
  • 加载的类信息存放在一块被称为方法区的内存空间。除了类的信息以外,方法区中还存放运行时的常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件常量池部分的内存映射)

请记住: ClassLoader加载calss文件除了将类的信息加载到方法区中,还会存放运行时的常量池信息。

在这里插入图片描述

2、类加载器ClassLoader

在这里插入图片描述

  • classs file存在于本地硬盘上,可以理解为设计师画在纸上的模板,可以理解为设计师画在纸上的模板。但是模板在执行的时候需要加载到jvm当中,以便根据这个模板实例出n个一摸一样的实例。
  • class file加载到jvm中,被称为DNA元数据模板,放在方法区中。
  • 在.class文件——>jvm——>最终成为元数据模板的过程中需要一个运输工具,他就是类加载器(ClassLoader)!

3、类的加载过程

类的加载过程就是类加载子系统装载.class文件的过程,实际上你可以理解为直接编译拿到的.class文件还不能用,我们需要将它生成大的Class实例,且将这个类的常量池、类信息等装载到jvm中。这个过程很重要,咱们一点点说:

在这里插入图片描述


加载——loading:

  1. 加载的第一步通过类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。

链接——linking

链接过程之前咱们先明白两个概念。啥叫类变量和实例变量

  • 类变量:有static修饰的,称为类变量(静态变量)。
  • 实例变量:没有static修饰的,这些成员变量是对象中的成员,称为实例变量。

在这里插入图片描述


链接阶段你要有印象的,最重要的,此时类变量会被分配内存并且设置该类的认初始值,即零值,例如int类型的类变量此时被赋值为0,引用类型的类变量被赋值为null。

初始化——initial

在这里插入图片描述


下面咱们通过具体的例子来看下,在类加载子系统的加载下类变量都会发生什么。

public class ClassClinitTest {

    static {
        number = 20;
    }

    private static int number = 10;

    public static void main(String[] args) {
        System.out.println(number);
    }
}

最终main()方法输出的number的值是10,当然我们都能猜出来。结合咱们刚刚说的类加载子系统对这个类ClassClinitTest的初始化,这个number的值经历了几次变化呢?

首先,linking阶段中的prepare阶段,此时静态的变量被赋予初始值,那么number=10,然后linking阶段走完之后来到初始化阶段,此时按照代码的顺序,static先执行然后变量的定义语句执行,所以最终number=10;

_________________________________________________________________________

再来看下一个例子,注意,子类的clinit方法执行之前,虚拟机会保证他的父类的clinit方法一定已经执行了。

public class ClInitTestOne {
    static class Father {
        public static int A = 1;

        static {
            A = 2;
        }
    }

    static class Son extends Father {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Son.B);
    }
}

那么最终打印出的Son中的类变量B的值是多少呢?容易知道在linking中的prepare阶段B将被赋予0,然后Son的ClInit方阿飞执行之前父类Father的Clint方法会率先执行,因此最终B的值是2。

4、ClassLoader家族介绍

其实上面类加载子系统对.class文件做的一系列的装载工作,包括准备、链接、初始化,都是由具体的ClassLoader进行的。

在这里插入图片描述


在这里插入图片描述


下面尝试获取下各层classLoader.

在这里插入图片描述


系统类加载器和扩展类加载器都能获取到,但是尝试获取引导类加载器的时候却发现获取不到,这是为啥呢?
引导类加载器bootStrapClassLoader是不能靠上述方式获取的,系统核心类库如java.lang.String都是使用bootStrapClassLoader进行加载的.引导类加载器是使用c/c++语言编写的。

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


注意。除了启动类加载器BootStrapClassLoader,剩余所有的加载器均继承自ClassLoader。

5、双亲委派机制理解

java虚拟机对class文件采取的是按需加载的方式,也就是说当真正需要使用该类的时候才会将它的class文件加载到内存中生成class对象。并且加载某个类的class文件时,java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。
双亲委派机制的工作原理:

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
  2. 如果父类的加载器不能执行加载,但是父类的加载器还存在父类,那么继续向上委托,依次递归,请求将最终到达顶层的启动类加载器。
  3. 如果父类加载器可以完成加载任务,那么就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

    在这里插入图片描述


    我们举个例子说明下:

    在这里插入图片描述


    我们创建一个包,叫java.lang,这个包下创建一个类,叫做String。然后咱们来玩个好玩的,请看下图:

在这里插入图片描述


咱们尝试执行自定义的String类中的main()方法,却显示不能执行,报错的原因就涉及到双亲委派机制了。找到顶层的启动类加载器bootStrapClassLoader,无法加载你这个类!

在这里插入图片描述

补充

查看类的反编译信息请使用javap 命令
例如现在定义Test类如下:

在这里插入图片描述


cmd到这个类的.class文件夹下,然后执行Javap -v Test.class。可以看到相关的常量池信息。

在这里插入图片描述


还有别的一些很有趣的,例如方法的具体执行过程,都可以看到。

在这里插入图片描述

总结

本篇到此!大家看得开心!

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐