图解JVM(一)—— 概述&类加载子系统

概述

​ 初期学习JVM时,总有两个感觉,一是看不懂,二是很枯燥,同时还会觉得这和我们平时写代码有什么关系。然而面试的时候,面试官总是各出花招的问你JVM相关问题,很扎心。我在学习的过程中,慢慢总结出来一些原因,主要是JVM内容的知识很深,很广,很杂,如果不花费大量连贯的时间学习,每次学习到的JVM知识总是九牛一毛,并且是孤立的知识,很快便遗忘了。那我想着如何加深印象呢,就通过图的方式,从大到小的方式学习记录,以求有深刻的记忆。

图解JVM

废话不多说,直接上图,下图应该屡见不鲜了,JVM加载class文件,运行的流程图。真心建议,如果是JVM小白的话,学习JVM就应该从下图开始学习。
在这里插入图片描述

为什么要从上图开始学习呢?我觉得是我们首先得对JVM有一个整体的理解,然后再分块到细节学习。我以前就是从运行时数据区的虚拟机栈开始学习,学着学着疑惑越来越多,反而无所适从。所以无论学习哪一块内容时,心里都得记着这个流程,清楚每一块的大致作用是什么,这样才能学得更清楚。

那我们现在来看,一个Java文件的执行流程:

  1. 向JVM输入Class files
  2. JVM通过类加载子系统,将文件加载到内存
  3. JVM将内存区域在逻辑上分成多块进行管理
  4. 执行引擎执行内存区域的指令

此流程主要分为4块

  1. Class files(jvm字节码文件)
  2. Class Loader Sub System(负责将jvm字节码文件加载到内存)
  3. Runtime Data Area(运行时数据区:存储和管理字节码文件所产生的数据信息)
  4. execute engine(执行引擎:相当于CPU,执行指令)

至此,希望已经对JVM的流程有一个大致的了解。本次我们先重点学习一下Class files和 Class Loader Sub System。

Class Files

Class Files,又叫做jvm字节码,最常见的是Java字节码,我们在idea项目中的target目录下,或者把jar解压开来看到的.class文件都是Java字节码。那为什么我们叫jvm字节码,而不是Java字节码呢?因为jvm支持各种语言编译成的符合jvm规范的字节码,而不仅仅是Java,当然Java字节码是最广泛的。那我们来看看jvm字节码的模样。

// java 代码    
public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
    }

// java字节码
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: return

上面的字节码是将class文件通过javap进行反编译后的代码(真正的字节码是16进制流),可以看到基本指令是iconst,istore,iload等,看起来和汇编语言有些类似。但其两者有一些区别。JVM支持的指令流是基于栈的指令集架构,而汇编是基于寄存器的指令集架构,其两者区别如下:

基于栈式架构

  1. 设计和实现简单,适用于资源受限的系统
  2. 指令集中的指令基本是零地址指令,指令集更小,编译器容易实现
  3. 不需要硬件支持,可移植性更好,更好实现跨平台

基于寄存器架构

  1. 依赖硬件,可移植性差
  2. 性能优秀,执行高效
  3. 花费更少的指令完成一项操作,以1,2,3地址指令 为主。

所以为了更好的跨平台,Java的指令都是根据栈来设计

类加载子系统(Class Loader Sub System)

先通过图 了解下类加载子系统加载的整体流程。
在这里插入图片描述

作用:类加载子系统负责从文件系统或者网络中加载Class文件,加载的类信息会存放在方法区的内存空间,除了类的信息外,方法区中还存放运行时常量信息,可能还包括字符串字面量和数字常量。其加载过程分为三步:加载,链接,初始化。链接又分为三步:验证,准备和解析。每个步骤的简单介绍都已经在图中了。后面就相对深入一下,一时如果无法消化,建议留个印象,后面会慢慢加深和对应起来。

加载(Loading)

简单来说是指查找字节流,并据此创建类的过程。详细:通过类的全限定名(路径和文件名)获取此类的二进制流,将二进制字节流的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个java.lang.Class对象,作为这个类的数据的访问入口。

上述过程通过**类加载器(ClassLoader)**完成。类加载器分为启动类加载器(bootstrap),扩展类加载器(extention),应用类加载器(application)和自定义加载器。

启动类加载:由C++实现,在Java中用null来指代,只加载最基础最重要的类,例如JRE的lib目录下jar包中的类以及有虚拟机参数-Xbootclasspath指定的类。

扩展类加载器:其父类是启动类加载器,加载相对次要但通用的类,例如JRE的lib/ext目录下jar包的类,以及java.ext.dirs指定的类。(java 9 之后,改名为平台类加载器,加载更多类)

应用类加载器:其父类是扩展类加载器,加载应用程序路径下的类。

