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…

GBASE南大通用GBase 8a 安装部署

1.ssh连通性验证 ssh root192.168.7.71 ssh root192.168.7.72 ssh root192.168.7.73 2.检查防火墙是否关闭 systemctl status firewalld.service systemctl stop firewalld systemctl disable firewalld 3.检查selinux是否禁用 sestatus 若系统提示以下信息说明 selinu…

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…

ubuntu 使用VNC链接树莓派

ubuntu PC端安装remina sudo apt-add-repository ppa:remmina-ppa-team/remmina-next 然后&#xff0c;运行以下命令来安装 Remmina 软件包&#xff1a; sudo apt update sudo apt install remmina remmina-plugin-rdp remmina-plugin-secret flatpak run -- pkill remmina p…

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。 同…

Docker容器,使用 Docker 做些什么

都在使用 Docker 来做些什么。首先&#xff0c;我们需要明确的是&#xff0c;Docker 作为一种容器化技术&#xff0c;广泛应用于各种不同的场景&#xff0c;从微服务、云计算到持续集成和持续部署&#xff08;CI/CD&#xff09;&#xff0c;它的应用几乎遍及现代软件开发的每一…

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: 字段…

用python写个三子棋游戏

下面是一个简单的三子棋游戏的Python代码示例。在这个游戏中&#xff0c;玩家需要使用鼠标点击来放置棋子&#xff0c;并尽可能地使自己的三个棋子连成一线&#xff08;横、竖或斜&#xff09;。 python 复制代码 import pygame import random # 初始化pygame pygame…

iptables TEE模块测试小记

概述 因为公司项目需求&#xff0c;需要对服务器特定端口进行流量镜像&#xff0c;各种百度之后&#xff0c;发现TEE的模块&#xff0c;后来一番折腾&#xff0c;发现被转发的机器死活收不到数据&#xff0c;最后tcpdump一通了解到根源&#xff0c;博文记录&#xff0c;用以备…

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

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

力扣289. 生命游戏

模拟 染色 思路&#xff1a; 可以复制一个表格&#xff0c;然后根据规则两层循环模拟出结果&#xff0c;但是空间复杂度太高&#xff1b;可以复用原有数组&#xff0c;对其进行染色标记&#xff1b; 最终状态是活的标记值 > 1&#xff0c;还原标记值时可以使用规则 val &g…