我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java虚拟机之类加载

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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类加载内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java虚拟机之类加载

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java虚拟机之类加载的示例分析

小编给大家分享一下Java虚拟机之类加载的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!Java是什么Java是一门面向对象编程语言,可以编写桌面应用程序
2023-06-15

JAVA虚拟机(JVM)详细介绍(五)——类加载机制

上篇中我们讲解了Class文件,这篇我们说说虚拟机是如何加载这些Class文件的?Class文件中的信息进入到虚拟机后会发生什么变化?这就涉及到了类加载机制。类加载机制是把类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚
JAVA虚拟机(JVM)详细介绍(五)——类加载机制
2015-12-28

Java高级之虚拟机加载机制的示例分析

这篇文章给大家分享的是有关Java高级之虚拟机加载机制的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。Jvm要加载的是二进制流,可以是.class文件形式,也可以是其他形式,按照它加载的标准来设计就不会有
2023-05-30

如何深入理解Java虚拟机JVM类加载初始化

如何深入理解Java虚拟机JVM类加载初始化,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。1. Classloader的作用,概括来说就是将编译后的class装
2023-06-17

Java虚拟机类装载:原理、实现与应用

Java虚拟机类装载:原理、实现与应用[@more@]  一、引言   Java虚拟机(JVM)的类装载就是指将包含在类文件中的字节码装载到JVM中, 并使其成为JVM一部分的过程。JVM的类动态装载技术能够在运行时刻动态地加载或者替换系统
2023-06-03

Java类加载机制及类加载器详解

熟悉java开发的同学都知道,我们日常所写的代码都被保存到.java文件中。这些\".java\"文件经过Java编译器编译成拓展名为\".class\"的文件,\".class\"文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟

Java的类加载机制

很长一段时间里,我对 Java 的类加载机制都非常的抗拒,因为我觉得太难理解了。但为了成为一名优秀的 Java 工程师,我决定硬着头皮研究一下。01、字节码在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。计算机只认
Java的类加载机制
2018-10-24

Java 类加载机制究竟是什么?(Java类加载机制是什么)

在Java编程中,类加载机制是一个非常重要的概念,它对于Java程序的运行和性能有着深远的影响。本文将详细介绍Java类加载机制的工作原理、过程以及相关的重要概念。一、类加载机制的概述类加载机制是Java虚拟机(
Java 类加载机制究竟是什么?(Java类加载机制是什么)
Java2024-12-22

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录