【JVM】从广度、深度,二维理解“类加载机制”

一、一道被我DIY的面试题

       那我自己做个demo,Vincent类定义了我自己的基本信息,年龄和体重,可以看到当前我25岁,体重130斤。当调用我的构造方法的时候,就代表着我过生日又要被“构造”一次,年龄涨一岁了,但是呢,我的体重预计会减5斤,具体代码如下:

       代码段1:

package com.basic.jvm;

class Vincent
{
	//定义Vincent的基本属性:体重和年龄
	public static int age = 25;
	public static int weight = 130;
	private static Vincent vincent = new Vincent();
	
	private Vincent(){ //过25岁生日时候会调用这个方法
		weight -= 5;
		age++;
	}
	
	public static Vincent getVincent(){
		return vincent;
	}
}

public class Test1 {
	public static void main(String[] args) {
		Vincent vincent = Vincent.getVincent();
		System.out.println("Vincent's Age = " + vincent.age);
		System.out.println("Vincent's Weight = " + vincent.weight);
		
	}
}

    代码段2:

    基本同上,在Vincent类中,仅仅是三个静态的变量的顺序换了换,如下:

class Vincent
{
	//定义Vincent的基本属性:体重和年龄
	private static Vincent vincent = new Vincent();
	public static int age = 25;
	public static int weight = 130;
        ……
}
public class Test1{
        ……
}
       就是酱紫啊,这个题让你猜一猜两个代码段,分别会输出个啥子???

       先不要看答案,自己在小本本上画一画,想一想。(当时遇到这种类似题目时候,我简直一脸懵逼啊,知道我学过了JVM的类加载过程)

       下面先公布一下答案:

       代码段1:

       
      代码段2:

      

      我滴天哪!!! 为啥不一样!!!(往下看,一会儿自己就清楚了)


二、说说“CL”流程

       1.这个“JVM 类加载(CL)”到底什么鬼?

        通俗地讲,“类加载"的本质,就是把.class的类文件,加到内存中。所有的类加载机制,都是在围绕着我写的这句红字,做文章。


       2.既然加载到内存,会加载到内存的哪里?

       如果你没有看过我写的上一篇博客,请先点击这里:【JVM】学习总览 , 在这篇文章中,我有一张JVM的4大组成部分的图,看到那个黄色背景的小方块了吗?这个过程就是"Class Load"的过程,在这里,我还要再贴一幅图:

       

        这张图,给我的感觉,有点像当年我在“安次区医院”拍CT的感觉,JVM的五脏六腑都被看的透透彻彻了。看到红色的框了吗?这就是Class Load过程所会涉及的java内存区域(内存分析,会在明后天出炉)。

       

      至此,已经介绍过了Class Load的what?和where?属性了,接着简单聊一聊它的过程

      类加载的过程,基本上会经历3大阶段:

      1)加载

      2)连接

            2.1)验证

            2.2)准备

            2.3)解析

       3)初始化

      如果用图像的形式展示的话,类加载的过程如下:

      

       如图,对三个阶段的浅显理解应该是这样:

       1.加载:把.class文件先以二进制数据的形式加载到内存当中;

       2.连接:(已经读到内存中的二级制数据合并到虚拟机的运行时环境中)

          2.1验证:确保被加载的类的正确性(可能.class文件不是通过javac生成的。。。。尴尬啊)

          2.2准备:为类的静态变量分配内存,并将其初始化为默认值(这个默认值,就是具体数据类型的默认值,比如int类型,为0)

          2.3解析:把类中的符号引用换位直接引用(如果现在看不懂,一会儿小张哥再给你画张图)

        3.初始化  :为类的静态变量赋予正确的初始值(用户给的值,比如int age = 25,就是这个阶段把这个25真正赋予age变量)


       至此,类加载最核心的3个过程,你已经有了浅显的认识,其实,这3个核心过程,仅仅是类的生命周期中的一部分,结合整个类的生命周期,如图:

       

       看到了吧,看到这张图,你就同时把握住了“Class的一辈子”的全局观。      



