目录
类加载的过程
加载
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。
链接(验证->准备->解析)
验证:确保加载的类信息符合JVM规范,没有安全方面的问题。
准备:正式为类变量(static)分配内存并设置类变量默认值的阶段,这些内存都将在方法区中进行分配。
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化
执行类构造器<clinit>()方法的过程,类构造器<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
当初始化一个类的时候,如果发现其父类还没有被初始化,则需要先初始化其父类。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
类加载器
作用
类加载器的作用就是将class文件字节码装载进内存中(其中的class文件是java编译器将源程序的.java文件编译而来),并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口,也就是上文中加载这步的实现。
过程
测试
package com.shao;
/**
* 测试家ClassLoader
*/
public class TestClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
//获取系统加载器(AppClassLoader)
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("appClassLoader是:"+appClassLoader);
//获取系统加载器的父类加载器扩展类加载器(ExtClassLoader)
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("extClassLoader是:"+extClassLoader);
//获取扩展类的父类加载器根加载器
ClassLoader parent = extClassLoader.getParent();
System.out.println("ExtClassLoader的父类加载器:"+parent);
System.out.println("-----------------------------------");
//测试当前类的加载器
ClassLoader classLoader = Class.forName("com.shao.TestClassLoader").getClassLoader();
System.out.println("当前类的加载器是:"+classLoader);
//测试JDK的内置类的加载器
String str = new String();
ClassLoader classLoader1 = str.getClass().getClassLoader();
System.out.println("String类的加载器是:"+classLoader1);
}
}
测试结果
分析:1.所有的加载器都是抽象类ClassLoader的实现类。
2.App ClassLoader 的父类 Extension ClassLoader;Extension ClassLoader 的父类Bootstap ClassLoader,测试中Bootstap ClassLoader获取不到,因为该加载器规定就是获取不到,对应我们从资料中所得的结论。
类加载的机制(双亲委派机制)
类加载的机制是,当一个类的加载器接收到一个加载请求的时候,它不会去加载而是调用它的父类加载器去加载,每层的加载器都是如此,直到委托给最顶层的启动类加载器位置,如果其找不到这个类才会一次退回给让下一层的加载器来加载。这个加载的机制就是双亲委派机制,这个机制可以有效的避免一些不安全因素。例如自己手写一个rt.jar包中的String类,但是用的时候一直用的都是rt.jar中的lang包下的String。
什么时候会初始化类
分析
在类的加载过程中当我们写好一个java类之后点击build或者运行其中的main方法,首先java会将.java文件编写成JVM能识别的.class文件,然后加载,准备。到这一步完成其实类并没有被初始化,除非是有些因素导致需要初始化这些类,JVM才会去初始化这些类。
初始化
类的主动引用(一定会发生类的初始化)
1)当虚拟机启动时,先初始化main方法所在的类
2)new一个类的对象
3)调用类的静态成员(除final常量)和静态方法
4)使用反射对类进行反射调用
5)当初始化一个类,如果父类没有被初始化,则会先初始化它的父类
类的被动引用(不会发生类的初始化)
1)当访问一个静态域时,只有真正声明这个域的类才会被初始化。例如:子类引 用父类 的静态变量,不会导致子类初始化
2)通过数组定义类引用,不会触发此类的初始化
3)引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中)
测试
测试每个上面的例子,并贴出运行结果
package com.shao;
public class Test{
static {
System.out.println("---->Test类的静态代码块");
}
public static void main(String[] args) throws ClassNotFoundException {
//会被初始化
// 1)当虚拟机启动时,先初始化main方法所在的类
/*运行结果 ---->Test类的静态代码块*/
// 2)new一个类的对象
//Parent parent = new Parent();
/*运行结果
---->Test类的静态代码块
---->Parent类的静态代码块
---->Parent类的无参构造器*/
// 3)调用类的静态成员(除final常量)和静态方法
//System.out.println(Son.num)
/*运行结果
---->Test类的静态代码块
---->Parent类的静态代码块
10
*/
// 3)调用类的静态成员final常量
// System.out.println(Son.A);
/*运行结果
---->Test类的静态代码块
12
*/
// 4)使用反射对类进行反射调用
//Class.forName("com.shao.Parent");
/*运行结果
---->Test类的静态代码块
---->Parent类的静态代码块
*/
// 5)当初始化一个类,如果父类没有被初始化,则会先初始化它的父类
//Son son = new Son();
/*运行结果
---->Test类的静态代码块
---->Parent类的静态代码块
---->son类的静态代码块
---->Parent类的无参构造器
---->Son类的无参构造器
*/
//不会被初始化
// 1)当访问一个静态域时,只有真正声明这个域的类才会被初始化。例如:子类引用父类 的静态变量,不会导致子类初始化
//System.out.println(Son.num);
/*运行结果
---->Test类的静态代码块
---->Parent类的静态代码块
10
*/
// 2)通过数组定义类引用,不会触发此类的初始化
//Parent [] parents = new Parent[10];
/*运行结果
---->Test类的静态代码块
*/
// 3)引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中)
//System.out.println(Parent.A);
/*运行结果
---->Test类的静态代码块
12
*/
}
}
class Parent {
static int num=10;
final static int A=12;
static {
System.out.println("---->Parent类的静态代码块");
}
public Parent() {
System.out.println("---->Parent类的无参构造器");
}
}
class Son extends Parent{
static int ber=20;
static {
System.out.println("---->son类的静态代码块");
}
public Son(){
System.out.println("---->Son类的无参构造器");
}
}
初始化的顺序
详细顺序
(自上而下)
父类静态代码块/父类静态属性(根据顺序执行)
↓
子类静态代码块/子类静态属性(根据顺序执行)
↓
父类代码块/父类属性(根据顺序执行)
↓
父类构造器
↓
子类代码块/子类属性(根据顺序执行)
↓
子类构造器
测试
package com.shao;
public class TestSort{
public static void main(String[] args) {
System.out.println("--------main开始--------");
B b = new B();
System.out.println("--------main结束--------");
}
}
class A {
static int num = 100;
private int a=1;
static {
System.out.println("---->A的静态代码块");
}
{
System.out.println("---->A的代码块");
}
public A(){
System.out.println("---->A的无参构造器");
}
}
class B extends A{
static int aum = 200;
private int b=2;
static {
System.out.println("---->B的静态代码块");
}
{
System.out.println("---->B的代码块");
}
public B(){
System.out.println("---->B的无参构造器");
}
}
分析:所测试的结果和查找的资料完全对照。
心得:这次是在学习狂神说java老师的注解和反射的时候看到扩展的jvm知识,刚开始感觉一团乱,但慢慢梳理和查找资料逐渐看懂其中的意思。总的来统一下这个blog都写什么,首先.java文件会被java编译器编译成.class文件存在方法区中在内存堆中生成一个class对象,这个时候在细一代点就要知道双亲委派机制,这一步完成的也就是加载然后进行链接,链接分为三个主要步骤检验.class文件是否不安全有什么错误----->为静态变量开辟内存空间并赋初始值---->解析;这时候jvm就等着指令初始化类了,问题就来了,什么时候初始化、什么时候不初始化,初始化的顺序以此类推来详细学习。总的学习来感觉jvm不是为了麻烦而故意这样做,相反设计的时候应该是想要这种加载效果,因为总体来看这种类的加载过程机制会让我们的内存最优化,不至于浪费内存,需要的时候在初始化用不到的时候我就给你放一边,重复用我就给你想办法让你能不用每次都加载,我觉得这才是设计java的时候设计者的用心吧。