jvm与类加载器简要概述

377 阅读13分钟

1.虚拟机:就是一台虚拟的计算机,本质仍是一款用于模拟独立计算机并执行相关虚拟计算机指令的软件,可分为两种,如下:

  • 系统虚拟机:它是完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台,如VMware、Visual Box;
  • 程序虚拟机:它专门为执行单个计算机程序而设计,提供了与这个计算机程序相适应的虚拟操作系统环境,便于该程序达成某些目的,典型代表就是JVM,在JVM中执行的指令为Java字节码指令;

但无论是系统虚拟机还是程序虚拟机,在上面运行的软件都受限于虚拟机提供的资源。

2.JVM总述:是一种用于计算设备的规范,它是一个虚构的基于堆栈的计算机,它借助符号引用扩展了虚拟机的实现方式,而垃圾回收使得空间利用率更高,JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行,字节码文件用网络文字顺序存储,使虚拟机不用考虑平台是大端还是小端编码。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法区。

3.JVM的结构

  • 上层:包含类的加载器、连接器等,主要用于加载类,并创建对应的类对象;
  • 中层:包含堆、元数据区、本地栈、虚拟栈、程序计数器,主要负责存储数据;
  • 下层:包含执行引擎、本地方法的接口与库,主要负责解释执行相应的指令;

image.png

4.JVM的架构模型:JVM输入的指令流基本上是一种基于栈的指令集架构,另外一种是基于寄存器的指令集架构,两者区别如下:

  • 基于栈式架构的特点
    • 容易实现;
    • 性能较低;
    • 指令集更小,但完成同一个操作需要更多条指令;
    • 跨平台;
  • 基于寄存器架构的特点
    • 对平台依赖性高;
    • 实现困难;
    • 指令集丰富,能通过一些基础指令实现复杂功能;
    • 性能好;

5.JVM的生命周期

  • 启动:有引导类加载器(bootstrap class loader) 创建一个初始类来完成的,这个初始类受虚拟机的实现影响,而任何一个拥有main方法的类都可以作为JVM实例运行的启点;
  • 运行: 以main函数为起点,程序中的其他线程均由它启动,包括守护线程如JVM自己的GC线程和普通线程如main方法线程,运行时JVM就是一个进程;
  • 消亡:所有线程终止,JVM实例结束生命,JVM消亡会在如下几种情况发生:
    1. 执行了System.exit()方法;
    2. 程序正常执行结束;
    3. 程序遇到异常或错误而异常终止;
    4. 由于操作系统出现的错误或命令使Java虚拟机进程停止运行;

6.常见的虚拟机

  1. HotSpot特点
    • 适用于服务器、客户端等多个平台,执行引擎中解释器和及时编译器可以同时运行,协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡;
    • 实现了热点代码探测技术,通过计数器找到最具编码价值的代码,触发及时编译或栈上替换;
    • 拥有方法区;
  2. JRockit特点
    • 专注于服务器端应用,不太关注程序启动速度,因此不含解析器,全部代码靠即时编译器编译后执行;
    • 由于主要关注服务器端,对服务启动做了很多优化,使得其成为世界上最快的JVM;
    • 在JDK8的时候与HotSpot整合在了一起;
    • 无方法区;
  3. J9特点
    • 定位与HotSpot类似,广泛应用IBM的各种Java产品上,由于是IBM自身设置的,所以这种虚拟机在IBM的硬件上运行速度很快;
    • 无方法区;

7.JDK、JRE、JVM三者关系

  1. JDK:Java语言的软件开发工具包(SDK),是物理存在,包括programming tools、JRE;
  2. JRE:Java运行时环境,是物理存在,主要由Java API和JVM组成,提供了用于执行Java应用程序所需的最低环境;
  3. JVM:一种用于计算机设备的规范,它是一个虚构的计算机软件实现,简单的来说JVM是运行字节码程序的一种容器;

8.类加载子系统的功能

  • 负责从文件系统或网络中加载并检查该class文件;
  • 加载的类信息会存放在一块称为方法区(元数据区)的内存空间中,除了类信息外,常量池的信息也会存放在方法区中,常量池中常包含数字常量与字符串字面量;

注意:Classloader只负责加载及检测class文件是否符合规范,至于其是否能够运行则由Execution Engine决定。

9.类的加载器:负责加载程序中的类型(类和接口),并赋予唯一的名字予以标识

  • 特点:

    • 动态加载:Java的类加载器是在运行时加载类的,而非编译时;
    • 层级结构:Java里的类装载器被组织成了有父子关系的层级结构,Bootstrap类装载器是所有装载器的父亲;
    • 代理模式:基于层级结构,类的代理可以在加载器之间进行代理,当加载器装载一个类时,首先会检查它在父加载器中是否进行了加载,如果上层装载器已经加载了这个类,这个类会被直接使用,反之,类加载器会请求装载这个类;
    • 可见性限制:一个子加载器可以查找父加载器中的类,但是父加载器不能查询子加载器里的类;
    • 不允许卸载:类加载器在加载一个类后时不能卸载它的,不过可以删除当前的类加载器然后创建一个新的类加载器去加载;
  • 两种加载器:

    • 引导类加载器:非继承自ClassLoader,常见为引导类加载器,由C++实现,一般Java的核心类都是使用引导类加载器加载的,它在Java虚拟机启动后初始化的,之后会加载ExtClassLoader并将其父加载器设置为自身,加载完扩展类加载其后会加载APPClassLoader并将其父加载器设置为ExtClassLoader;
    • 自定义加载器:继承自ClassLoader,常见有扩展类加载器、系统或应用类加载器、以及我们自定义的类加载器,由Java实现,常用于加载扩展类、用户自定义的类等,如果一个类型是由自定义类加载器加载的,则JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中

