02-类加载子系统分析与实践

简介

类加载子系统是负责将类从磁盘或网络读到JVM内存,然后交给执行引擎执行,如图所示。
在这里插入图片描述
说明:学习类加载有助于我们更深入地理解JAVA类成员的初始化过程,运行过程。并可以为后续的线上问题的解决及调优提供一种基础保障。

类加载器

是什么?

类加载器是在类运行时负责将类读到内存的一个对象,其类型为ClassLoader类型,此类型为抽象类型,通常以父类形式出现。
类加载器对象常用方法说明:

  1. getParent() 返回类加载器的父类加载器(不继承而是组合)。
  2. loadClass(String name) 加载名称为 name的类.
  3. findClass(String name) 查找名称为 name的类.
  4. findLoadedClass(String name) 查找名称为 name的已经被加载过的类
  5. defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类。
  6. ……

如何获取?

package com.java.jvm.loader;
public class ClassLoaderTypeTests {
    public static void main(String[] args) {
        //获取系统类加载器(也是我们的应用类加载器)
        ClassLoader systemClassLoader = 
ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(systemClassLoader); 
        //获取其上层:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
//sun.misc.Launcher$ExtClassLoader@2503dbd3
        System.out.println(extClassLoader);
        //获取其上层:获取不到引导类加载器(基于c/c++实现)
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        //对于用户自定义类来说:默认使用系统类加载器进行加载
        ClassLoader classLoader = 
ClassLoaderTypeTests.class.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classLoader); 
        //String类使用引导类加载器进行加载的。
//Java的核心类库都是使用引导类加载器进行加载的。
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null

    }
}

这些类加载器的关系,例如:

在这里插入图片描述
课堂练习:获取Bootstrap ClassLoader 可以加载的资源路径有哪些?代码如下:

package com.java.jvm.loader;
import sun.misc.Launcher;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
public class ClassLoaderDirTests {
    public static void main(String[] args) 
throws Exception {
        //获取Bootstrap ClassLoader可以加载的类
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for(URL url:urls){
            System.out.println(url);
        }
        //获取String类的类加载器
        ClassLoader classLoader = String.class.getClassLoader();
        System.out.println(classLoader);//null

        //获取ExtClassLoader可以加载的路径
        ClassLoader parent = 
ClassLoader.getSystemClassLoader().getParent();

        Class<? extends ClassLoader> aClass = parent.getClass();
        Method getExtDirs = aClass.getDeclaredMethod("getExtDirs");
        getExtDirs.setAccessible(true);
        
File[] files = (File[])getExtDirs.invoke(parent);
        for(File f:files){
            System.out.println(f.getPath());
        }
    }
}

双亲委派模型

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。如图所示:

在这里插入图片描述
基于双薪委派模型进行类的加载,其具体过程如下:

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;

  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;

  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

  4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常。

具体代码我们可以参考ClassLoader#loadClass方法的具体实现,例如:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

基于这种双亲委派机制实现了类加载时的优先级层次关系,同时也可以保证同一个类只被一个加载器加载(例如Object类只会被BootstrapClassLoader加载),这样更有利于java程序的稳定运行。

简易类加载实现

为什么我们要自己定义类加载器呢?在Java的日常应用程序开发中,类的加载几乎是由JDK默认提供的类加载器相互配合来完成类的加载的,但我们也可以自定义类加载器,来定制类的加载方式。例如:

 修改类的加载方式(打破类的双亲委派模型)
 扩展加载源(例如从数据库中加载类)
 防止源码泄漏(对字节码文件进行加密,用时再通过自定义类加载器对其进行解密)
 隔离类的加载(不同框架有相同全限定名的类)

如何创建自定义类加载器呢?一种简单的方式就是继承URLClassLoader,此类可以直接从指定目录、jar包、网络中加载指定的类资源。

URLClassLoader继承ClassLoader,可以从指定目录、jar包、网络中加载指定的类资源,我们自己定义类加载器,最简单的方式就是继承URLClassLoader进行类加载实践。代码如下:

package com.java.jvm.loader;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 自己构建类加载器(基于URLClassLoader进行落地实现)
 */
public class SimpleUrlClassLoader extends URLClassLoader {
    public SimpleUrlClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }
}

编写测试类

package com.java.jvm.loader;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

public class SimpleUrlClassLoaderTests {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {
        File file=new File("E:\\TCGBIV\\DEVCODES\\CGB2202CODES");//这个就为你pkg这个包所在的路径
        URL[] urls={file.toURI().toURL()};
        SimpleUrlClassLoader classLoader=new SimpleUrlClassLoader(urls,null);
        Class<?> aClass = classLoader.loadClass("pkg.HelloJVM");
        System.out.println(aClass.getClassLoader().getParent());
    }
}