三、品品细节特色

       如上所示,看完这些文字,你已经基本了解了Class Load是怎么回事,也模糊地了解到了类加载时,数据会存在内存中的运行时方法区,但是知道这些还远远不够!!!接下来,咱们再往深处潜一潜。

       1.加载

       一张导图:

       

       如图所示,对于“类加载”小编将从这几个角度进行总结。

       1)原理

       1、类的.class文件中的二进制数据读入内存.
       2、放到运行时数据区的方法区.
       3、堆区创建一个java.lang.Class对象(唯一一个),用来封装类在方法区内的数据结构。(反射的入口)(import)

       

       再结合上图,java程序对内存中类对象的调用,其实是从堆区到方法区的映射,通过反射来解决问题。(类加载的最终产物,是堆区的类对象,类对象封装了类在方法区的数据结构,并向java程序员提供了访问方法区内的数据结构接口


       2)方式(做了解)

      常见的类加载方式由如下几种:

      1.从本地系统中直接加载,比如直接java命令的执行;(常用

      2.通过网络下载.class文件;

      3.从zip,jar等归档文件中加载.class文件;(常用

      4.从专有数据库中提取.class文件 (这个没有见过)

      5.将java源文件动态编译为.class文件


      3)时机

       至于何时加载.class文件,JVM有一个很好的机制,值得一提。

       1.JVM规范允许类加载器在预料某个类将要被使用时就预先加载他,如果在预先加载过程中遇到.class文件缺失或者错误,类加载器必须在程序首次主动使用该类时,才报告错误(LinkageError错误)。

       2.如果这个类一直没有被程序主动使用,so,类加载器就暂时不报告错误。

       时机就是预先加载,我个人比较欣赏的是他的报错机制,我想他的设计师一定是一个很有包容精神、大度的人。

       

      4)类加载器

      说了一堆类加载,如果没有类加载器,.class根本就进不去内存,就像该文中第一张图所示那样。类加载器可以分为两大类:

      1.Java虚拟机自带的加载器

            1.1 根类加载器(Bootstrap)

            1.2 扩展类加载器(Extension)

            1.3 系统类加载器(System)

      2.用户自定义的类加载器

            2.1 java.lang.ClassLoader的子类

            2.2 用户可以定制类的加载方式


       代码展示:

package com.basic.jvm;

public class Test1
{
	public static void main(String[] args) throws Exception
	{
		Class clazz = Class.forName("java.lang.String");
		System.out.println(clazz.getClassLoader());
		
		Class clazz2 = Class.forName("com.basic.jvm.C");
		System.out.println(clazz2.getClassLoader());
	}
}

class C
{

}
       如上代码所示,通过反射的形式动态加载了“String”类和“C”类,得到的结果如图:

       
       跟了JDK中Class类源码,返回null,就是使用“根加载器”的结果。

       这块的深度,暂时先知道名词,知道分类,同时结合加载时机,知道这个过程何时报错,就先ok。


     2.连接

      一张导图:

     

      按照3阶段顺序:

      2.1验证

      1.为什么验证?

      这就不得不说上一步“加载”的事情了,参考加载方式,都是正常情况下的.class文件,但是不妨会有手动编辑成的.class文件,或者被修改过的,这个时候,难免会有一些错误。So,此时,就需要进行一下验证了。

      通常情况,验证包含这么些个内容:

      1)类文件的结构检查:遵从java类文件格式;

      2)语义检查:遵从java语法规定,如验证final类型的类没有子类,等;

      3)字节码验证:确保字节码流可以被虚拟机安全执行;

      4)二进制兼容性验证:保证互相引用的类之间协调一致。  


      2.2准备

      1.这个阶段干点啥?

      其实准备阶段,1JVM为类的静态变量分配内存,2并设置默认的初始值。举个栗子:

public class Celine{
	private static int age = 24;
	public static float weight; //女生体重要保密哦~

	static{
		weight = 52; //单位理解为Kg.
	}
	…
}
      如上程序,是Celine这个女生的两个属性描述:1.年龄,2.体重;在准备阶段,首先为int类型的变量age分配了4个字节的内存空间,并赋予默认值0,为float类型的变量weight分配4个字节的内存空间,并赋予默认值0.0;
   

      2.3解析

       1.解析啥?

       在解析阶段,Java虚拟机会把类的二级制数据中的符号引用替换为直接引用


       2.啥是符号引用,啥事直接引用?

       画一个图:

       

       如上,在对象A中,引用b.work()方法,而解析的过程,就是把b.work()这个符号引用,变成一个类似指针的东西,直接在内存指向b对象(类)中的work方法区域。

       原理性的东西,我现在认为,最好的办法,就是用图,把它画出来。


     3.初始化

     一幅导图:

      

      As shown in this figure:

      1.初始化干了啥?

      Java虚拟机执行类的初始化语句,就是为类的静态变量赋予初始值。比如“private int age = 25;”,这个阶段,真正的把25这个数,赋给了age这个变量。

      

       2.途径?

      (1)在静态变量的声明处进行初始化;

      (2)在静态代码块中进行初始化。

       比如代码:

public class Vincent
{
	private static int age = 25;
	public static float weight;
	public static int grade;
	
	static{
		weight = 65.0;
	}
}
       最终结果:age = 25; weight = 65.0; grade = 0; 可见,最终被初始化的变量是age和weight,对于grade,我没有给他值。


      3.步骤

      1)类是否加载、连接,如果没有,加载、连接。

      2)若类A有直接父类,且父类没有被初始化,先初始化父类。

      3)如果类中存在初始化语句,那就依次执行这些初始化语句。

      代码:

