zoukankan      html  css  js  c++  java
  • 探秘Java类加载

    Java是一门面向对象的编程语言。

    面向对象以抽象为基础,有封装、继承、多态三大特性。

    宇宙万物,经过抽象,均可归入相应的种类。不同种类之间,有着相对井然的分别。

    Java中的类,便是基于现实世界中的类别抽象出来的。

    类本身表示一类事物,是对这类事物共性的抽象与封装。类封装了一类事物的属性和方法。

    类与类之间,有着不同的层级。

    以生物界中的分类为例,遵循“界门纲目科属种”的级别体系,人类(亦可称为“人种”)的层级体系是:动物界---脊索动物门---哺乳纲---灵长目---人科---人属---人种。

    从人种到动物界,依次继承父类的共有属性和方法,而且又独具形态。

    举例来说,动物都需要吃东西来维持生命所需的能量,同是吃东西,不同种类的动物各有特点。

    又譬如,动物界与植物界的一个关键区别是,能否移动。在动物界之中,都是移动,但是各子类的移动方式几乎互不相同。

    举例来说,人通过走路、奔跑、攀爬等来移动,鸟通过飞翔、两下肢等来移动,鱼则通过在水中漂游来移动等。这使得动物的移动功能丰富多彩。

    不仅如此,即便属于同一种类的个体,在表现出来的公有功能方面,也是各不相同。

    譬如,虽然同为人类,普遍具备说话的功能,但是每个具体的个人在说话时,音色又各自不同。

    我们生活的世界,就是这样丰富多彩。既有共性的东西,又有具体不同的风格。

    Java语言源于为解决现实世界中各种各样应用问题提供一整套解决方案。

    所以,我们生活的现实世界,乃至整个宇宙,深深地映射入Java语言中。

    世界与宇宙何其深邃与复杂,同样,Java的博大精深不言而喻。

    可以说,每个Java程序的运行,都是为了解决某个或某种应用问题而生。

    古人说“格物致知”,我们探秘Java程序运行的内在原理,有助于帮助我们深入认识Java世界的运行机制。

    每个Java程序,都离不开类和对象。

    所以,我们就从类加载说起。

    一、类的生命周期

    想象一下,你在Eclipse里写了一个Java程序,通过javac(Java编译器),将Java源代码编译为.class字节码文件。

    字节码文件静静地躺在你的电脑磁盘里,你要运行这个Java程序,就要去运行编译后的字节码文件。

    加载.class字节码文件到内存,形成供JVM使用的类,并到这个类从内存中销毁,这便是类的生命周期。

    总的来说,类的生命周期经过了如图所示的阶段:

     

     1.加载

    关于加载,其实,就是根据.class文件找到类的信息将其加载到方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类信息的入口。

    需要简单科普一下的是:Java程序运行起来时成为进程,操作系统需要为该进程分配内存空间。Java程序的进程会将所分得的内存空间再予以分区,主要有栈区(存储局部变量)、堆区(存储创建的对象)、方法区(存储类的方法代码,以及类的静态成员变量信息,还有常量池)、程序计数器(记录线程的执行信息)、本地方法栈(与 操作系统底层交互时使用)。如图所示:

    2.链接

    有的出处称为“连接”,若从英文单词“linking”判断,则翻译为“链接”比较合适。

    链接一般会与加载阶段和初始化阶段交叉进行。

    链接的过程由三部分组成:验证、准备和解析。
    (1)验证:该阶段是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    (2)准备:主要是为由static修饰的成员变量分配内存空间,并设置默认的初始值。默认初始值如下:

      ①8种基本数据类型的默认初始值是0。
      ②引用类型默认的初始值是null。
      ③对于有static final修饰的常量会直接赋值,例如:static final int x=10;则x默认就是10。
    (3)解析:就是把常量池中的符号引用转换为直接引用,也就是说,JVM会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

    3.初始化
    这是将静态成员变量(也称为“类变量”)赋值的过程。

    也就是说,只有static修饰的变量才能被初始化,执行的顺序是:

    父类静态域(静态成员变量)或者静态代码块,然后是子类静态域或者子类静态代码块。

    并非所有的类都会被初始化,只有那些被直接引用(主动引用)的类才会被初始化。在Java中,类被直接引用的情况有:

      ①通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法;
      ②通过反射方式执行以上三种行为;
         ③初始化子类的时候,会触发父类的初始化;
         ④作为程序入口直接运行时(也就是直接调用main方法);

    除了以上4种情况,其他使用类的方式叫做被动引用,被动引用不会触发类的初始化。

    被动引用举例:

    (1)子类调用父类的静态变量,子类不会被初始化,只有父类被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。

    (2)通过数组定义来引用类,不会触发类的初始化。

    (3)访问类的常亮,不会初始化类。

    4.使用

    类在使用过程中也存在三步:对象实例化、垃圾收集、对象终结。
    (1)对象实例化:就是执行类中构造函数的内容,如果该类存在父类,JVM会通过显式或者隐式的方式先执行父类的构造函数,在堆内存中为父类的实例变量开辟空间,并赋予默认的初始值;然后,引用变量获取对象的首地址,通过操作对象来调用实例变量和方法。
    (2)垃圾收集:当对象不再被引用的时候,就会被JVM虚拟机标上特别的垃圾标识,在堆区中等待被GC回收。
    (3)对象的终结:对象被GC回收后,对象就不再存在了,对象的生命也就走到了尽头。
    5.卸载
    这是类的生命周期中最后的一步。

    程序中不再有该类的引用,该类会被JVM执行垃圾回收,类在本次程序运行中的生命结束。

    二、双亲委派

    Java中的类加载存在层次性,一个重要的加载模型是双亲委派。

    先来看Java中类加载器的层次体系:

    什么是类加载器呢?

    简而言之,类加载器可以将.class字节码文件加载到JVM内存中的方法区形成类模板(或者称为该类的数据结构/镜像),并在堆区中产生Class对象。

    如果站在JVM的角度来看,只存在两种类加载器:

    1.启动类加载器(Bootstrap ClassLoader):

    也称为“根加载器”。由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。

    2.其他类加载器:

    由Java语言实现,继承自抽象类ClassLoader。如:
    (1)扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>libext目录或java.ext.dirs系统变量指定的路径中的所有类库。
    (2)应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况下,如果我们没有自定义类加载器,默认就是用这个加载器。通过在控制台打印(System.out.println(System.getProperty("java.class.path"));),可以看到应用程序类加载器加载的路径信息。如图所示:

    C:Program FilesJavajdk1.8.0_181jrelib
    esources.jar;
    C:Program FilesJavajdk1.8.0_181jrelib
    t.jar;
    C:Program FilesJavajdk1.8.0_181jrelibjsse.jar;
    C:Program FilesJavajdk1.8.0_181jrelibjce.jar;
    C:Program FilesJavajdk1.8.0_181jrelibcharsets.jar;
    C:Program FilesJavajdk1.8.0_181jrelibjfr.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextaccess-bridge-64.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextcldrdata.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextdnsns.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextjaccess.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextjfxrt.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextlocaledata.jar;
    C:Program FilesJavajdk1.8.0_181jrelibext
    ashorn.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextsunec.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextsunjce_provider.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextsunmscapi.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextsunpkcs11.jar;
    C:Program FilesJavajdk1.8.0_181jrelibextzipfs.jar;
    E:workspaceeclipsework_j2eejava1_8in
    

    双亲委派模型的工作过程是:

    如果一个类加载器收到类加载的请求,它会先判断这个类是否已经加载过,若已经加载过,就不再重复加载;若还未加载过,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,若该类加载器无父类加载器,则将加载请求委派给根类加载器。每个类加载器都是如此(根类加载器除外)。只有当父类加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子类加载器才会尝试自己去加载。
    Java在类加载中采用双亲委派模型有什么好处呢?

    使得Java类同其类加载器一起具备了一种带优先级的层次关系,从而保证了程序运行中类的唯一性。

    我们知道,程序运行起来时,每个类在堆内存中的Class对象仅有唯一的一个,不会引起程序运行中类的混乱,其根源在于Java类加载中采用的双亲委派模型。

    三、自定义类加载器

     有的时候,我们需要当前程序以外的class文件,这时,我们就需要自定义类加载器,对相应的class文件进行加载。

    自定义类加载器的步骤是:

    1.继承ClassLoader   

    2.重写findClass()方法

    3.调用defineClass()方法

    接下来自定义一个类加载器,加载E:/test下的Test2.class文件。

    Test2.class文件的源代码文件Test2.java:

    package bwie2;
    
    public class Test2 {	
    	public void say() {
    		System.out.println("Hello China");
    	}	
    }
    

     接着,创建自定义类加载器:

    package bwie;
    
    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.InputStream;
    
    public class MyCloassLoader2 extends ClassLoader {
    	private String classPath;// 要加载的类路径
    
    	public MyCloassLoader2(String classPath) {// 构造方法传参
    		this.classPath = classPath;
    	}
    
    	@Override
    	protected Class<?> findClass(String name) throws ClassNotFoundException {// 查找类
    		byte[] classData = getData(name);
    
    		if (classData == null) {
    			//若字节码为空,则抛出异常
    			throw new ClassNotFoundException();
    		} else {
    			// defineClass,将字节码转化为类
    			return defineClass(name, classData, 0, classData.length);
    		}
    		//return super.findClass(name);
    	}
    
    	// 返回类的字节码
    	private byte[] getData(String className) {
    		InputStream in = null;
    		ByteArrayOutputStream out = null;
    		String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    		try {
    			in = new FileInputStream(path);
    			out = new ByteArrayOutputStream();
    			byte[] buffer = new byte[1024];
    			int len = 0;
    			while ((len = in.read(buffer)) != -1) {
    				out.write(buffer, 0, len);
    			}
    			in.close();
    			out.close();
    			return out.toByteArray();
    		} catch (FileNotFoundException e) {
    			e.printStackTrace();
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    		return null;
    	}
    }
    

     然后,通过测试类进行测试:

    package bwie;
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class Test {	
    	public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    		//自定义类加载器的加载路径
    		MyCloassLoader2 classLoader = new MyCloassLoader2("E:/test");
    		
    		//包名+类名
    		Class<?> clazz = classLoader.loadClass("bwie2.Test2");		
    		if(clazz!=null) {
    			Object obj = clazz.newInstance();
    			Method method = clazz.getMethod("say");
    			method.invoke(obj);			
    			System.out.println(clazz.getClassLoader().toString());
    		}	
    	}
    }
    

     程序执行后,控制台打印如图所示:

    可见,笔者使用自定义的类加载器MyCloassLoader2成功地加载了程序以外的class文件。

    四、深入讲解反射

     反射是Java语言中一个非常重要的机制。

    程序员们一般都知道:通过反射,可以获取类与对象的所有信息,执行若干操作(如创建对象,方法调用),还可以修改类的数据结构(如修改访问权限)。

    在Java中,反射对应的单词是reflect。

    提到反射,不免让人霎时想起光的反射(Reflection of light)。

    Java里运用反射,是否与光的反射有关?这也涉及Java为什么要取名为反射。

    举个例子来说,一个美女站在镜子前,请问,镜子里的美女和镜子前的美女,是否同一个美女?

    答案是肯定的。

    我们再来看Java程序的加载与运行。

    一个被编译为.class字节码文件的类,经过JVM的加载,在方法区中形成对应的类模板。

    那么请问,JVM加载出的类模板,与加载前的类,是不是同一个类?

    答案是肯定的。

    大家想一下:一个人站在镜子前,通过光的反射,可以在镜子里产生一个镜像。镜像与镜子前的人是同一个人。这是运用了光的反射规则。

    实际上,我们能看到五彩缤纷的世界,一个重要原因是光的反射的存在。

    光的反射外在表现为一种现象,本质是一种机制和规则。

    同样,一个表现为.class字节码文件的类,经过JVM中的类加载器加载,在方法区中形成类模板,也相当于类的“镜像”。

    大家再想下:Java中,加载前、表现为.class字节码文件的类,与加载后、在方法区中形成的类模板,同属于一个类,这与光的反射是不是有异曲同工之妙?

    这也就是Java为什么将类加载后、在内存的方法区中形成类模板的机制,称为反射的缘由。

    看来,Java语言的缔造者不愧是大牛,将技术比喻得那么贴切,又那么接近生活!

    大家还会看到,上图中,堆区里有个Class对象,类加载时会在堆区中产生Class对象。

    程序加载运行时,一个类在内存中的Class对象与类模板都是唯一的。

    程序中通过Class对象操作类模板。

    可以说,程序中要运用反射,就离不开Class对象。那么,Class对象究竟是什么?

    如果我们把JVM看作是人的话,对于程序员来说,通过阅读Java源代码,能够了解一个类的数据结构,那么,Java程序在运行中,JVM又是如何读懂类的数据结构的呢?

    这要归功于类加载器加载class文件在方法区生成该类的模板。如果说,class文件静态地存储了类信息,类加载器加载出来的类模板相当于类在动态运行环境中的数据结构,JVM就是通过这个类模板来认识与操作这个类的。

    编程语言实现了人机交互。Java语言也是如此。

    我们要操控JVM虚拟机去操作内存中的某个类,应该怎么办呢?Java语言为所有Java数据类型(基本数据类型与引用数据类型)均提供了class属性,通过该属性可以返回Class对象,这个Class对象是我们在程序中运用反射机制,是我们与JVM交互、指挥JVM去操作类模板的接口性工具。

    机器懂的,我们未必懂。怎么办呢?找个中间人,通过中间人操作机器。这就好比,我们通过操作系统去操作电脑硬件那样。

    我们通过Class对象,指挥JVM操作程序动态运行中的类模板。

    五、对象的生命周期

    在Java中,对象的生命周期包括以下几个阶段:

    1.  创建阶段(Created)
    2.  应用阶段(In Use)
    3.  不可见阶段(Invisible)
    4.  不可达阶段(Unreachable)
    5.  收集阶段(Collected)
    6.  终结阶段(Finalized)
    7.  对象空间重分配阶段(De-allocated) 

    如图所示:

    1.创建阶段(Created)
    在创建阶段系统通过下面的几个步骤来完成对象的创建过程:
        l  为对象分配存储空间
        l  开始构造对象
        l  从超类到子类对static成员进行初始化
        l  超类成员变量按顺序初始化,递归调用超类的构造方法
        l  子类成员变量按顺序初始化,子类构造方法调用
    一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段。

    2.应用阶段(In Use)
    对象至少被一个强引用持有着。

    3.不可见阶段(Invisible)
    当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然这些引用仍然是存在着的。
    简单来说,就是程序的执行已经超出了该对象的作用域了。

    比如,在使用某个局部变量count时,已经超出该局部变量的作用域(不可见),那么就称该变量count处于不可见阶段。这种情况下,编译期在编译阶段通常就会提示与报错。
    4.不可达阶段(Unreachable)
    对象处于不可达阶段是指该对象不再被任何强引用所持有。
    与“不可见阶段”相比,“不可达阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。这些GC root可能会导致对象的内存泄露,使得对象无法被回收。


    5.可收集阶段、终结阶段与释放阶段

    这是对象生命周期的最后一个阶段:可收集阶段、终结阶段与释放阶段。

    当对象处于这个阶段的时候,可能处于下面三种情况:

    (1)垃圾回收器发现该对象已经不可到达,则对象进入“可收集阶段”。

    (2)finalize方法已经被执行,则对象空间等待被垃圾回收器进行回收,即“终结阶段”。

    (3)对象空间已被重用,即“对象空间重新分配阶段”。

    当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。JVM虚拟机就可以直接将该对象回收了。

  • 相关阅读:
    另一种阶乘问题
    韩信点兵
    java字符串大小写转换
    实现对字符串数组{"Allen","Smith","James","Martin","Ford"} 求得包含字母a的所有的名字
    将一维数组中的0去掉,不为0的项存入一个新的数组中
    hibernate -- HQL语句总结
    oracle intersect 取交集
    Spring中applicationContext.xml的bean里的id和name属性区别
    spring 源码分析
    python爬虫
  • 原文地址:https://www.cnblogs.com/lizhangyong/p/10002756.html
Copyright © 2011-2022 走看看