其中,pkg包下的HelloJVM类定义如下:

package pkg;
public class HelloJVM{
	public static void main(String[] args){
		int a=10;
		int b=20;
		int c=a+b;
		System.out.println("HelloJVM,c="+c);
	}
}

自定义类加载器进阶实现

我们可以通过继承java.lang.ClassLoader抽象类的方式,实现自己的类加载器,以满足一些特殊的需求。建议把自定义的类加载逻辑写在findclass()方法中。例如:

package com.java.jvm.loader;

import java.io.*;

/**
 * ClassLoader没有抽象方法为什么还要将此类定义为抽象类?
 * 外界不允许直接构建此类对象
 */
public class SimpleAppClassLoader extends ClassLoader{

    private String baseDir;

    public SimpleAppClassLoader (String baseDir) {
        this.baseDir = baseDir;
    }
    /**
     * 查找类
     * @param className 类全名 (eg,com.java.jvm.HelloJVM)
     * @return 返回值为字节码对象(这就说明了一个问题,类加载时字节码对象就创建了)
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        //1.基于类名读取类(从磁盘指定路径下找到对应的类,并基于io对内容进行读取),并将信息存储到字节数组。
        byte[] data=readClassInfo(className);
        //2.将字节数组中的信息转换字节码对象
        if(data==null||data.length==0)
            throw new ClassNotFoundException();
        return defineClass(className, data,0 , data.length);
    }
    /**
     * 读取字节码信息
     * @param className
     * @return
     */
    private byte[] readClassInfo(String className){
        //1.获取className的绝对路径
        String absFilePath=baseDir+className.replace('.', File.separatorChar)+".class";
        //2.判定文件是否存在
        InputStream in=null;
        File file=new File(absFilePath);
        try {
            if (!file.exists()) throw new FileNotFoundException();
            //3.构建流对象
            in = new FileInputStream(file);
            //4.读数据并返回
            byte[] data = new byte[in.available()];
            in.read(data);
            return data;
        }catch (IOException ex){
            ex.printStackTrace();
            throw new RuntimeException("文件不存在或读取失败");
        }finally {
            //5.释放资源
            if(in!=null)try{in.close();}catch (Exception e){e.printStackTrace();}
        }
    }
}

说明:自己写类加载器一般不建议重写loadClass方法,当然不是不可以重写。
定义测试方法:假如使用自定义类加载器加载我们指定的类,要求被加载的类应与当前类不在同一个命名空间范围内,否则可能直接使用AppClassLoader进行类加载。

package com.java.jvm.loader;

public class DefaultAppClassLoaderTests {
    public static void main(String[] args) throws ClassNotFoundException {
        //String baseDir="E:\\TCGBIV\\DEVCODES\\CGB2202CODES\\";
        String baseDir="E:\\TCGBIV\\DEVCODES\\CGB2202CODES\\01-java\\target\\classes\\";
        DefaultAppClassLoader loader=new DefaultAppClassLoader(baseDir);
        //Class<?> aClass = loader.loadClass("pkg.HelloJVM");
        Class<?> aClass = loader.loadClass("com.java.jvm.HelloJVM");
        System.out.println(aClass);
        System.out.println(aClass.getClassLoader());
    }
}

输出的类加载名称应该为我们自己定义的类加载器名称。

打破双亲委派机制

自定义类加载器,重写loadClass方法,实现类的加载,例如:

package com.java.jvm.loader;

import java.io.*;

/**
 * ClassLoader没有抽象方法为什么还要将此类定义为抽象类?外界不允许直接构建此类对象
 */
public class BreakDoubleParentAppClassLoader extends ClassLoader{

    private String baseDir;

    public BreakDoubleParentAppClassLoader(String baseDir) {
        this.baseDir = baseDir;
    }