自定义加载器:实现特殊的加载方式,例如对class文件加密,加载时利用自定义的类加载器进行解密后加载。

双亲委派模型

JVM对class文件采用按需加载的方式,加载class文件时采用的双亲委派模型,如下图
在这里插入图片描述

当收到类加载请求,会先查找是否加载过,若加载过,直接返回,反之不会自己先去加载,而是把这个请求委托给父类加载器,若父类加载器还存在父类加载器,进一步向上委托,最终可能会到顶层的启动类加载器进行加载。若父类加载器可以完成类加载,则成功返回,若无法完成,则子加载器会尝试加载。看下源码就很清晰了。

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

优点:假设攻击者想篡改系统级别的类,例如String.class,但是呢启动类加载器已经加载了String.class对象,便不会再加载篡改后的,保证了一定的安全性;避免重复加载,父加载器加载过的class对象,不会再次加载。

PS:在JVM虚拟机中,类的唯一性由类加载器实例和类的全名一同确定,哪怕相同的class文件,由不同的类加载器加载,也会得到两个不同的类。

链接(LINKING)

链接分为三个步骤,分别是验证,准备,解析

验证(Verify)

确保class文件的字节流中包含的信息符合虚拟机要求,保证被加载类的正确性,不会危害虚拟机。包含四种验证:文件格式验证(所有class文件的开头的都是cafe babe,叫做魔数),元数据验证,字节码验证,符号引用验证。

准备(prepare)

为类变量(以static修饰的变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

  1. 不会为实例变量分配初始化,而实例变量会随着对象分配在堆中
  2. 设置初始值,会设为默认的零值

为类静态变量分配内存并且设置该类变量的默认初始值,即零值。不包含用static final 修饰的常量,static final在编译时候就分配了。不会为实例变量分配初始化,类静态变量会分配方法区中,而实例变量会随着对象分配在堆中。

解析(Resolve)

将常量池内的符号引用转换为直接引用。

解析操作往往伴随JVM执行完初始化之后再执行

初始化(initialize)

执行()方法的过程。

1.此方法是javac编译器自动收集类中所有类变量的赋值操作和静态代码块中的语句合并而来。()方法中的指令按照语句在原文件中出现的顺序执行。不同于构造器方法()方法(类加载完成后,创建对象时候将调用的 ()方法来初始化对象)

public class Test {
    static {
        // 给变量赋值可以正常编译通过
        i = 0;
        // 这句编译器会提示"非法向前引用"
        System.out.println(i);
    }

    static int i = 1;
}

2.若该类包含父类,JVM会保证再执行子类()方法前,先调用父类的()方法。

3.JVM会保证在多线程下一个类的()方法同步加锁。

总结

jvm是一个基于栈的指令集架构的应用程序,其作用是运行jvm字节码,其功能主要分为两点:

  1. write once,run anywhere:在不同的硬件平台都有对应的jvm程序,字节码可以运行在任意平台的jvm中。
  2. 提供内存管理和垃圾回收等功能

附录:发展历程

Sun Classic VM : Java1.0发布,世界上第一款商用Java虚拟机,只提供解释器

Exact VM : jdk1.2时发布,提供了准确式内存管理,热点探测及编译器与解释器混合工作模式,短暂使用,很快就被HotSpot替换

HotSpot:jdk1.3时发布,目前JDK6,8等新版本默认虚拟机都是HotSpot,HotSpot含义就是热点代码探测技术。(三大JVM之一

JRockit:BEA公司发明,已被oracle收购,专注于服务器端应用,不包含解释器,是世界上最快的JVM。JRockit Real Time(毫秒或微妙级响应)&MissionControl(极低开销来监控,管理和分析生产环境的工具)是其特点。(三大JVM之一

J9:IBM发布,仅广泛应用于IBM的各种Java产品,移植性较差。(三大JVM之一

KVM,CDC/CLDC HotSpot:Oracle应用在移动领域的Java虚拟机,已几乎不用;Azul VM:应用在Azul Systems的专有硬件Vega系统上。Liquid VM:运用在BEA公司Hypervisor系统,实现了操作系统的必要功能,可直接与硬件交互,目前已停止。Apache Harmony:IBM和Intel联合开发,但Sun公司不让其获得JCP认证,最终退役。最后被应用在了Android SDK中。MicroSoft JVM:微软用以支持浏览器中的Java程序,只能在windows平台运行,但遭Sun公司指控,被迫抹掉。目前windows上都是HotSpot。

Taobao JVM:阿里基于OpenJDK,深度定制且开源的高性能服务器版Java虚拟机。GCIH技术实现了off-heap:将生命周期长的Java对象从heap中移到了heap之外,且GC不管理此类对象,降低回收频率和提高回收效率,heap之外的对象还能在多个Java虚拟机进程中实现共享。降低JNI开销…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值