10.类加载过程

  1. 加载
    1. 通过一个类的全限定名获取定义此类的二进制字节流,并存放到一个字节数组中;
    2. 解析并检查这些字节是否代表一个class对象并包含正确的major及major版本信息,此阶段检测的是总体结构;
    3. 在内存中创建一个代表这个类或接口的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口;
  2. 连接:
    1. 验证:确保导入类型的准确性,验证阶段做过的检查运行时就不需要再做了,它是类加载中最复杂的过程,花费时间也最长,此阶段检测的是具体细节;
    2. 准备:分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口信息等,在此之间还会给类的成员变量赋予默认值;
    3. 解析:将常量池中的符号引用转换为直接引用
  3. 初始化:
    1. 通过执行类构造方法为类的静态变量赋予正确的初始值
    2. 类的构造方法就是将类的变量初始化语句聚合在一起而成的,如果没有对类变量的初始化也便不会有类的构造方法,构造器方法为;
    3. 虚拟机必须保证一个类的方法在多线程下被同步加锁;

11.直接引用与符号引用

  1. 符号引用:
    • 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可;
    • 符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中;
    • 在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替;
    • 各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中;
  2. 直接引用:
    • 直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同;
    • 如果有了直接引用,那引用的目标必定已经被加载入内存中了;
    • 直接引用可以为:
      1. 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针);
      2. 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量);
      3. 一个能间接定位到目标的句柄;

12.默认值

  1. 局部变量:局部变量声明后,Java虚拟机不会自动给它初始化为默认值。因此对于局部变量,必须经过显示的初始化,才能使用它。如果使用一个没有被初始化的局部变量,编译器会报错;
  2. 类的成员变量:包括静态变量与实例变量,不管程序有没有显示的初始化,Java虚拟机都会先自动给它初始化为默认值;
  3. 数组:由于数组也是类,其父类为Object,其类型在运行时会被动态创建,new一个数组与new一个类相同,数组的成员与类的实例属性类似,所以new一个数组时其成员也会被赋予默认值;
  4. 默认值:
    • byte,short,int,long初值为0;
    • float,double则为0.0;
    • char为空格字符;
    • boolean为false;
    • 包装类及其他引用为null;

13.双亲委派机制具体细节如下

  1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类;
  2. 当前ClassLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到Bootstrap ClassLoader;
  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回;

小结:双亲委托机制的核心思想分为两个步骤,首先自底向上检查类是否已经加载,然后自顶向下尝试加载类。

14.双亲委派相关知识

  1. ClassLoader隔离问题:

    1. 问题产生条件:
      1. 每个类装载器都有一个自己的命名空间用来保存已装载的类,当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名进行搜索来检测这个类是否已经被加载了;
      2. 而JVM对类的唯一性识别是基于ClassLoader id+PackageName+ClassName;
    2. 问题描述:因为以上条件,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的,并且如果这两个类不是由一个ClassLoader加载,是无法将一个类的示例强转为另外一个类的,这就是ClassLoader隔离,双亲委派是可以解决这个问题的;
  2. 沙箱机制:

    1. 沙箱:一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏,沙箱主要限制的是对系统资源的访问

    2. Java安全模型:虚拟机会通过类加载器把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问,虚拟机中不同的受保护域,对应不一样的权限

    3. 沙箱的基本组件

      1. 字节码校验器:确保Java类文件遵循Java语言规范,这样可以帮助Java程序实现内存保护,但并不是所有的类文件都会经过字节码校验,比如核心类
      2. 类加载器:防止恶意代码去干涉善意代码,守护被信任的类库边界,将代码归入保护域,确定了代码可以进行哪些操作,而双亲委派要求类加载要从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用,同时由于严格通过包来区分访问域使得外层恶意的类通过内置代码也无法访问内层类,从而也就无法是破坏代码生效了
      3. 存取控制器:存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定
      4. 安全管理器:实现权限控制,比存取控制器优先级高
      5. 安全软件包:java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括安全提供者、消息摘要、数字签名、加密、鉴别等

15.类的初始化

  1. 类的初始化时机:所有Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化他们(类不会在首次主动使用时才被加载,虚拟机运行预先加载);

  2. 主动使用情形:

    • 创建类的实例;
    • 访问某个类或接口的静态变量,或者对该静态变量赋值, 调用类的静态方法;
    • 反射(如Class.forName("top.vignetting.test”));
    • 初始化一个类的子类( 接口不会);
    • Java虚拟机启动时被标明为启动类的类(含有main方法的类);
    • JDK1.7提供的动态语言支持中一些类没有初始化时会自动初始化;

16.类的实例化

  1. 为实例在堆中分配内存;
  2. 为实例变量赋予默认值;
  3. 为实例变量赋予正确的初始值(类似于类的初始化,赋予正确值是依靠构造方法实现的,此方法会有标注);

原文

博客

本文由博客群发一文多发等运营工具平台 OpenWrite 发布