    /**
     * 重写loadClass方法,打破双亲委派模型
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            //我们自己先去加载,加载不了(例如java.lang.Object)则抛出异常
            return findClass(name);
        }catch (Exception e){
            //自己加载不了,再交给parent加载器对加载
            return super.loadClass(name);
        }
    }
    /**
     * 查找类
     * @param className 类全名 (eg,com.java.jvm.HelloJVM)
     * @return 返回值为字节码对象(这就说明了一个问题,类加载时字节码对象就创建了)
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        //1.基于类名读取类(从磁盘指定路径下找到对应的类,并基于io对内容进行读取),并将信息存储到字节数组。
        byte[] data=readClassInfo(className);
        //2.将字节数组中的信息转换字节码对象
        if(data==null||data.length==0)
            throw new ClassNotFoundException();
        return defineClass(className, data,0 , data.length);
    }
    /**
     * 读取字节码信息
     * @param className
     * @return
     */
    private byte[] readClassInfo(String className){
        //1.获取className的绝对路径
        String absFilePath=
                baseDir+className.replace('.', File.separatorChar)+".class";
        //2.判定文件是否存在
        InputStream in=null;
        File file=new File(absFilePath);
        try {
            if (!file.exists()) throw new FileNotFoundException();
            //3.构建流对象
            in = new FileInputStream(file);
            //4.读数据并返回
            byte[] data = new byte[in.available()];
            in.read(data);
            return data;
        }catch (IOException ex){
            //ex.printStackTrace();
            throw new RuntimeException(ex);
        }finally {
            //5.释放资源
            if(in!=null)try{in.close();}catch (Exception e){e.printStackTrace();}
        }
    }
}

编写测试,进行单元测试,例如:

package com.java.jvm.loader;

public class BreakDoubleParentAppClassLoaderTests {//AppClassLoader
    public static void main(String[] args) throws ClassNotFoundException {
        String baseDir="E:\\TCGBIV\\DEVCODES\\CGB2202CODES\\";
        BreakDoubleParentAppClassLoader loader1=
               new BreakDoubleParentAppClassLoader(baseDir);
        Class<?> aClass1 = loader1.loadClass("pkg.HelloJVM");
        System.out.println(aClass1.getClassLoader());

        BreakDoubleParentAppClassLoader loader2=
                new BreakDoubleParentAppClassLoader(baseDir);
        Class<?> aClass2 = loader2.loadClass("pkg.HelloJVM");
        System.out.println(aClass2.getClassLoader());
        System.out.println(aClass1==aClass2);
        //false表示同一个class在内存中可以存在多个字节码对象(类对象)
  
    }
}

思考:试试用这个类加载器去加载classpath下的类,试试是什么效果。

类加载过程分析

类加载步骤

类加载的一个基本步骤如下:

  1. 通过一个类的全限定名(类全名)来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口,如图所示。

在这里插入图片描述
我们看到加载过程中大致可分为加载、验证、准备、解析、初始化几大阶段,但这几个阶段的执行顺序又是怎样的呢?JVM规范中是这样说的:

  1. 加载、验证、准备和初始化发生的顺序是确定的,而解析阶段则不一定.
  2. 加载、验证、准备和初始化这四个阶段按顺序开始不一定按顺序完成。
    另外,一个已经加载的类被卸载的几率很小,至少被卸载的时间是不确定的,假如需要卸载的话可尝试System.exit(0);

类加载路径

JVM 从何处加载我们要使用的类呢?主要从如下三个地方:

  1. JDK 基础类库中的类(lib\jar,lib\ext)。
  2. 第三方类库中的类。
  3. 应用程序类库中的类。

类加载方式

JVM 中的类加载方式主要两种:隐式加载和显式加载.
1. 隐式加载

  1. 访问类的静态成员(例如类变量,静态方法)
  2. 构建类的实例对象(例如使用new 关键字构建对象或反射构建对象)
  3. 构建子类实例对象(构建类的对象时首先会加载父类类型)

2. 显式加载

  1. ClassLoader.loadClass(…)
  2. Class.forName(…)

代码分析:

class ClassA{
	static {
		System.out.println("ClassA");
	}
}
public class ClassLoaderTraceTests{
	
	public static void main(String[] args)throws Exception {
		
		//ClassLoader systemClassLoader =
                //ClassLoader.getSystemClassLoader();		 
//loader.loadClass("com.java.jvm.loader.ClassA");
		Class.forName("com.java.jvm.loader.ClassA");
		
	}
	
}

说明:

  1. 通过ClassLoader对象的loadClass方法加载类不会执行静态代码块。
  2. 可通过指定运行参数,查看类的加载顺序。
-XX:+TraceClassLoading

在这里插入图片描述

类链接(Linking)分析

