02-类加载子系统

02-类加载子系统,第1张

文章目录
  • 简介
  • 类加载器
    • 是什么?
    • 如何获取?
    • 双亲委派模型
    • 简易类加载实现
    • 自定义类加载器进阶实现
  • 类加载过程分析
    • 类加载步骤
    • 类加载路径
    • 类加载方式
  • 代码分析实践
    • 案例1
    • 案例2
  • 总结(Summary)
    • 重难点分析
    • FAQ分析
    • 参考

简介

类加载子系统是负责将类从磁盘或网络读到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 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()方法中。例如:

class BaseAppClassLoader extends ClassLoader {
	private String baseDir;
	public BaseAppClassLoader(String baseDir) {
		this.baseDir=baseDir;
	}
	@Override
    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassBytes(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }
	/**自己定义*/
    private byte[] loadClassBytes(String className) {//pkg.Search
        String fileName =baseDir+className.replace('.', File.separatorChar) + ".class";
        System.out.println("fileName="+fileName);
        InputStream ins=null;
        try {
            ins= new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}finally {
        	if(ins!=null)try{ins.close();}catch(Exception e) {}
        }
    }

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

public class BaseAppClassLoaderTests {
	public static void main(String[] args) throws Exception{
		 String baseDir="F:\\WORKSPACE\\";
		 BaseAppClassLoader classLoader = 
new BaseAppClassLoader (baseDir);
		 //此类不要和当前类放相同目录结构中
		 String pkgCls="pkg.Search";
	     Class testClass = classLoader.loadClass(pkgCls);
	     Object object = testClass.newInstance();
	     System.out.println(object.getClass());
	     System.out.println(object.getClass().getClassLoader());
	}
}

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

类加载过程分析 类加载步骤

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

  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. 构建子类实例对象(构建类的对象时首先会加载父类类型)
  1. 显式加载
  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

代码分析实践 案例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/

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/915394.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存