做网站需要什么证件吗,外链官网,网站游戏入口,湘潭做网站选择磐石网络Java在运行期才对类进行加载到内存、连接、初始化过程。这使得Java应用具有极高的灵活性和拓展性#xff0c;可以依赖运行期进行动态加载和动态连接。
主要加载哪些#xff1f;Java中的数据类型分为基本数据类型和引用数据类型#xff0c;基本数据类型由虚拟机预先定义可以依赖运行期进行动态加载和动态连接。
主要加载哪些Java中的数据类型分为基本数据类型和引用数据类型基本数据类型由虚拟机预先定义而引用数据类型需要进行类的加载。
注对于同一个类加载器类只加载一次但对于不同的类加载器可以将类多次加载到内存并相互隔离
1 装载阶段Loading
装载阶段简言之查找并加载类的二进制数据生成该类的数据结构和Class的实例。
字节码文件加载到内存中的类模版结构存储在方法区class的实例存储在堆空间。
1、在加载类时Java虚拟机必须完成以下3件事情
通过类的全名获取类的二进制数据流。解析类的二进制数据流为方法区内的数据结构Java类模型。创建java.lang.Class类的实例表示该类型。作为方法区这个类的各种数据的访问入口。
说明
方法区1.8之前常被称为永久代1.8之后被元空间所取代
如果用一个变量clazz接收这个Cl ass类的实例这个变量在栈中并保存了实例的地址
创建Class的实例的构造方法是私有的只有JVM能够创建同时也是实现反射的关键 2、二进制流的获取方法
对于类的二进制数据流虚拟机可以通过多种途径产生或获得。只要所读取的字节码符合JVM规范即可
虚拟机从文件系统读入一个class后缀的文件。最常见读入jar、zip等归档数据包提取类文件。(jar包)事先存放在数据库中的类的二进制数据使用类似于HTTP之类的协议通过网络进行加载在运行时生成一段Class的二进制信息等
在获取到类的二进制信息后Java虚拟机就会处理这些数据并最终转为一个java.lang.Class的实例。
如果输入数据不是ClassFile的结构则会抛出ClassFormatError。
3、数组的加载
创建数组类的情况稍微有些特殊因为数组类本身并不是由类加载器负责创建而是由JVM在运行时根据需要而直接创建的但数组的元素类型仍然需要依靠类加载器去创建。创建数组类下述简称A的过程
如果数组的元素类型是引用类型那么就遵循定义的加载过程递归加载和创建数组A的元素类型JVM使用指定的元素类型和数组维度来创建新的数组类。如果数组的元素类型是引用类型数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public。
2 链接阶段Linking
2.1 验证Verify
它的目的是保证加载的字节码是合法、合理并符合规范的。
验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证以及符号引用验证等。
当类加载到系统后就开始链接操作验证是链接操作的第一步。格式验证会和装载阶段一起执行。符号引用验证在解析环节才会执行。
2.2 准备Prepare
为类的静态变量分配内存并将其初始化为默认值。
注意区分这里虽然用了初始化是给变量赋默认值但是后面还有初始化阶段是给变量显式赋值这两个初始化是不同的概念
private static int num 1;
// 这个静态成员变量在准备阶段会赋0初始化阶段赋1注意
这里不包含基本数据类型的字段用static final修饰的情况即常量因为final在编译的时候就会分配了准备阶段会显式赋值。注意这里不会为实例变量即非静态变量分配初始化实例变量是会随着对象一起分配到Java堆中。在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。Java并不支持boolean类型对于boolean类型内部实现是int,由于int的默认值是0,故对应的boolean的默认值就是false。
说明常量在编译阶段会存入到调用这个常量的方法所在类的常量池中。
// 即对于下面这个字符串常量在编译阶段就已经为其分配好了内存和值在字节码文件加载后直接将其显式赋值
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);}
}// 输出 hello world
// 但也有例外情况如果常量的值并不是字面量并非编译期间可以确定的那么其值就不会被放到调用类的常量池中这时在程序运行时会导致主动使用这个常量所在的类显然会导致这个类会初始化。在初始化阶段才会显示赋值clinit()方法
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
//4e5bc60b-ec26-40c1-aeea-a5eddcb2dbaf2.3 解析Resolve
将类、接口、字段和方法的符号引用转为直接引用。
1.具体描述
符号引用就是一些字面量的引用和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在Class类文件中通过常量池进行了大量的符号引用。但是在程序实际运行时只有符号引用是不够的比如当如下println()方法被调用时系统需要明确知道该方法的位置。
以方法为例Java虚拟机为每个类都准备了一张方法表将其所有的方法都列在表中当需要调用一个类的方法的时候只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作符号引用 -- 目标方法在类中方法表中的位置从而使得方法被成功调用。
2.小结
所谓解析就是将符号引用转为直接引用也就是得到类、字段、方法在内存中的指针或者偏移量。因此可以说如果直接引用存在那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用不能确定系统中一定存在该结构。
不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot VM中加载、验证、准备和初始化会按照顺序有条不紊地执行但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行解析操作并不一定按照上述顺序执行。
3 初始化阶段Initializing
始化阶段简言之为类的静态变量赋予正确的初始值。(显式初始化)
具体描述
类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题那么表示类可以顺利装载到系统中。此时类才会开始执行Java字节码。即到了初始化阶段才真正开始执行类中定义的 Java 程序代码代码块、静态代码块、构造器。或者说此时才是开发者可控的范围之前的都由虚拟机自动完成
clinit()方法
初始化阶段的重要工作是执行类的初始化方法clinit()方法。该方法仅能由Java编译器生成并由JVM调用程序开发者无法自定义一个同名的方法更无法直接在Java程序中调用该方法虽然该方法也是由字节码指令所组成。它是由类静态成员的显示赋值语句以及static语句块合并产生的。静态变量不赋值、非静态变量、常量都不能生成clinit()方法一个类的clinit()方法在多线程执行时会加同步锁保证只有一个线程执行这个方法如果clinit()方法中有较耗时的内容将会造成阻塞无法执行初始化后的内容。但一般来说一个类只会初始化一次。
clinit() : 只有在给类的中的静态变量显式赋值或在静态代码块中赋值了才会生成此方法这个方法不是必需的。 init() 一定会出现在Class的method表中。非静态变量的显式赋值、非静态代码块、构造器
// clinit
private static int a 1;
static{a2;
}
/*
* a1会被a2覆盖静态代码块和变量赋值同等优先度按顺序执行
* 但由于只能访问在它之前被定义的静态变量所以一般静态代码块晚于静态变量赋值
* 对于非静态变量的显式赋值与非静态代码块的执行顺序也是如此
*/注意非静态成员变量在初始化时不会分配内存空间也不会赋默认值。非静态成员变量只有在类被创建实例对象的时候才会一起创建。如果是由于创建对象触发的类的加载则会在初始化后紧接着创建类的实例和成员变量并对变量赋默认值。然后相继执行非静态变量、构造方法对非静态变量进行初始化。注意区分类的初始化和创建类的实例。
在静态代码块中只允许有局部变量和静态变量不允许操作非静态成员变量。
常量如果被赋字面量的值在链接的准备阶段就被赋值在初始化阶段不再需要clinit来初始化但如果常量被赋的值不是字面量需要初始化阶段才能确定则会延后到初始化阶段赋值。
触发类的加载、初始化的时机
主动使用完成装载、验证、准备和初始化一套流程解析可能会在运行时才开始
当创建一个类的实例时比如使用new关键字或者通过反射、克隆、反序列化。当调用类的静态方法时即当使用了字节码invokestatic指令。当使用类、接口的静态字段时(final修饰特殊考虑)比如使用getstatic或者putstatic指令。当使用java.lang.reflect包中的方法反射类的方法时。比如Class.forName(“com.atguigu.java.Test”)当初始化子类时如果发现其父类还没有进行过初始化则需要先触发其父类的初始化。
static class Parent{public static int A 1;static {A 2;}
}
static class Sub extends Parent {public static int B A;public static void main(String[] args){System.out.println(Sub.B);}
}// 初始化子类前先初始化父类父类的静态代码块先于子类的显式赋值的执行输出2如果一个接口定义了默认方法那么直接实现或者间接实现该接口的类的初始化该接口要在其之前被初始化。当虚拟机启动时用户需要指定一个要执行的主类包含main()方法的那个类虚拟机会先初始化这个主类。
特别说明初始化父接口的子接口或接口的实现类时并不会像父子类一样先初始化这个父接口**。只有调用接口的具体静态常量、静态方法或被实现的接口有默认方法时才会初始化。接口内没有常量外的其他变量也没有起初始化作用的代码块
执行的主类在运行前会先完成类的装载、验证、准备和初始化然后在程序的入口main()开始执行main()方法内使用到其他的类同样会触发类的加载。也即主类的静态代码块会早于main()执行但同上面所说的非静态代码块会在创建实例时才会执行。
static String a a;
static {aaa;
}
{aaaa;
}
public static void main (String[] args){System.out.println(a);
}
// 输出 aa
// 如果main()中加入aaaaa则最终输出aaaa但无论如何不会输出aaa因为没有创建实例。被动使用类可能被加载但不会初始化。
当访问一个静态字段时只有真正声明这个字段的类才会被初始化。当通过子类引用父类的静态变量不会导致子类初始化。通过数组定义类引用不会触发此类的初始化。引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。调用ClassLoader类的loadClass方法加载一个类并不是对类的主动使用不会导致类的初始化。
被动的使用意味着不需要执行初始化环节意味着没有clinit()的调用。
4 类的卸载
通过前面的几个阶段类已经可以被正常使用梳理一下这过程中产物
装载阶段将字节码文件读入生成对应类的Class实例堆中以及类的结构和常量方法区而其实这个过程是由类的加载器Class Loader实现的所以在此之前会先生成加载该类的加载器对象堆中。链接阶段和初始化阶段主要是除了给变量赋值没有产生新的东西。
如果初始化阶段一并new了类的对象则会生成类的实例对象类的成员变量等结构。
堆中一共产生了3个类的对象这三个对象之间也有关联关系同时也可以用变量去引用它们如下图所示。 那么什么时候回触发类的卸载呢当类卸载后代表该类的Class对象不再被引用或unreachable其在堆空间和方法区内的数据也会被清空。
引用变量可以为null类的实例对象也可以被销毁但是由于Class类对象被加载器引用一般除特殊处理类是被系统加载器加载而系统加载器在JVM运行期间很难会不再使用所以类的卸载往往是不确定的也很难会被卸载。
所以类的卸载需要同时满足下面三个条件
该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景如OSGi、JSP的重加载等否则通常是很难达成的。该类对应的java.lang.Class对象没有在任何地方被引用无法在任何地方通过反射访问该类的方法。
注意Java虚拟机只是允许对满足上述三个条件的无用类进行回收这里说的仅仅是“被允许”而并不是和对象一样没有引用了就必然会回收。
可以得到的结论是一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。也因此开发者在开发代码时候不应该在对虚拟机的类型卸载做任何假设前提下来实现系统中的特定功能不应该设计一种代码逻辑是在类卸载后执行因为往往是不可靠的。也正因为类很难被卸载所以类虽然可以被创建多个实例但往往只加载一次。