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.hqwc.cn/news/337231.html

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

相关文章

javacc知识点

javacc的语法描述性文件以.jj结尾&#xff0c;一般情况下采用 1、option{JavaCC的选项} options部分&#xff0c;用于放置 JavaCC 的选项&#xff0c;常见option包括&#xff1a; STATIC&#xff1a;用于决定JavaCC生成的所有成员及方法是否被定义为static(注意&#xff1a;定义…

【算法Hot100系列】搜索旋转排序数组

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…

Pytorch张量通过索引获取指定数据

import torch x torch.tensor([1,2,3])x Out[3]: tensor([1, 2, 3])x[0] # 索引操作&#xff1a;取单个数字 Out[4]: tensor(1)x[0:1] # 切片操作&#xff1a;可以保持维度不变 Out[5]: tensor([1])x[torch.tensor([True,False,True])] # 布尔值索引&#xff0c;通过条件筛…

Xcode15 升级问题记录

这里写自定义目录标题 新版本Xcode15升级问题1&#xff1a;rsync error: some files could not be transferred (code 23) at ...参考 新版本Xcode15升级 下载地址&#xff1a;https://developer.apple.com/download/all/ 我目前使用的版本是Xcode15.2 我新创建了一个项目&…

uni微信小程序强制用户更新版本

强制更新的代码参考官方文档 uni.getUpdateManager() | uni-app官网 我这边的如下&#xff1a; //检查版本更新const updateManager uni.getUpdateManager();updateManager.onCheckForUpdate(function (res) {// 请求完新版本信息的回调console.log(res.hasUpdate, "是…

【JavaFX】JavaFX11开发踩坑记录

文章目录 技术栈踩坑记录 技术栈 JavaFX 11MavenJDK 11 踩坑记录 这些坑对于初学者很容易踩&#xff0c;JavaFX经常会报错空指针异常遇到其中一个问题可能就会消耗好几天的时间。 JavaFX 采用的是MVC架构设计&#xff0c;页面设计使用 fxml文件&#xff1b;业务逻辑采用Con…

部分城市公交站点数据,Shp+excel格式数据,2020年,几何类型为点

随着城市的发展和人口的增长&#xff0c;公共交通成为了人们出行的重要方式之一。而公交站点作为公共交通的重要组成部分&#xff0c;其数据信息的获取和分析对于城市规划和管理具有重要意义。 今天来分享一下部分城市公交站点数据&#xff1a; 首先先了解下该数据的基本信息 …

SOLIDWORKS2024新功能——SOLIDWORKS篇(一)

SOLIDWORKS2024新功能——SOLIDWORKS篇章节概括&#xff1a; • 切口工具 • 槽口延伸 • 戳记工具 • 薄片和槽口中的切割法线 切口工具 您可以使用切口工具在空心或薄壁圆柱体和圆锥体中生成切口。通过选择圆柱面或圆锥面上的边线&#xff0c;您可以将零件平展为钣金。 在…

加工零件的题解

目录 原题描述&#xff1a; 题目描述 输入格式 输出格式 样例 #1 样例输入 #1 样例输出 #1 样例 #2 样例输入 #2 样例输出 #2 提示 题目大意&#xff1a; 主要思路&#xff1a; 但是我们怎么才能判断出x走到1时L是偶数还是奇数呢&#xff1f; 初始化&#xff1a;…

test fuzz-05-模糊测试 kelinci AFL-based fuzzing for Java

拓展阅读 开源 Auto generate mock data for java test.(便于 Java 测试自动生成对象信息) 开源 Junit performance rely on junit5 and jdk8.(java 性能测试框架。性能测试。压测。测试报告生成。) test fuzz-01-模糊测试&#xff08;Fuzz Testing&#xff09; test fuzz-…

Nginx实战 | 高性能HTTP和反向代理神器Nginx前世今生,以及它的“繁花之境”

专栏集锦&#xff0c;大佬们可以收藏以备不时之需&#xff1a; Spring Cloud 专栏&#xff1a;http://t.csdnimg.cn/WDmJ9 Python 专栏&#xff1a;http://t.csdnimg.cn/hMwPR Redis 专栏&#xff1a;http://t.csdnimg.cn/Qq0Xc TensorFlow 专栏&#xff1a;http://t.csdni…

C++并发编程实战第2版笔记

文章目录 p19 某个线程只可以join()一次p22 只有当joinable()返回true时才能调用detach()P21 在std::thread对象析构前&#xff0c;必须明确是等待还是分离线程P25 移动语义P25 将类的成员函数设定为线程函数 p19 某个线程只可以join()一次 只要调用了join()&#xff0c;隶属于…