【JVM学习笔记】类加载过程
在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的;提供了更大的灵活性,增加了更多的可能性
JVM启动过程包括:加载、连接、初始化
- 加载:就是将class文件加载到内存。详细的说是,将class文件加载到运行时数据区的方法区内(JDK7是方法区,JDK8对应的是Metaspace),然后创建一个java.lang.Class对象,用来封装类在方法区类的数据结构。JVM规范并未说明Class对象位于何处,Hotspot虚拟机将其放在了方法区。JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
- 有两种类型的类加载器:
- 1.JVM自带提供的三类加载器:
- 根类加载器Bootstrap Classloader(C++写的, 程序员无法在JAVA代码中获得该类)
- 扩展加载器Extension Classloader,使用Java代码实现
- 系统加载器System ClassLoader,也叫应用加载器 Application Classloader,使用Java代码实现
- 2.用户自定义的类加载器,都是java.lang.ClassLoader的子类
- 连接:连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去,分为三步
- 1.验证:检查被加载的类的正确性,具体来说包括但不限于以下操作:
- 类文件的结构检查
- 语义检查
- 字节码验证
- 二进制兼容性的验证
- 2.准备:虚拟机为类的静态变量分配内存,并设置默认的初始值。比如一个类中有一句private static int num=1; 实际上在这一步的时候,只是将num初始化为默认值0
- 3.解析:把类中的符号引用转换为直接引用。这个也好理解,毕竟所有类都加载好,才能真正进行直接引用,但是类的加载也有现后顺序之分,所以如果先加载的类引用了后加载的类,只有等到后者完成加载,才能真正引用到内存中的地址
- 1.验证:检查被加载的类的正确性,具体来说包括但不限于以下操作:
- 初始化:这一步将静态变量最终赋值,比如上面举例钟,num变量将被赋值为1。这一步骤中,有以下要点知识:1.JVM规定,"任何JVM实现,必须在每个类或接口被JAVA程序"首次主动使用时,才初始化它们"。2.静态变量的声明语句,以击静态代码块都被看做类的初始化语句;Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行他们,所以初始化语句和静态块的顺序可能会影响程序结果,这一点在本文后面的例子中有所体现(详见本页"初始化的顺序问题")。类的初始化步骤如下:
- 假如这个类还没有被加载和连接,那就先进行加载和连接
- 假如类存在直接父类,并且父类还没有被初始化,那就先初始化直接父类
- 加入类中存在初始化语句,那就依次执行这些初始化语句
何为类的主动使用/被动使用,类是否可以被卸载
- 类被加载后,是可以进行卸载的,比如OSGI就这样做了,类卸载相关知识见 https://www.cnblogs.com/heben/p/11438503.html "类的卸载”
- Java程序对类的使用分为两种:主动使用,被动使用。所有的Java虚拟机实现,必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
- 什么是主动使用呢,有7钟情况(但不是非常准确的划分)
- 创建类的实例,比如new一个对象
- 访问某个类或者接口的静态变量,或对该静态变量赋值,在虚拟机字节码层面是使用的getstatic/putstatic助记符来操作的
- 调用类的静态方法,在虚拟机字节码层面是使用invokestatic注记符来操作的
- 反射的方式获取到一个类的Class对象,如Class.forName("com.test.Test");
- 初始化一个类的子类
- Java虚拟机启动时,被标名为启动类的类,即包含了main方法的类
- JDK7开始提供了动态语言支持,如果java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化
- 什么是被动使用呢,除了上述主动使用的情况外,其他对类的使用方式都视作被动使用,都不会导致对类的初始化,比如:
- 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
- 声明一个类的引用对象,并不是对类的主动使用,例如:Parent parent; 就不是对Parent类的主动使用,不会导致类的初始化
- 什么是主动使用呢,有7钟情况(但不是非常准确的划分)
加载.class文件的方式
- 从本地文件系统中加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将java源文件动态地编译为.class文件,动态代理里面会出现,因为编译阶段这个类不存在,运行期才生成。又如JSP会被转换成Servlet,被编译成一个class文件
对于静态字段来说,只有直接定义了该字段的类才会被初始化,下面代码中,对MyParent1是主动使用,对MyChild1是被动使用
关于static块的执行顺序,可参考之前的笔记 https://www.cnblogs.com/heben/p/5391964.html
/**
* @author wx
* @Description 对于静态字段来说,只有直接定义了该字段的类才会被初始化
* @date 2019/08/30 1:38
*/
public class ClassUsesTest {
public static void main(String[] args) {
System.out.println(MyChild1.str);
}
} class MyParent1 {
public static String str = "hello world";
static {
System.out.println("MyParent1 static block");
}
} class MyChild1 extends MyParent1 {
static {
System.out.println("MyChild1 static block");
}
}
对于静态字段来说,只有直接定义了该字段的类才会被初始化,以上代码输出结果为
MyParent1 static block
hello world
如果在MyChild1中也定义一个静态变量 public static String str = "hell"; 则输出将变为
MyParent1 static block
MyChild1 static block
heel
这是因为初始化一个类的子类,首先要初始化其父类
6.疑问:在第5点的示例代码中,只有父类得到了初始化,子类没有初始化,那子类有没有被加载呢,关于这一点,虚拟机并没有进行明确规定,但是可以用参数-XX:+TraceClassLoading,用于追踪类的加载信息并打印出来
从运行结果可以看出,子类是被加载了的
顺带一提JVM参数的规律,JVM的参数都是-XX:开头的,其中
-XX:+<option>表示开启option
-XX:- <option>表示关闭option
-XX:<option>=<value>表示将option选项的值设置为value
option指的就是例如TraceClassLoading这样的参数
7.final关键字带来的影响
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
} class MyParent2 {
public static final String str = "hello world"; static {
System.out.println("MyParent2 static block");
}
}
与之前代码的区别在于变量带上了final关键字,输出结果为
hello world
这是因为,str变量作为一个常量,在编译阶段,这个常量就会被存入到调用这个常量的那个方法的类所在的常量池中,也就是"hello world"这个常量,会被放置到 MyTest2 这个类的常量池中。
本质上,调用类并没有直接引用到定义常量的那个类,因此并不会促发对那个类的初始化,并且此后MyTest2 类和MyParent2类就没有关系了,甚至可以将MyParent2的字节码文件删除
通过javap -c MyTest2.class可以看到相关信息
D:\workspace-learn\common-learn\learn-jvm\target\classes\com\learn\jvm\loader>javap -c MyTest2.class
Compiled from "MyTest2.java"
public class com.learn.jvm.loader.MyTest2 {
public com.learn.jvm.loader.MyTest2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
助记符ldc,表示将int, float, 或是String类型的常量值从常量池推送至栈顶
如果将上面的代码稍作改变,将str变量修改为short类型的数字,则反编译的结果,里ldc助记符将变为bipush
助记符bipush,表示将一个单字节(-128 ~ 127)的常量值推送至栈顶
另外还有:
助记符sipush,表示将一个短整型常量值(-32768 ~ 32767)推送至栈顶
助记符iconst_1,表示将一个int类型的1推送至栈顶,另外jvm可能认为-1到5这7个数字很常见,所以有iconst_m1,iconst_0,iconst_1,iconst_2,一直到iconst_5这7个专门的助记符表示int类型的-1到5
助记符是由相关的类来操作的,比如com.sun.org.apache.bcel.internal.generic.ICONST类
8.如果final变量是运行期才能确定的值
package com.learn.jvm.loader; import java.util.UUID; public class MyTest3 { public static void main(String[] args) {
System.out.println(MyParent3.str);
}
} class MyParent3 {
public static final String str = UUID.randomUUID().toString(); static {
System.out.println("MyParent3 static block");
}
}
输出
MyParent3 static block
5bd87094-5c54-4888-a053-387083ca1a28
由于str的值是运行期才能确定,所以这个常量在编译期无法被直接存入MyTest3的常量池,因此这个例子中,MyParent3类得到了初始化
9.对象数组的情况
public class MyTest4 { public static void main(String[] args) {
System.out.println("自定义类型的数组情况");
MyParent[] array = new MyParent[5];
System.out.println(array.getClass());
System.out.println(array.getClass().getSuperclass()); System.out.println("基本类型的数组情况");
int[] nums = new int[5];
System.out.println(nums.getClass());
System.out.println(nums.getClass().getSuperclass());
}
} class MyParent {
public MyParent() {
System.out.println("MyParent constructor");
}
static {
System.out.println("MyParent static block");
}
}
运行结果
自定义类型的数组情况
class [Lcom.learn.jvm.loader.MyParent;
class java.lang.Object
基本类型的数组情况
class [I
class java.lang.Object
代码没有输出静态块的内容,这是由于MyParent类没有得到初始化,没有初始化的原因是没有MyParent类的实例生成。这里生成的实例是MyParent[]类型的实例,这个实例的类型就是[Lcom.learn.jvm.loader.MyParent,这是虚拟机运行期间生成的类型
,打头一个左方括号表示一维数组,二维数组则是两个左方括号
对于这个例子的总结:对于数组来说,其类型是由JVM在运行时创建的,表示为[Lcom.xxx]这种形式,其父类型为Object,对于数组来说,JavaDoc经常将构成数组的元素成为Component,实际上就是将数组降低一个维度后的类型
反编译结果如下:
D:\workspace-learn\common-learn\learn-jvm\target\classes\com\learn\jvm\loader>javap -c MyTest4.class
Compiled from "MyTest4.java"
public class com.learn.jvm.loader.MyTest4 {
public com.learn.jvm.loader.MyTest4();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 自定义类型的数组情况
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: iconst_5
9: anewarray #5 // class com/learn/jvm/loader/MyParent
12: astore_1
13: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_1
17: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
20: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
23: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_1
27: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
30: invokevirtual #8 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
33: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #9 // String 基本类型的数组情况
41: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: iconst_5
45: newarray int
47: astore_2
48: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
51: aload_2
52: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
55: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
58: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
61: aload_2
62: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
65: invokevirtual #8 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
68: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
71: return
}
助记符anewarray,表示创建一个引用类型的数组,并将其引用的值压入栈顶
助记符newarray,表示创建一个指定的原始类型(如int,float,char等)的数组,并将其引用值压入栈顶
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
- 因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
以下代码佐证了上面的第一种说法
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
}
} interface MyParent5 {
// 定义一个一个静态内部类,并实例化它,并赋值给thread变量
// 如果MyParent5接口被初始化,即thread被初始化,就一定会打印出hello world
public static Thread thread = new Thread() {
{
System.out.println("hello world");
}
};
} class MyChild5 implements MyParent5 {
public static int b = 5;
}
运行结果,只打印出了5
11.初始化的顺序问题
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1: " + Singleton.counter1);
System.out.println("counter2: " + Singleton.counter2);
}
} class Singleton {
public static int counter1;
public static int counter2 = 0; private static Singleton singleton = new Singleton(); private Singleton() {
counter1++;
counter2++;
} public static Singleton getInstance() {
return singleton;
}
}
运行结果:
counter1: 1
counter2: 1
但如果稍微改变一下counter2申明和初始化的位置:
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1: " + Singleton.counter1);
System.out.println("counter2: " + Singleton.counter2);
}
} class Singleton {
public static int counter1; private static Singleton singleton = new Singleton(); private Singleton() {
counter1++;
counter2++;
} public static int counter2 = 0; public static Singleton getInstance() {
return singleton;
}
}
运行结果如下
counter1: 1
counter2: 0
仅仅调整了代码的位置,结果就发生了变化
这是因为在对Singleton类的初始化过程中,是按照变量声明的顺序进行初始化的,首先是初始化counter1,然后接着初始化single对象,导致构造函数被调用,然后才是初始化counter2变量。从这里也可以看出“连接”步骤中,“准备“阶段,也就是赋初值的意义所在,如果没有这一步,显然counter2++就是没有意义的语句
Java虚拟机与程序的生命周期
在如下几种情况中,Java虚拟机将结束生命周期
- 执行了System.exit方法
- 程序正常执行结束
- 程序运行中发生错误/异常导致异常终止
- 操作系统出现错误导致jvm进程终止
一张总结了类和对象有关加载,连接,初始化的知识的图
【JVM学习笔记】类加载过程的更多相关文章
- JVM学习笔记——类加载过程
JVM学习笔记——类加载过程 类加载模型——双亲委派模型(Parents Delegation Model)也可称为“溯源委派加载模型” Java的类加载器是一个运行时核心基础设施模块,主要是启动之初 ...
- [jvm学习笔记]-类加载过程
JVM类加载的过程 加载=>验证=>准备=>解析=>初始化 5个阶段所执行的具体动作 加载 在加载阶段,虚拟机需要完成3个事情1.通过一个类的全限定名获取定义此类的二进制字节流 ...
- JVM学习笔记——类加载和字节码技术篇
JVM学习笔记--类加载和字节码技术篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的类加载和字节码技术部分 我们会分为以下几部分进行介绍: 类文件结构 字节码指令 编译期处理 类 ...
- JVM学习笔记——类加载器与类加载过程
类加载器与类加载过程 类加载器ClassLoader 类加载器 ClassLoader 用于把 class 文件装载进内存. 启动类加载器(Bootstrap ClassLoader): 这个类加载使 ...
- JVM学习笔记:虚拟机的类加载机制
JVM类加载机制分两部分来总结: (1)类加载过程 (2)类加载器 一.JVM类加载过程 类的加载过程:加载 →连接(验证 → 准备 → 解析)→ 初始化. 类的生命周期:加载 →连接(验证 → 准备 ...
- JVM学习笔记-第七章-虚拟机类加载机制
JVM学习笔记-第七章-虚拟机类加载机制 7.1 概述 Java虚拟机描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被 ...
- java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)
java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...
- java之jvm学习笔记二(类装载器的体系结构)
java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行引擎从总的来说主要的执行方式分为四种, 第一种,一次性解释代码,也就是当字节码转载到内存后,每次需要都会重新 ...
- JVM学习笔记-第六章-类文件结构
JVM学习笔记-第六章-类文件结构 6.3 Class类文件的结构 本章中,笔者只是通俗地将任意一个有效的类或接口锁应当满足的格式称为"Class文件格式",实际上它完全不需要以磁 ...
- JVM学习笔记——垃圾回收篇
JVM学习笔记--垃圾回收篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的垃圾回收部分 我们会分为以下几部分进行介绍: 判断垃圾回收对象 垃圾回收算法 分代垃圾回收 垃圾回收器 ...
随机推荐
- BZOJ 世界树
第一步首先建虚树 第二步两遍dfs,一次从叶子到根,一次从根到叶子,就可以得到虚树中每个节点在M个询问点中离他最近的是哪个(简称为控制点) 第三步考虑计算答案,对于整个树,我们把节点化为三个种类 1. ...
- web页面ios浏览器img图片的坑
大家都知道ios浏览器有个默认的内置事件,就是长按屏幕,会放大预览你点击的内容. 不同浏览器效果各异,有的浏览器弹出提示框,预览图片或保存图片.主要呈下面2中形式.体验最差的就是后者,会直接将页面中的 ...
- Oracle修改表,提示“资源正忙,要求指定NOWAIT”
今天往一个表里面多增加了两个字段,修改完毕,保存的时候,提示如下内容:“资源正忙,要求指定nowait”.重试好几遍,都没有解决,于是搜索了一下,找到了解决方法,如下: 首先执行下面一段代码,得到锁定 ...
- 使用IDEA 搭建一个 SpringBoot + Hibernate + Gradle 项目
现在创建个项目: 勾上 自已 需要东西.(这里作为演示) maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'} 关闭项目 ...
- hive 汇率拉链表转日连续流水表
1.什么是拉链表 拉链表是针对数据仓库设计中表存储数据的方式而定义的,顾名思义,所谓拉链,就是记录历史.记录一个事物从开始,一直到当前状态的所有变化的信息. 我们先看一个示例,这就是一张拉链表,存储的 ...
- JZOJ 5988 珂学计树题 (Burnside引理)
什么神题a-没学过Burnside引理a学了也做不来系列-考场没怎么看这题,上最后十分钟打了样例就溜了-然后这题爆0了. Here CODE #include <cctype> #incl ...
- Prism框架中View与Region关联的几种方式
Prism.Regions命名空间下有2个重要接口:IRegionManager.IRegion IRegionManager接口中的方法与属性:AddToRegion().RegisterViewW ...
- 【C#】图片处理(底片,黑白,锐化,柔化,浮雕,雾化)
https://www.cnblogs.com/bomo/archive/2013/03/01/2939453.html --------------------------------------- ...
- python中的位运算
目录 1.判断奇偶数 2.交换两个数 3.找出没有重复的数 4.3的n次方 5. 找出不大于N的最大的2的幂指数 1.判断奇偶数 如果把n以二进制形式展示的话,我们只需要判断最后一个二进制位是1还是0 ...
- svn版本更新
1.查看当前版本:svn --version 2.配置svn yum源 tee /etc/yum.repos.d/wandisco-svn.repo <<-'EOF' [WandiscoS ...