Java虚拟机之类加载

一、类加载流程

类加载的流程可以简单分为三步:

  • 加载
  • 连接
  • 初始化

而其中的连接又可以细分为三步:

  • 验证
  • 准备
  • 解析

下面会分别对各个流程进行介绍。

1.1 类加载条件

在了解类接在流程之前,先来看一下触发类加载的条件。

JVM不会无条件加载类,只有在一个类或接口在初次使用的时候,必须进行初始化。这里的使用是指主动使用,主动使用包括如下情况:

  • 创建一个类的实例的时候:比如使用new创建,或者使用反射、克隆、反序列化
  • 调用类的静态方法的时候:比如使用invokestatic指令
  • 使用类或接口的静态字段:比如使用getstatic/putstatic指令
  • 使用java.lang.reflect中的反射类方法时
  • 初始化子类时,要求先初始化父类
  • 含有main()方法的类

除了以上情况外,其他情况属于被动使用,不会引起类的初始化。

比如下面的例子:

public class Main {
    public static void main(String[] args){
        System.out.println(Child.v);
    }
}

class Parent{
    static{
        System.out.println("Parent init");
    }
    public static int v = 100;
}

class Child extends Parent{
    static {
        System.out.println("Child init");
    }
}

输出如下:

Parent init
100

而加上类加载参数-XX:+TraceClassLoading后,可以看到Child确实被加载了:

[0.068s][info   ][class,load] com.company.Main
[0.069s][info   ][class,load] com.company.Parent
[0.069s][info   ][class,load] com.company.Child
Parent init
100

但是并没有进行初始化。另外一个例子是关于final的,代码如下:

public class Main {
    public static void main(String[] args){
        System.out.println(Test.STR);
    }
}

class Test{
    static{
        System.out.println("Test init");
    }
    public static final String STR = "Hello";
}

输出如下:

[0.066s][info   ][class,load] com.company.Main
Hello

Test类根本没有被加载,因为final被做了优化,编译后的Main.class中,并没有引用Test类:

0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #4                  // String Hello
5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

在字节码偏移3的位置,通过ldc将常量池第4项入栈,此时在字节码文件中常量池第4项为:

#3 = Class              #24            // com/company/Test
#4 = String             #25            // Hello
#5 = Methodref          #26.#27        // java/io/PrintStream.println:(Ljava/lang/String;)V

因此并没有对Test类进行加载,只是直接引用常量池中的常量,因此输出没有Test的加载日志。

1.2 加载

类加载的时候,JVM必须完成以下操作:

  • 通过类的全名获取二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构
  • 创建java.lang.Class类的实例,表示该类型

第一步获取二进制数据流,途径有很多,包括:

  • 字节码文件
  • JAR/ZIP压缩包
  • 从网络加载

等等,获取到二进制数据流后,JVM进行处理并转化为一个java.lang.Class实例。

1.3 验证

验证的操作是确保加载的字节码是合法、合理并且规范的。步骤简略如下:

  • 格式检查:判断二进制数据是否符合格式要求和规范,比如是否以魔数开头,主版本号和小版本号是否在当前JVM支持范围内等等
  • 语义检查:比如是否所有类都有父类存在,一些被定义为final的方法或类是否被重载了或者继承了,是否存在不兼容方法等等
  • 字节码验证:会试图通过对字节码流的分析,判断字节码是否可以正确被执行,比如是否会跳转到一条不存在的指令,函数调用是否传递了正确的参数等等,但是却无法100%判断一段字节码是否可以被安全执行,只是尽可能检查出可以预知的明显问题。如果无法通过检查,则不会加载这个类,如果通过了检查,也不能说明这个类完全没有问题
  • 符号引用验证:检查类或方法是否确实存在,并且确定当前类有没有权限访问这些数据,比如无法找到一个类就抛出NoClassDefFoundError,无法找到方法就抛出NoSuchMethodError

1.4 准备

类通过验证后,就会进入准备阶段,在这个阶段,JVM为会类分配相应的内存空间,并设置初始值,比如:

  • int初始化为0
  • long初始化为0L
  • double初始化为0f
  • 引用初始化为null

