Java虚拟机类加载机制探究:生命周期、初始化、使用与验证

一、java虚拟机与程序的生命周期

在如下几种情况之下,java虚拟机将结束生命周期:

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或者错误而异常终止
  • 由于操作系统用出现错误而导致java虚拟机进程终止

二、类的加载,链接,初始化

2.1 加载:查找并加载类的二进制数据

类加载器并不需要某个类被首次主动使用时再加载他。JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。类被加载后,就进入连接阶段。

2.2 连接:

将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。然后要经过一系列的验证。

2.2.1 验证:确保被加载的类的正确性(验证字节码)
  • 类文件的结构检查:确保类文件遵从java类文件的固定格式。
  • 语义检查:确保类本身符合java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖。(虽然编译时就可以发现错误,但不经过编译,手动生成class文件,那么就会发现不了final类型的方法被覆盖,但是语义检查就可以发现)
  • 字节码验证:确保字节码流可以被java虚拟机安全的执行。字节码流代表java方法(报空静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
  • 二进制兼容性的验证:确保相互引用的类之间的协调一致,例如在Wroker类的gotoWork()方法中会调用Car类的run()方法。java虚拟机在验证work()类时,会检查在方法区内是否存在Car类的run()方法,假如不存在(当worker类和Car类的版本不兼容,就会出现这种问题),就会抛出NoSuchMethodError方法。
public class Wroker{public void gotoWork(){Car car = new Car();	car.run();//这段代码在worker类的二进制数据中表示为符号引用}
}
2.2 准备:为类的静态变量分配内存,并将其初始化为默认

在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于一下Sample类,在准备阶端,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0。

public class Sample{private static int a=1;public static long b;static{b=2;}
}
2.3 解析:把类中的符号引用转换为直接引用

在解析阶段,java虚拟机会把类的二进制数据中的符号引用替换为直接引用。例如在Worker类的gotoWork()方法中会引用Car类的run()方法。

在Worker类中的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,Java虚拟机会把这个符号替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置。这个指针就是直接引用。

public class Wroker{public void gotoWork(){Car car = new Car();	car.run();//这段代码在worker类的二进制数据中表示为符号引用}
}
2.3 初始化:为类的静态成员变量赋予正确的初始值

在初始化阶段,java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量初始化有两种途径:

  • 在静态变量的声明处进行初始化
  • 在静态代码快中进行初始化。例如在以下代码中,静态变量a和b都被显示初始化,而静态变量c没有被显示初始化,它将保持默认值为0;但是如果要使用c,则必须进行初始化。
public class Sample{private static int a=1; //在静态变量声明出进行初始化public static long b;public static long c;  //但是如果要使用c,则必须进行初始化static{b=2; //在静态代码块中进行初始化}
}

示例:

public class ClassLoaderTest {public static void main(String[] args) {Singleton singleton=Singleton.getInstance();System.out.println("counter1= "+singleton.counter1);System.out.println("counter2= "+singleton.counter2);}
}/***程序是从上向下顺序执行
* new Singleton()时,counter1,counter2初始值均为0
* 在通过构造方法Singleton(),均加1.则返回的值counter1,counter2均为1
* 然后再程序在继续向下执行,由于counter1没有显示初始化,则值还是为1
* 但是counter2经过显示初始化后,其值为0
* @author coderacademy
*/
class Singleton{private static Singleton singleton=new Singleton();//new语句在这是结果为counter1= 1;counter2= 0public static int counter1;public static int counter2=0;//private static Singleton singleton=new Singleton();//new语句在这是结果为counter1= 1;counter2= 1private Singleton(){counter1++;counter2++;}public static Singleton getInstance(){return singleton;}
}
  • 静态变量的声明语句,以及静态代码块都被看做类的初始化语句,java虚拟机会按照初始化语句在类文件中的先后顺序来一次执行他们。
  • 类的初始化步骤
  • 假如这个类还没有被加载和连接,那就先进行加载和连接
  • 假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。
  • 假如父类中存在初始化语句,那就依次执行这些初始化语句。

public class FinalTest {public static void main(String[] args) {System.err.println(Test.X);}
}/**
* 当X=6/3时,编译时即可算出X=2,即编译时常量,即不需要运行类,所以不打印静态代码块中的内容
*当X=new Random().nextInt(100)时,编译时不能算出X的值,只有运行程序才知道,所以打印结果为:FinalTest static final 2
* @author coderacademy
*/
class Test{public static final int X=6/3;//打印结果: 2//public static final int X=new Random().nextInt(100);//打印结果为FinalTest static final 2static{System.err.println("FinalTest static final");}
}
  • 类的初始化时机:当java虚拟机初始化一个类时,要求他的所有父类都已经被初始化,但是这条规则并不适用于接口。
  • 在初始化一个类时,并不先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化他的父接口
    因此,一个父接口并不会因为他的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
public class Test4 {static {System.err.println("Test4 static block");}public static void main(String[] args) {System.err.println(Child.b);}
}/**
* Test4 static block
* Parent static block
* Child static block
* 4
* @author coderacademy
*/
class Parent{public static final int a=3;static{System.err.println("Parent static block");}
}class Child extends Parent{public static int b=4;static{System.err.println("Child static block");}
}

如以下示例赋值的执行流程:

public class test(){private static int a=3;
}//首先在准备阶段java虚拟在内存中为a分配内存,int的初始值是0,所以此时a的值是0;在初始化阶段,给赋值为3
//相当于:public class test(){private static int a;//从上到下执行static{a=3;}
}

image.png

三、java程序对类的使用方式可分为两种:

3.1 主动使用
  • 创建类的实例。比如:new Test()
  • 访问某个类或者接口的静态变量,或者对该静态变量赋值。比如:int b=Test.a
  • 调用类的静态方法。例如:Test.doSomething();
  • 反射(如class.forName("com.jvm.classloader.test"))
  • 初始化一个类的子类(对父类的主动使用)。例如
class Parent {
}class Child extends Parent{public static int a=4;
}
Child.a=8;
  • java虚拟机启动时被表明为启动类的类

程序中对子类的“主动使用”会导致父类被初始化,但对父类的“主动使用”并不会导致子类初始化,不可能说生成一个Object类的对象就导致系统中所有的子类都会被初始化。

public class Test5 {static{System.err.println("Test5 static block");}public static void main(String[] args) {Parent2 parent;System.err.println("-------------");parent=new Parent2();System.err.println(Parent2.a);System.err.println(Child2.b);}
}/*** Test5 static block
* -------------
* Parent2 static block
* 3
* Child2 static block
* 4
*
*/
class Parent2{public static final int a=3;static{System.err.println("Parent2 static block");}}class Child2 extends Parent2{public static int b=4;static{System.err.println("Child2 static block");}
}

只有当程序访问的静态变量或静态方法确实在当前接口定义时,才可以认为是对类或接口的主动使用。

public class Test6 {public static void main(String[] args) {System.err.println(Child3.a);Child3.doSomething();}
}/**
* Parent3 static block
* 3
* doSomething
* @author coderacademy
*/
class Parent3{static int a=3;static {System.err.println("Parent3 static block");}static void doSomething(){System.err.println("doSomething");}}class Child3 extends Parent3{static{System.err.println("Child3 static block");}
}

调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

public class Test7 {public static void main(String[] args) throws ClassNotFoundException {ClassLoader loader=ClassLoader.getSystemClassLoader();Class<?> clazz=loader.loadClass("com.jvm.classloader.Z");System.err.println("------------------------");clazz=Class.forName("com.jvm.classloader.Z");}
}/**
* ------------------------
*Z static block
* @author coderacademy
*/
class Z{static{System.err.println("Z static block");}
}
3.2 被动使用

除去以上六种主动使用以外的使用都是被动使用,都不会导致类的初始化。所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其方法存进运行时数据区的方法区内。然后在堆区创建一个Java.lang.Class对象,用来封装在类在方法区内的数据结构。

image.png

四、 加载class文件的方式

4.1 本地系统中直接加载
  • 通过网络下载.class文件(java.net.URLClassLoader(URL[] urls))
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将java源文件动态编译为.class文件。
    类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
4.2、两种类型的类加载器
4.2.1 Java虚拟机自带的加载器
  • 根类加载器(Bootstrap)。使用C++编写,程序员无法在java代码中获得该类。
  • 扩展类加载器(Extension),使用java代码实现
  • 系统类加载器(System),应用加载器,使用java代码实现
4.2.2 用户自定义的类加载器
  • java.lang.ClassLoader的子类
  • 用户可以定制类的加载方式
    public ClassLoader getClassLoader()方法。针对这个类返回一个个加载器,但是某些实现可能会返回null代表根类加载器。如果使用根类加载器加载类,那么这个方法就会返回null;例:
public class BootStrapTest {public static void main(String[] args) throws Exception {Class clazz=Class.forName("java.lang.String");ClassLoader loader=clazz.getClassLoader();/*** 打印结果为null*/System.err.println(loader);Class clazz2=Class.forName("com.jvm.classloader.C");ClassLoader loader2=clazz2.getClassLoader();/*** 打印结果为:sun.misc.Launcher$AppClassLoader@54a5f709 应用加载器*/System.err.println(loader2);}
}class C{}

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/614003.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于ssm运动会管理系统的设计与实现 【附源码】

基于ssm运动会管理系统的设计与实现 【附源码】 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuil…

CSS 圆形分割按钮动画 带背景、图片

<template><view class="main"><view class="up"> <!-- 主要部分上 --><button class="card1"><image class="imgA" src="../../static/A.png"></image></button><butt…

NIO通信代码示例

NIO通信架构图 1.Client NioClient package nio;import constant.Constant;import java.io.IOException; import java.util.Scanner;public class NioClient {private static NioClientHandle nioClientHandle;public static void start() {nioClientHandle new NioClientHa…

3 快速前端开发

3 前端JavaScript 3 前端JavaScript1. JavaScript1.1 代码位置1.2 注释1.3 变量1.4 字符串类型案例&#xff1a;跑马灯 1.5 数组案例&#xff1a;动态数据 1.6 对象&#xff08;字典&#xff09;案例&#xff1a;动态表格 1.7 条件语句1.8 函数 2.DOM2.1 事件的绑定 3.知识点的…

redis中的string相关的部分命令

redis命令手册 redis中文官网查看文档 挨个进行输出调试 Redis Setnx 命令 Redis Getrange 命令 Redis Mset 命令 redis 127.0.0.1:6379> MSET key1 "Hello" key2 "World" OK redis 127.0.0.1:6379> GET key1 "Hello" redis 127.0.0.1:…

LLVM的安装步骤实战

目录 1. 准备环境 1.1 安装必备软件包 1.2 配置Git 2. 用CMake构建 2.1 克隆代码库 2.2 创建构建目录 2.3 生成构建系统文件 3. 自定义构建 3.1 CMake定义的变量 3.2 LLVM定义的变量 4. 总结 1. 准备环境 首先操作系统可以是Linux、FreeBSD、macOS或Windows。 同…

2.【CPP】入门(宏||内联函数||拷贝构造||析构函数||构造函数)

0x01.引言 1.实现一个宏函数ADD #define ADD(x,y) ((x)(y))//宏是预编译阶段完成替换&#xff0c;注意括号2.宏的优缺点 优点&#xff1a; 1.增强代码的复用性 2.宏函数不用建立栈帧&#xff0c;提高性能 缺点&#xff1a; 1.不方便调试 2.没有安全检查 0x02.内联函数 1.以空…

可狱可囚的爬虫系列课程 11:Requests中的SSL

一、SSL 证书 SSL 证书是数字证书的一种&#xff0c;类似于驾驶证、护照、营业执照等的电子副本。SSL 证书也称为 SSL 服务器证书&#xff0c;因为它是配置在服务器上。 SSL 证书是由受信任的数字证书颁发机构 CA 在验证服务器身份后颁发的&#xff0c;其具有服务器身份验证和…

base64与BytesIO图片进行编码、解码;api调用

base64与BytesIO简单介绍 io.BytesIO 和 Base64 编码都是用于在内存中处理二进制数据的方法&#xff0c;但它们的目的和使用场景有所不同。 1&#xff09; io.BytesIO io.BytesIO 是 Python io 库中的一个类&#xff0c;它提供了一个在内存中处理二进制数据的接口&#xff0…

Linux最常用的几个时间日期命令

文章目录 Linux最常用的几个时间日期命令一日难再晨及时当勉励 date默认输入显示时区世界协调时格式化日期 时光总是催人老 time语法示例 休息一会 sleep休息5分钟1小时后提醒我时分秒搭配使用倒计时计时器结合脚本 更多信息 Linux最常用的几个时间日期命令 桃花谢了春红&…

如何在Docker本地搭建流程图绘制神器draw.io并实现公网远程访问

推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站 前言 提到流程图&#xff0c;大家第一时间可能会想到Visio&#xff0c;不可否认&#xff0c;VIsio确实是功能强大&#xff0c;但是软…

C++里main函数int main(int argc, char **argv)

C里main函数int main(int argc, char **argv), 这两个参数argc和argv分别是什么

Vue3组件库 -- element plus 树形选择器组件怎样显示已有的树形菜单?

<el-tree-selectv-model"form.topmneu":data"tableData":props"{ label: title, value: id }":render-after-expand"false"style"width: 100%"check-strictly/> 添加 :props "{ lable : 字段名 , value: 字段…

极客时间-读多写少型缓存设计

背景 内容是极客时间-徐长龙老师的高并发系统实战课的个人学习笔记&#xff0c;欢迎大家学习&#xff01;https://time.geekbang.org/column/article/596644 总览内容如下&#xff1a; 缓存性价比 一般来说&#xff0c;只有热点数据放到缓存才更有价值 数据量查询频率命中…

java基础之异常练习题

异常 1.Java 中所有的错误/异常都继承自 Throwable类&#xff1b;在该类的子类中&#xff0c; Error 类表示严重的底层错误&#xff0c; 对于这类错误一般处理的方式是 直接报告并终止程序 &#xff1b; Exception 类表示异常。 2.查阅API&#xff0c;完成以下填空&#xff1a;…

leetcode动态规划(零钱兑换II、组合总和 Ⅳ)

518.零钱兑换II 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 示例 1: 输入: amount 5, coins [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 55 5221 52111 511111 示例 2: 输入: amount 3, coi…

【江科大STM32单片机】day1点亮LED灯流水灯蜂鸣器

知识点 推挽模式&#xff1a;高-》低、低-》高电平都能驱动 开漏模式&#xff1a;只能低-》高电平能驱动&#xff0c;高电平相当于高阻态 GPIO_WriteBit 操作单个 GPIO_ResetBits 操作同组 3-2 led闪烁 配置相关驱动 USE_STDPERIPH_DRIVER 配置输出文件格式debug配置slink勾选…

基于Springboot的课程答疑系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的课程答疑系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&…

Python知识点(史上最全)

Python期末考试知识点&#xff08;史上最全&#xff09; python简介 Python是一种解释型语言 Python使用缩进对齐组织代码执行&#xff0c;所以没有缩进的代码&#xff0c;都会在载入时自动执行 数据类型&#xff1a;整形 int 无限大 浮点型 float…

小程序基础学习(组件化)

&#xff08;一&#xff09;创建 找到components文件夹下面创建新的文件夹 然后再文件夹内创建component格式的文件 创建后这样 我创建的是my-info的文件夹以及my-info的components文件&#xff0c;跟着普通的页面一样 &#xff08;二&#xff09; 注册组件 找到你需要使用组…