在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
①、其中标红的"类型"指的是我们定的Class、Interface、Enum,注意:在类加载这个阶段并未涉及到任何对象的概念。
②、类加载过程是在程序运行期间完成的,因为这个特性使得我们程序员可以做一些创新的东东,具体的体现待未来再来发掘。
1、加载:查找并加载类的二进制数据
将处于磁盘上的类的.class文件中的二进制数据读入内存中,将其放在运行时数据区的方法区中,然后在内存中创建一个Java.lang.Class对象,用于封装类在方法区内的数据结构
加载.class文件的方式
(1)从本地系统直接加载【最常见的方式】
(2)通过网络下载.class文件
(3)从zip,jar等归档文件中加载.class文件【这就是为什么三方的库通常都是以jar包的形式提供的原因】
(4)从专有数据库中提取.class文件【用的比较少,了解既可】
(5)将Java源文件动态编译为.class文件【动态代理就是最典型的例子】
2、连接(连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。)
- 验证:确保被加载的类的正确性
【并非所有内容,而是只列了主要的内容】
①、类文件的结构检查。
②、语义检查。
③、字节码验证。
④、二进制兼容性的验证。 - 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转化为直接引用
符号引用和直接引用的区别
①:符号引用:可以理解为一种间接的引用方式,通过一个符号的表示来引用,比如说一个类里面的方法引用了另外一个类。
②:直接引用:直接将方法通过指针的方式指向了目标的对象的内存地址,这样一下就能找到该方法。
3、初始化:为类的静态变量赋予正确的初始值
4、使用
5、卸载
class Test{
public static int a = 1;
}
准备阶段,先对a变量分配内存空间,将a初始化为0,在初始化过程中才把a变成1
Java虚拟机与程序的生命周期
在如下几种情况下,Java虚拟机(进程)将结束生命周期
1、执行了System.exit()方法
2、程序正常执行结束
3、程序在执行过程中遇到了异常或错误而异常终止
4、由于操作系统出现错误而导致Java虚拟机进程终止
Javs程序对类的使用可分为两种
- 主动使用
- 被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
主动使用(7种)
1、创建类的实例
2、访问某个类或接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
3个助记符:getstatic,putstatic,invokestatic
4、反射(如Class.forName("com.test.Test"))
5、初始化一个类的子类
当Child类初始化时,Parent这个类也会被初始化
class Parent {}
class Child extends Parent {}
6、Java虚拟机启动时被标明为启动类的类(含有Main方法的类)
7、JDK1.7开始提供的动态语言支持
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化
- 假如这个类还没有被加载和连接,那就先进行加载和连接。
- 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类。
- 假如类存在初始化语句,那就依次执行这些初始化语句
例子
public class Main {
public static void main(String[] args) {
//只执行这条时
/*System.out.println(MyChild.str);
/*输出:
MyParent static block
hello world*/
//只执行这条时
/*System.out.println(MyChild.str2);*/
/* 输出:
MyParent static block
MyChild static bolck
welcome*/
}
}
class MyParent {
public static String str = "hello world";
static {
System.out.println("MyParent static block");
}
}
class MyChild extends MyParent {
public static String str2 = "welcome";
static {
System.out.println("MyChild static bolck");
}
}
对于静态字段来说,只有直接定义了该字段的类才会被初始化;当一个类在初始化时,要求其父类全部都已经初始化完毕了
1、对于只输出System.out.println(MyChild.str)
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们,因此MyChild这个类是没有被初始化过,然而对于MyParent类来说,调用了MyParent类的静态变量,属于主动使用,所以会初始化MyParent这个类,就会先输出"MyParent static block",再输出"hello world"
2、对于只输出System.out.println(MyChild.str2)
由于调用了MyChild的静态变量,因此MyChild这个类会被初始化,然而 初始化一个类的子类时,父类会事先被初始化,因此MyParent会被更早地初始化,因此先输出MyParent static block,再输出MyChild static bolck,最后输出welcome
常量的本质
1、常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,本质上,调用类并没有对定义常量的类进行主动使用,因此并不会触发定义常量的类的初始化
注意:这里指的是将常量存放到MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了,甚至,我们可以将MyParent2的class文件删除
代码中只输出“hello world”
助记符:
ldc表示将int,float或者String类型的变量值从常量池中推送至栈顶
bipush表示将单字节(-128 ~ 127)的常量值推送至栈顶
sipush表示将一个短整型常量值(-32768 ~ 32767)推送至栈顶
iconst_1表示将int类型1推送到栈顶(iconst_-1 - iconst_5),只有5个
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent.str);
}
}
class MyParent2 {
public static final String str = "hello world";
public static final short a = 127;
public static final int i = 128;
public static final int m = 5;
static {
System.out.println("MyParent static block");
}
}
2、当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化
例子:
结果:
MyParent static code
9fd44deb-f811-4770-af3a-dde45deef547
import java.util.UUID;
public class MyTest {
public static void main(String[] args) {
System.out.println(MyParent.str);
}
}
class MyParent {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent static code");
}
}
3、初始化类和接口理论
image.png
额外说明
- 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。
- 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
论证1:“在初始化一个类时,并不会先初始化它所实现的接口”
image.png
也就是在接口中可以定义一个变量,然后在初始化变量里面加一个初始化块,如果MyParent5初始化了,那它里面的thread变量肯定也需要被初始化,而只要一初始化那“MyParent5 invoked”就会从初始化块中打印出来,注意:类的初始化和类的准备是两个不同的阶段,千万不要搞混啦,那下面来运行看一下:
image.png
而主动去调用MyChild5.b很显示会导致MyChild5子类的初始化,那“在初始化一个类时,并不会先初始化它所实现的接口。”,这样就精确论证了这一点,好,那如果将接口改为class呢?
image.png
当然会呀,因为目前是父子关系了,如下:
image.png
那如果再变化:
image.png
这个前面多次提到了,肯定是不会的,因为它是编译器的常量会被放到MyTest5的常量池了,如下:
image.png
接下来继续修改代码:
image.png
编译运行:
image.png
这个结论如预想,那如果改成类呢?
image.png
image.png
论证2:“在初始化一个接口时,并不会先初始化它的父接口。”:
image.png
编译运行:
image.png
很显然在初始化MyParent5这个子接口时,其MyGrandpa并未进行初始化。
4、类的初始化阶段是从上到下的
例子1:
public class Main {
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
分析:
在准备阶段,Singleton的静态变量counter1默认值是0,counter2默认值是0,singleton默认值是null,当主函数调用了Singleton类的静态方法时,对Singlenton类进行主动使用,因此开始初始化这个Singlenton类,由于类的初始化阶段是从上到下的,因此counter2 初始化成0,singleton 初始化成 new Singlenton(),这这个函数刚好调用了构造函数,counter1 ++; counter2 ++;因此counter1 = 1,counter2 = 1,主函数调用了Singleton类的静态方法后返回 singleton
例子2:(改变了例子1的代码位置)
public class Main {
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 = 1;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1 ++;
counter2 ++;//准备阶段的重要性
}
public static int counter2 = 0;
public static Singleton getInstance() {
return singleton;
}
}
结果会打印:
counter1: 2
counter2: 0
分析:
在准备阶段,Singleton的静态变量counter1默认值是0,counter2默认值是0,singleton默认值是null,当主函数调用了Singleton类的静态方法时,对Singlenton类进行主动使用,因此开始初始化这个Singlenton类,由于类的初始化阶段是从上到下的,因此counter1 初始化成1,singleton 初始化成 new Singlenton(),这这个函数刚好调用了构造函数,counter1 ++; counter2 ++;因此此时counter1 = 2,counter2 = 1,然后又对counter2进行初始化成0,因此counter2 = 0,主函数调用了Singleton类的静态方法后返回 singleton
5、类的生命周期
image.png
更详细版
image.png
深入理解
6、类的加载:
- 类的加载的最终产品是位于内存中的Class对象。
- Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
-
有两种类型的类加载器:(下面有详细说明)
1、Java虚拟机自带的加载器
①、根类(启动类)加载器(Bootstrap)。
②、扩展类加载器(Extension)。
③、系统(应用)类加载器(System)。
2、用户自定义的类加载器
①、java.lang.ClassLoader的子类。
这个类我想应该都知道,特别神秘,它是一个抽象类,也是所有自定义的直接继承类:
image.png
②、用户可以定制类的加载方式。 -
类的加载器并不需要等到某个类被“首次主动使用”时再加载它。
image.png
编译运行:
image.png
很显然MyChild1并未主动首次使用,因为它的static块木有被执行,但是!!这里看一下类的加载信息【还是在run中加一个jvm参数-XX:+TraceClassLoading用来查看类的加载信息】:
image.png
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。
- 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
7、类的准备
在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋预默认值0。程序如下:
public class Sample {
private static int a = 1;
private static long b;
static {
b = 2;
}
...
}
8、类的初始化
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:(1)在静态变量的声明处进行初始化;(2)在静态代码块中进行初始化。例如在以下代码中,静态变量a 和 b都被显式初始化,而静态变量c没有被显式初始化,它将保持默认值0。
public class Sample {
private static int a = 1; //在静态变量的声明处进行初始化
private static long b;
private static long c;
static {
b = 2;//在静态代码块中进行初始化
}
...
}
静态变量的声明语句,以及静态代码块都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。例如当以下Sample类被初始化后,它的静态变量a的取值为4。
public class Sample{
static int a = 1;
static {
a = 2;
}
static {
a = 4;
}
public static void main(String args[]){
System.out.println("a=" + a); //打印a=4
}
}
类加载器:
两个图片说明
image.png
image.png
除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
最后看一张图,它表示了类加载器的一个层次关系,如下:、
image.png
它们是一种双亲委托的关系,比如说系统类加载器尝试加载某一个类,它不会自己去加载,而会将其委托给“扩展类加载器”,而又会委托给“根类加载器”,如果它也加载不了,则会返回给“扩展类加载器”,如果它也加载不了则又会返回给“系统类加载器”,而它也加载不了则直接抛异常了,但凡以上加载器能加载则代表加载成功了,另外需要注意:貌似从关系图上看好像是系统类加载器继承至扩展类加载器,而扩展类加载器又继承根类加载器,其实不是这样的,而是一种包含关系:系统类加载器包含扩展类加载器,而扩展类加载器包含了根类加载器。关于这个在之后会进一步学习,先对理论有个大致的了解既可。
image.png
若有一个类加载器能够成功加载Test类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器
系统类加载器是定义类加载器,也是初始类加载器,loader1称初始类加载器












网友评论