如果存在常量字段,那么这个阶段也会为常量赋值。

1.5 解析

解析就是将类、接口、字段和方法的符号引用转为直接引用。符号引用就是一些字面量引用,和JVM的内存数据结构和内存布局无关,由于在字节码文件中,通过常量池进行了大量的符号引用,这个阶段就是将这些引用转为直接引用,得到类、字段、方法在内存中的指针或直接偏移量。

另外,由于字符串有着很重要的作用,JVMString进行了特别的处理,直接使用字符串常量时,就会在类中出现CONSTANT_String,并且会引用一个CONSTANT_UTF8常量项。JVM运行时,内部的常量池中会维护一张字符串拘留表(intern),会保存其中出现过的所有字符串常量,并且没有重复项。使用String.intern()可以获得一个字符串在拘留表的引用,比如下面代码:

public static void main(String[] args){
    String a = 1 + String.valueOf(2) + 3;
    String b = "123";
    System.out.println(a.equals(b));
    System.out.println(a == b);
    System.out.println(a.intern() == b);
}

输出:

true
false
true

这里b就是常量本身,因此a.intern()返回在拘留表的引用后就是b本身,比较结果为真。

1.6 初始化

初始化阶段会执行类的初始化方法<clint><clint>是由编译期生成的,由静态成员的赋值语句以及static语句共同产生。

另外,加载一个类的时候,JVM总是会试图加载该类的父类,因此父类的<clint>方法总是在子类的<clint>方法之前被调用。另一方面,需要注意的是<clint>会确保在多线程环境下的安全性,也就是多个线程同时初始化同一个类时,只有一个线程可以进入<clint>方法,换句话说,在多线程下可能会出现死锁,比如下面代码:

package com.company;

import java.util.concurrent.TimeUnit;

public class Main extends Thread{
    private char flag;
    public Main(char flag){
        this.flag = flag;
    }

    public static void main(String[] args){
        Main a = new Main('A');
        a.start();
        Main b = new Main('B');
        b.start();
    }

