在Java中,类型(Class、interface、枚举等,不是对象)的加载、连接和初始化过程都是在程序运行期间(Runtime)完成的。更为灵活。(注意是运行期)
最终产品就是Class对象。
类的加载指的是将class文件中的二进制数据读入到内存,将其放入运行时数据区的方法区,然后在内存中创建一个java.lang.Class对象(HotSpot虚拟机将Class对象放在了方法区中,但是一般来说Class应该在堆区),这个Class对象用来封装类在方法区中的数据结构。
方法区: 各个线程共享,存储被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码 等数据。
将已经存在的class文件从磁盘加载到内存。
虚拟机参数 :
-XX:+TraceClassLoading 用于追踪类的加载信息并打印
-Xms512M -Xmx4096M JVM初始内存,JVM最大可用内存
类与类之间的关系。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
确保被加载的类的正确性。
为类的静态变量分配内存,并将其初始化为默认值。
例如:
实际上是先在连接阶段中的准备阶段赋值为0(默认值),再在初始化阶段赋值为1。
class Test {
public static int a = 1;
}
把类的符号引用转换为直接引用。主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符类符号引用进行。如在A类中a方法中引用了B类中的b方法,将b方法放入A类中
为类的静态变量赋予正确的初始值。按照代码从上到下的顺序。
例如1.2.2小节。
当JVM初始化一个类时,要求他的所有父类都已经初始化了。但不适用于接口。如何理解:
因此,一个父接口不会因为他的子接口或者实现类的初始化而初始化,只有程序调用特定接口的静态变量的时候,才会导致该接口的初始化。
Java程序对类的使用方式有两种:
所有的jvm实现必须在每个类或者接口被Java程序首次主动使用时才初始化他们。是否初始化不影响是否被加载和连接
实例参考:Java 中对类的主动引用和被动引用
被动调用的一个有意思的点:
final常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类(被调用类),因此不会触发定义常量的类的初始化。甚至可以在编译之后删除被调用类的class文件。
但是: 如果如下,则被调用类还是会初始化:
public static final String str = UUID.randomUUID().toString();
因为无法在编译期就确定str的值,也就不能把str放入到常量池。
public class Test{
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; // here
private static Singleton singleton = new Singleton();
private Singleton(){
counter1++;
counter2++;
}
public static Singleton getInstance(){
return singleton;
}
}
会输出:
但是,如果把public static int counter2 = 0;
换个位置,如下所示,输出什么?
public class MaximumSubarray53 {
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; // here
public static Singleton getInstance(){
return singleton;
}
}
准备阶段赋给默认值0或者null。
在初始化阶段会从上到下依次执行代码,新的值覆盖掉了默认值。
参考: B站视频
卸载:当类A被加载连接初始化后,生命周期开始。当代表类A的Class对象不再被引用(即不可触及)时,Class对象就会结束生命周期,类A在方法区的数据也会被卸载,从而结束类A的生命周期。
静态初始化块 > 初始化块 > 构造器
父类 > 子类
综合下来顺序就是:
父类静态初始化块
子类静态初始化块
父类初始化块
父类构造器
子类初始化块
子类构造器
需要注意静态初始化块是在类第一次加载的时候就会进行初始化。
参考 类加载器工作原理
看起来是继承,实际上是“包含”,下面包含了上面
System.getProperty("sun.boot.class.path");
System.getProperty("java.ext.dirs");
System.getProperty("java.class.path");
只有“初始化”才需要首次主动使用。
JVM允许类加载器在预料某个类将要被使用时就预先加载它。
如果预先加载的过程中找不到class文件,则仅仅在首次主动使用时才报LinkageError错误。
这就是和Class.forName()的区别
更好的保证Java平台的安全。
除了根加载器外,其他加载器都有且仅有一个父加载器。当程序要求加载器X加载Sample类时,X会去先委托父加载器去加载Sample类,如果父加载器不能加载才会让X发挥作用。
并不是所有的虚拟机实现都是这样的机制。
好处:
- 可以确保Java核心库的类型安全:例如,Object类总是会被加载,但如果是自定义加载器来加载的Object类,那么很可能会有多个版本的Object类,这些类之间还不兼容,互相不可见(命名空间的原因)。
- 确保Java核心类库不会被自定义的类所替代:例如自定义了一个Object类,但根类加载器会先去加载官方的Object,之后系统加载器就不会去加载自定义的Object了。
- 不同的类加载器可以为相同名字的类(binary name)创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的加载器来加载即可。相当于创建了多个相互隔离的类空间。
代码实例:
package JVM;
public class TestClassLoader {
public static void main(String[] args) throws Exception {
/**
* clazz.getClassLoader()
* 返回加载了这个对象所代表的类(或者接口)的类加载器。
* 如果返回null,则代表是根类(启动类)加载器(或者其他情况)
*/
Class<?> clazz = Class.forName("java.lang.String");
System.out.println(clazz.getClassLoader());
Class<?> clazz2 = Class.forName("JVM.C");
System.out.println(clazz2.getClassLoader());
// sun.misc.Launcher$AppClassLoader@18b4aac2 系统类加载器(应用类加载器)
}
}
class C {
}
上面的例子中,如果用getClassLoader得到的loader再去执行
loader.loadClass("某个类的全限定名称");
不会导致该类的初始化:
clazz.getClassLoader();
Thread.currentThread().getContextClassLoader();
ClassLoader.getSystemClassLoader()
DriverManager.getCallerClassLoader()
前面所说的一个类不能加载两次,是有前提的,也就是必须在同一个命名空间中。
在运行期,一个Java类是由该类的完全限定名称+加载该类的加载器所共同决定的。如果相同名字的类由两个不同的加载器所加载,那么这些类也是不同的,即便class文件的字节码完全一样,并且从相同的文职加载亦如此。
sout(Thread.currentThread().getContextClassLoader());
sout(Thread.class.getClassLoader);
输出:
AppClassLoader
null (代表根类加载器)
1.当前类加载器(Current ClassLoader)指加载了当前类的加载器。
每个类都会使用自己的类加载器去加载其他所依赖的类。
如果ClassA
引用了ClassY
,那么ClassX
的加载器就会尝试去加载ClassY
(前提是ClassY尚未被加载)。
2.线程上下文类加载器(context classloader)
Thread类中有context classloader的set和get方法。如果没有通过set方法来设置的话,线程降级成府县丞的上下文类加载器。
Java的初始线程的上下文加载器是系统类加载器。
线程上下文类加载器的重要性:
SPI(service provider interface)
父加载器可以使用当前线程Thread.currentThread().getContextClassLoader()
所指定的上下文加载器所加载的类。
这就改变了父classloader不能使用子class loader或是其他没有直接父子关系的加载器所加载的类的情况,即改变了双亲委托模型。
线程上下文类加载器就是当前线程的current classloader。
注意接口和接口实现类:
在双亲委托模型下,下层的类加载器回去委托上层进行加载。但是对于SPI来说,有些接口是Java核心库提供的,而核心库是启动类加载器来加载的,接口的实现缺来自于不同的jar包(厂商提供),启动类加载器不会加载其他来源的jar包,这样双亲委托机制就无法满足SPI的要求。而通过给当前线程设置上下文加载器,就可以有上下文加载器来实现对于接口实现类的加载。
会显示什么?
public class Father{
private int i = test();
private static int j = method();
static{
System.out.print("(1)");
}
Father(){
System.out.print("(2)");
}
{
System.out.print("(3)");
}
public int test(){
System.out.print("(4)");
return 1;
}
public static int method(){
System.out.print("(5)");
return 1;
}
}
public class Son extends Father{
private int i = test();
private static int j = method();
static{
System.out.print("(6)");
}
Son(){
// super();//写或不写都在,在子类构造器中一定会调用父类的构造器
System.out.print("(7)");
}
{
System.out.print("(8)");
}
public int test(){
System.out.print("(9)");
return 1;
}
public static int method(){
System.out.print("(10)");
return 1;
}
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}
第一行的Son s1 = new Son();
是对Son类的首次主动使用,需要初始化Son类,但其继承了Father,所以需要先初始化Father类。
接下来是实例的初始化。首先是父类:
调用子类的test,输出9
运行父类的非静态代码块,输出3
调用父类的构造函数,输出2
子类实例的初始化:
同上一步,输出9,8,7。
此时第一行的Son s1 = new Son();
执行完毕。
接下来第二个Son s1 = new Son();
:
因为不再是首次主动使用了,所以不会进行类的初始化,直接是实例的初始化。步骤和 4 , 5 一致,输出9,3,2,9,8,7。
javap -c 或者 -verbose xxx
。.class
后缀省略。.class
文件的前四个字节,永远是CAFEBABE
。java -version
来验证。