public class Sample{
    static int a=1;  
    static{a = 2;}
    static{a = 4;}  //执行过程有4步。(赋值时候,分别赋值为0.1.2.4)
}
      如上,依次执行代码块中的初始化过程,最终 "a = 4".


      4.时机

      发现,无论是初始化,还是之前的其他阶段,都会讨论“时机”这个概念,这里,不得不引申出一个新的概念:“java程序对类的使用方式”。正如思维导图中所示,“使用方式”可以分为两大类,一类是:“主动使用”,一类是:“被动使用”。

      1)主动

1.创建类的实例
2.访问类的静态变量,或者复制
3.调用类的静态方法
4.反射
5.初始化一个类的子类
6.java虚拟机启动时被标明为启动类的类(比如,一个Test.class文件中,同时还有 Parent类,Teacher类,启动Test的过程  Java Test)
      对应代码展示:
new Test();
int b = Test.a;
Test.a = b;
Test.doSomething();
Class.forName("com.tgb.jvm.Test");
以及
class Parent{}
class Child extends Parent{
	public static int a = 3;
}
Child.a = 4;
以及
java com.tgb.jvm.Test;
       2)被动

       除了主动,剩下的,都是被动。


       理解:“所有的java虚拟机实现必须是在每个类或接口被java程序“首次主动使用”时才初始化他们。

       这句话,形象地描述了类初始化的时机,关键词“首次主动”一句道破天机,初始化这个过程,要么不执行,要么只执行一次,就是在该类被java程序首次主动使用,结合上面的“主动使用”概念,一切都清晰了。

       但是初始化时机,如果遇到了接口,就要注意了:

1在初始化一个类时,并不会先初始化它所实现的接口。
2在初始化一个接口时,并不会先初始化它的父接口。


       如果有父类,初始化时,先初始化父类,再是子类。

class Parent
{
	static int a = 1;
	static 
	{
		System.out.println("Parent static block");
	}
}

class Child extends Parent
{
	static int b = 2;
	static
	{
		System.out.println("Child static block");
	}
}

public class ClassLoader1 {

	static
	{
		System.out.println("ClassLoader static block");
	}
	
	public static void main(String[] args) {
		Parent parent; //这里暂时没有输出Parent中的内容 -- 没有创建实例
		
		System.out.println("----------------");
		
		parent = new Parent(); //对parent的主动使用 
		
		System.out.println(Parent.a);
		
		System.out.println(Child.b);
	}

}

     执行结果:

     


     另外一个程序:

class Parent2
{
	static int a = 3;
	
	static 
	{
		System.out.println("Parent2 static block");
	}
	
	static void doSomething(){
		System.out.println("do something");
	}
}

class Child2 extends Parent2
{
	static 
	{
		System.out.println("Child2 static block");
	}
}

public class ClassLoader2 {

	public static void main(String[] args) {
		System.out.println(Child2.a);
		
		Child2.doSomething();
	}
}

      执行结果,如图:

     

       注意:没有执行Child2中的static里的语句。因为:“只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用”。 即,静态变量Child2.a,静态方法doSomething都不是Child2所拥有的。这样就不能看做是主动使用,这块很是零碎,小张哥觉得不常用的话,肯定也是蒙蒙的,所以总结下来,以后备用!!!


      4.对于常量

      如果是常量,判断它是否会被初始化,这个时候,参考的因素,就不再是“主动访问”了,而是“是否编译常量”!

      看代码:

class FinalTest
{
	public static final int x = 6/3;
	
	static
	{
		System.out.println("FinalTest staic block");
	}
}

public class Test2
{
	public staitic void main(String[] args){
		System.out.println(FinalTest.x);
	}
}
       此时,执行结果返回值为“2”;


       另外一段代码:

class FinalTest2
{
	public static final int x = new Random().nextInt(100);
	
	static
	{
		System.out.println("FinalTest2 staic block");
	}
}

public class Test3
{
	public staitic void main(String[] args){
		System.out.println(FinalTest.x);
	}
}
      执行结果:

       
      可见,根据x是否是编译时常量,判断是否对类进行初始化。
     
     

四、聊聊注意事项

      就一点:通过ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化!!!

package JVM;

class CL
{
	static
	{
		System.out.println("Class CL");
	}
}

public class ClassLoader3 {

	public static void main(String[] args) throws Exception
	{
		ClassLoader loader = ClassLoader.getSystemClassLoader();
		
		Class<?> clazz = loader.loadClass("JVM.CL"); //调用loadClass时,不进行加载
		
		System.out.println("---------------------");
		
		clazz = Class.forName("JVM.CL");  //调用反射时,会加载该类
	}

}
       执行结果:

       


五、小结

       洋洋洒洒,这是我写过最长的一篇博客了,写的过程中,也在追踪溯源地想,这个类加载的核心是什么?结合开始的那幅图,以及几个子标题中的思维导图,就能把控住所有,3个大过程,注意“静态”二字,我感觉是类加载过程中的重中之重啊。

       最后,分析一开始的源程序:

    

       一旦,new Vincent语句和为weight和age赋值语句倒置,结果就不一样了,道理一样,就不分析了。


       That's all.

       国庆节快乐!




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值