    @Override
    public void run() {
        try{
            Class.forName("com.company.Static"+flag);
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

class StaticA{
    static {
        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        try{
            Class.forName("com.company.StaticB");
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        System.out.println("StaticA init ok");
    }
}

class StaticB{
    static {
        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        try{
            Class.forName("com.company.StaticA");
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        System.out.println("StaticB init ok");
    }
}

在加载StaticA的时候尝试加载StaticB,但是由于StaticB已经被加载中,因此加载StaticA的线程会阻塞在Class.forName("com.company.StaticB")处,同理加载StaticB的线程会阻塞在Class.forName("com.company.StaticA")处,这样就出现死锁了。

二、ClassLoader

2.1 ClassLoader简介

ClassLoader是类加载的核心组件,所有的Class都是由ClassLoader加载的,ClassLoader通过各种各样的方式将Class信息的二进制数据流读入系统,然后交给JVM进行连接、初始化等操作。因此ClassLoader负责类的加载流程,无法通过ClassLoader改变类的连接和初始化行为。

ClassLoader是一个抽象类,提供了一些重要接口定义加载流程和加载方式,主要方法如下:

public Class<?> loadClass(String name) throws ClassNotFoundException:给定一个类名,加载一个类,返回这个类的Class实例,找不到抛出异常

protected final Class<?> defineClass(byte[] b, int off, int len):根据给定字节流定义一个类,offlen表示在字节数组中的偏移和长度,这是一个protected方法,在自定义子类中才能使用

protected Class<?> findClass(String name) throws ClassNotFoundException:查找一个类,会在loadClass中被调用,用于自定义查找类的逻辑

protected Class<?> findLoadedClass(String name):寻找一个已经加载的类

2.2 类加载器分类

在标准的Java程序中,JVM会创建3类加载器为整个应用程序服务,分别是:

  • 启动类加载器:Bootstrap ClassLoader
  • 扩展类加载器:Extension ClassLoader
  • 应用类加载器(也叫系统类加载器):App ClassLoader

另外,在程序中还可以定义自己的类加载器,从总体看,层次结构如下:

一般来说各个加载器负责的范围如下:

  • 启动类加载器:负责加载系统的核心类,比如rt.jar包中的类
  • 扩展类加载器:负责加载lib/ext/*.jar下的类
  • 应用类加载器:负责加载用户程序的类
  • 自定义加载器:加载一些特殊途径的类,一般是用户程序的类

2.3 双亲委派

默认情况下,类加载使用双亲委派加载的模式,具体来说,就是类在加载的时候,会判断当前类是否已经被加载,如果已经被加载,那么直接返回已加载的类,如果没有,会先请求双亲加载,双亲也是按照一样的流程先判断是否已加载,如果没有在此委托双亲加载,如果双亲加载失败,则会自己加载。

在上图中,应用类加载器的双亲为扩展类加载器,扩展类加载器的双亲为启动类加载器,当系统需要加载一个类的时候,会先从底层类加载器开始进行判断,当需要加载的时候会从顶层开始加载,依次向下尝试直到加载成功。

在所有加载器中,启动类加载器是最特别的,并不是使用Java语言实现,在Java中没有对象与之相对应,系统核心类就是由启动类加载器进行加载的。换句话说,如果尝试在程序中获取启动类加载器,得到的值是null

System.out.println(String.class.getClassLoader() == null);

输出结果为真。

到此这篇关于Java虚拟机之类加载的文章就介绍到这了,更多相关JVM类加载内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈JVM核心之JVM运行和类加载

    前言 本篇博客将写一点关于JVM的东西,涉及JVM运行时数据区.类加载的过程.类加载器.ClassLoader.双亲委派机制.自定义类加载器等,这些都是博主自己的一点理解,如果有误,欢迎大家评论拍砖~ 关于JVM运行时数据区 JVM运行时数据区 关于类加载 class文件加载至内存,链接(校验.解析),初始化:最终形成JVM可以直接使用的JAVA类型的过程. 加载:在方法区形成类的运行时数据结构:在堆里面形成该类的Class对象,作为访问方法区的入口. 加载 链接:class文件是否存在问题:一

  • JVM类加载机制详解

    一.先看看编写出的代码的执行过程: 二.研究类加载机制的意义 从上图可以看出,类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行. 研究类加载机制的第二个目的是让程序能动态的控制类加载,比如热部署等,提高程序的灵活性和适应性. 三.类加载的一般过程 原理:双亲委托模式 1.寻找jre目录,寻找jvm.dll,并初始化JVM: 2.产生一个Bootstrap Loader(启动类加载器): 3.Bootstrap Loader自动加载E

  • 通俗讲解JVM的类加载机制

    前言 我们很多小伙伴平时都是做JAVA开发的,那么作为一名合格的工程师,你是否有仔细的思考过JVM的运行原理呢. 如果懂得了JVM的运行原理和内存模型,像是一些JVM调优.垃圾回收机制等等的问题我们才能有一个更清晰的概念. 为了走进JVM,深入了解底层,王子打算写一个JVM的专题,留下自己对JVM探索的足迹,同时也希望能帮到小伙伴们更好的理解JVM. 那我们开始吧. JAVA代码的运行流程 首先我们就来聊一聊JAVA代码是怎么运行起来的,这部分比较基础相信大家都知道,就当成是个复习吧. 我们编写

  • jvm之java类加载机制和类加载器(ClassLoader)的用法

    当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载.连接.初始化3个步骤来对该类进行初始化.如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化. 一.类加载过程 1.加载 加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象. 类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础

  • 深入理解JVM之类加载机制详解

    本文实例讲述了深入理解JVM之类加载机制.分享给大家供大家参考,具体如下: 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 与那些在编译时需要进行链接工作的语言不同,在Java语言里,类型的加载.连接和初始化过程都是在程序运行期间完成的,例如import java.util.*下面包含很多类,但是,在程序运行的时候,虚拟机只会加载哪些我们程序需要的类.这种策略虽然会令类加载时稍微增加

  • 浅谈Java自定义类加载器及JVM自带的类加载器之间的交互关系

    JVM自带的类加载器: 其关系如下: 其中,类加载器在加载类的时候是使用了所谓的"父委托"机制.其中,除了根类加载器以外,其他的类加载器都有且只有一个父类加载器. 关于父委托机制的说明: 当生成 一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器将成为该类加载器的父类加载器 下面,自定义类加载器.自定义的类加载器必须继承java.lang.ClassLoader类 import java.io.*; public class MyClassLoader extend

  • JVM的类加载过程以及双亲委派模型详解

    jvm 的主要组成部分 类加载器(ClassLoader) 运行时数据区(Runtime Data Area) 执行引擎(Execution Engine) 本地库接口(Native Interface) jvm 运行时数据区的组成 方法区: ①方法区主要用来存储已被虚拟机加载的类信息(构造器,接口定义).常量.静态变量和运行时常量池等数据. ②该区域是被线程共享的. ③方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用.该常量池具有动态性,也就是说常量并不一定是编译时确定,运行

  • 详解JVM类加载机制及类缓存问题的处理方法

    前言 大家应该都知道,当一个Java项目启动的时候,JVM会找到main方法,根据对象之间的调用来对class文件和所引用的jar包中的class文件进行加载(其步骤分为加载.验证.准备.解析.初始化.使用和卸载),方法区中开辟内存来存储类的运行时数据结构(包括静态变量.静态方法.常量池.类结构等),同时在堆中生成相应的Class对象指向方法区中对应的类运行时数据结构. 用最简单的一句话来概括,类加载的过程就是JVM根据所需的class文件的路径,通过IO流的方式来读取class字节码文件,并通

  • 一文读懂Jvm类加载机制

    前言 一个月没更新了,这个月发生了太多的事情,导致更新的频率大大降低,不管怎样收拾心情,技术的研究不能落下! jvm作为每个java程序猿必须了解的知识,博主推荐一本书<深入理解Java虚拟机>,以前博主在学校的时候看过几遍,每一次看都有新的理解.加上工作了也有一年多的时间了,有必要好好总结一番~ 什么是jvm 平常我们编写代码都是编写的.java文件,怎么部署到机器上运行呢?通过打jar包或者war包,然后部署运行. 如果看过jar包的内容那么就能知道,我们写的.java文件全部被编译成了.

  • JVM核心教程之JVM运行与类加载全过程详解

    为什么要使用类加载器? Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性.例如: 1.编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类: 2.用户可以自定义一个类加载器,让程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分:(这个是Android插件化,动态安装更新apk的基础) 为什么研究类加载全过程? 有助于连接JVM运行过程 更深入了解java动态性(解热部署,动态加载),提高程

  • jvm类加载器基础解析

    [类加载器简介] 类加载器(classloader)用于将类的class文件加载到JVM虚拟机.JVM有三种加载器,引导类加载器器(bootstrapclassloader).扩展类加载器(extensionsclassloader)和应用类加载器(applicationclassloader),另外还可以继承java.lang.ClassLoader类创建自定义加载器. [类加载器种类] 1.引导类加载器(BootStrap):并不是一个Java类,采用C++语言编写.内嵌在JVM内核里面,使

  • JVM中ClassLoader类加载器的深入理解

    JVM的体系结构图 先来看一下JVM的体系结构,如下图: JVM的位置 JVM的位置,如下图: JVM是运行在操作系统之上的,与硬件没有直接的交互,但是可以调用底层的硬件,用JIN(Java本地接口调用底层硬件) JVM结构图中的class files文件 class files文件,是保存在我们电脑本地的字节码文件,.java文件经过编译之后,就会生成一个.class文件,这个文件就是class files所对应的字节码文件,如下图: JVM结构图中的类加载器ClassLoader的解释 类加

  • jvm运行原理以及类加载器实例详解

    JVM运行原理 首先从".java"代码文件,编译成".class"字节码文件,然后类加载器将".class"字节码文件中的类给加载带JVM中,最后就是JVM执行写好的代码.执行过程如下图 类加载器 类加载过程 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载 加载 一旦JVM进程启动之后,一定会先把类加载到内存中,然后从main()方法的入口代码开始执行 public class

  • JVM类加载机制原理及用法解析

    一.JVM 类加载机制 JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 1. 加载: 加载是类加载过程中的第一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口.注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的

随机推荐