验证(Verification)

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:

  1. 文件格式的验证(魔数,版本号,常量池,访问标志,当前类索引,…)。
  2. 元数据验证(int a=10,其中int a就为元数据)。
  3. 字节码合法性验证(同一个.java,生成的字节码结构是固定,例如都是16进制)。
  4. 符号引用验证(Class文件中以CONSTANT_Class_info、
    CONSTANT_Fieldref_info等常量形式出现)
    说明:验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备(Preparation)

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

  1. 类变量(static)内存分配。
  2. 按类型进行初始默认值分配(如0、0L、null、false等)。
    例如:假设一个类变量的定义为:public static int value = 3;那么变量value在准备阶段过后的初始值为0,而不是3,把value赋值为3的动作将在初始化阶段才会执行。
  3. 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
    例如:假设上面的类变量value被定义为: public static final int value = 3;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3

解析(Resolution)

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,其中:

  1. 符号引用:就是一组符号(例如CONSTANT_Fieldref_info)来描述目标,可以是任何字面量。
  2. 直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
    说明:相同的符号引用不同JVM机器上对应的直接引用可能不同,直接引用一般对应已加载到内存中的一个具体对象。

类初始化(Initialization)分析

初始化方式

此阶段为类加载的最后一个阶段,这个阶段的JVM负责对类进行初始化,主要对类变量进行初始化。
在Java中,对类变量进行初始值的设定有两种方式:

  1. 声明类变量时指定初始值。
  2. 使用静态代码块为类变量指定初始值。
    分析如下程序给出执行结果:
package com.java.jvm.loader;
//-XX:+TraceClassLoading
class ClassB{
   private static final ClassB instance=new ClassB();
   static {
      System.out.println("static{}.instance="+instance);
   }
   public ClassB(){
      System.out.println("ClassB().instance="+instance);
   }
}

public class ClassLoadingInitTests {
   public static void main(String[] args)throws Exception {
      ClassLoader loader=ClassLoader.getSystemClassLoader();
      Class.forName("com.java.jvm.loader.ClassB", false, loader);
   }
}

练习:分析如下代码,并对其进行加载,给出其输出结果。

package com.java.jvm.loader;
class ClassC{
    private static  ClassC instance=new ClassC();
    private static  Map<String,Object> map=new HashMap<>();
    public ClassC(){
        map.put("A", 100);
        map.put("A", 200);
        System.out.println(map);
    }
}

类初始化时机

Java类在加载时,其初始化时机,可从如下两种方式进行分析:

  1. 主动使用:会执行加载、连接、初始化静态域
  2. 被动使用:只执行加载、连接,不初始化类静态域

分析如下代码的执行结果,例如:

package com.java.jvm.loader;
class ClassAB{
    public static int a=10;
    static {
        System.out.println("AB.a="+a);
    }
}
class ClassCD extends ClassAB{
    static {
        System.out.println("CD");
    }
}

public class ClassPassiveLoading {
    public static void main(String[] args) {
        System.out.println(ClassCD.a);
    }
}

笔试代码分析实践

案例分析1

阅读如下代码,分析程序的执行结果:

package com.java.jvm.loader;

public class ClassLoadingPractise01{
    static int a=10;
    static{
        a=11;
        b=11;
    }
    static int b=10;
    public static void main(String[] args) {
        System.out.println(a);
        System.out.println(b);
    }
}

案例分析2

阅读如下代码,分析程序的执行结果:

package com.java.jvm.loader;

class C{
    static{
        System.out.println("1");
    }
    public C(){
        System.out.println("2");
    }
}
class D extends C{
    static{
        System.out.println("a");
    }
    public D(){
        System.out.println("b");
    }
}
public class ClassLoadingPractise02{
    public static void main(String[] args) {
        C c1=new D();
        C c2=new D();
    }
}

总结(Summary)

重难点分析

 类加载过程。
 常用类加载器。
 双亲委派模型。

FAQ分析

 JVM的类加载子系统解决了什么问题?(将指定位置的类读取到内存中)
 你知道类的双亲委派模型吗?(这是类加载时的一个委派机制)
 类的双亲委派机制可以解决什么问题?有什么缺点?
 我们是否可以改变类的双亲委派机制实现类的加载?(可以)
 你知道类加载的一个具体步骤吗,也就是一个具体的过程是怎样?
 类加载时一定会执行静态代码块吗?
 如何理解类中的主动加载和被动加载?
 static int a=10这条语句中将10赋值给a这个变量,发生在类加载的什么阶段?
 我们构建子类对象时,是否会加载父类?假如会,那每次构建都会加载吗?
 听说过热启动吗?(类修改完以后系统自动重启,重新加载这个类)
 。。。。。。

参考

https://docs.oracle.com/javase/specs/index.html
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
https://docs.oracle.com/javase/specs/jls/se8/html/index.html
http://hg.openjdk.java.net/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值