jvm学习笔记-类加载器

概念

类加载器是java虚拟机提供给应用程序去实现获取类和接口自己字节码数据的技术,类加载器只参与加载过程中的字节码获取并加载到内存的这一部分。

本地接口:允许JAVA调用其他语言编写的方法。在hotspot类加载器中,主要用于调用Java虚拟机中的方法,这些方法使用C++编写。

应用场景

  • 企业级应用:SPI机制,类的热部署,Tomcat类的隔离
  • 解决线上问题:使用Arthas不停机解决线上故障

启动类加载器

  • 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。
  • 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar等
通过启动类加载器去加载用户jar包
  • 加入jre/lib下进行扩展(不推荐)

不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地加载。

  • 使用参数进行扩展(推荐)

启动类加载器使用 -Xbootclasspath /a:jar包目录/jar包名进行扩展

扩展类加载器和应用程序类加载器

  • 扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器。
  • 它们的源码都位于sun.mics.Launcher中,是一个静态内部的类。继承自URLCLassLoader。具备通过目录或者指定Jar包将字节码文件加载到内存中。

扩展类加载器
  • 扩展类加载器是JDK中提供的、使用JAVA编写的类加载器。
  • 默认加载Java安装目录/jre/lib/ext下的类文件。
通过扩展类加载器去加载用户jar包
  • 放入/jre/lib/ext下进行扩展

不推荐,尽可能不要去更改JDK安装目录中的内容

  • 使用参数进行扩展

推荐,使用-Djava.ext.dirs = jar包目录进行扩展,这种方式会覆盖掉原目录。可以用;(原始目录)方式追加上原始目录。

应用程序类加载器

应用程序类加载器(Application Class Loader)是Java虚拟机(JVM)的一种类加载器,也是ClassLoader的子类,它负责从CLASSPATH环境变量中指定的路径或JAR文件加载类,通常也称为系统类加载器。

应用程序类加载器加载类的方式
  • 从classpath中加载类文件

运行时自动将CLASSPATH中指定的路径下的类文件加载到JVM中。可以执行以下代码查看 CLASSPATH路径:

String classpath = System.getProperty("java.class.path");
System.out.println(classpath);
  • 使用addURL()方法加载Jar包

可以使用以下代码加载JAR包:addURL()方法会将该JAR文件添加到应用程序类路径中,这样就可以使用类加载器加载该JAR文件中的类了。

ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
URL jarUrl = new URL("file://test.jar");
appClassLoader.addURL(jarUrl);

类的双亲委派机制

概念

当一个类加载器接受到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。

每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器。

作用
  • 保证类加载的安全性:通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。
  • 避免重复加载:双亲委派机制可以避免同一个类被多次加载
源码分析
getParent()
 @CallerSensitive
    public final ClassLoader getParent() {
        if (parent == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Check access to the parent class loader
            // If the caller's class loader is same as this class loader,
            // permission check is performed.
            checkClassLoaderPermission(parent, Reflection.getCallerClass());
        }
        return parent;
    }
  • 返回真实加载这个类/接口的加载器,返回null表示“启动类加载器”。
  • 如果这里使用了安全管理器的话,并且”调用者的类加载器“或者”请求加载这个类的类加载器的祖先类加载器“不为空。那么这个方法就会去调用安全管理器的『checkPermission()』方法来去看是否能访问到这个类的加载器
loadClass(String name, boolean resolve)
 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;
        }
    }
  • 该方法在一开始采用synchronized (getClassLoadingLock(name))来确保loadClass方法在多线程情况下,只能被加载一次。
  • 调用findLoadedClass(String)方法来检查这个类是否已经被加载了。
  • 调用父类加载器的loadClass方法。如果父类加载器是null,那么会调用启动类加载器。
  • 调用findClass(String)方法来寻找类。(findClass方法推荐重写)
findClass(String) (推荐重写)
   protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
加载类的方式

使用Class.forName方法,使用当前类的类加载器去加载指定的类

ClassLoader classLoader = Demo1.class.getClassLoader();

获取到类加载器,通过类加载器的LoadClass方法指定某个类加载器加载。

Class<?> clazz = classLoader.loadClass("类");
打破双亲委派机制
使用自定义类加载器
案例

l一个tomcat程序中是可以运行多个web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类。如果不打破双亲委派机制,当应用类加载器加载web应用1中的serlvet后,web应用2中相同限定名的servlet类就无法加载了。

解决方案

tomcat中使用自定义加载器来实现应用间类的隔离,每一个应用会有一个独立的类加载器加载对应的类。

原理

在同一个java虚拟机中,只有相同类加载器+相同类限定名才会被认为是同一个类。

自定义类加载器的实现
  1. 先别写一个普通方法类,供类加载器加载。
package com;
 
public class TwoNum {
    //返回两数之和
    public int twoNum(Integer a,Integer b){
        return a+b;
    }
 
    public static void main(String[] args) {
        TwoNum twoNum=new TwoNum();
        System.out.println(twoNum.twoNum(1,2));
    }
}

  1. 自定义ClassLoader重写里面的findClass()方法。
package com;
 
import java.io.*;
 
public class MyClassLoader extends ClassLoader{
    private String codePath;
 
    public MyClassLoader(ClassLoader parent, String codePath) {
        super(parent);
        this.codePath = codePath;
    }
    public MyClassLoader( String codePath) {
        this.codePath = codePath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        BufferedInputStream bis=null;
        ByteArrayOutputStream bos=null;
        codePath=codePath+name.replace(".", File.separator)+".class";
        byte[] bytes=new byte[1024];
        int line=0;
        try{
            //读取编译后的文件
            bis=new BufferedInputStream(new FileInputStream(codePath));
            bos=new ByteArrayOutputStream();
            while((line= bis.read(bytes))!=-1){
                bos.write(bytes,0,line);
            }
            bos.flush();
            bytes=bos.toByteArray();
            return defineClass(null,bytes,0,bytes.length);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                bis.close();
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
 
        }
        return super.findClass(name);
    }
 
}
  1. 想要打破双亲委派机制,则需要重写loadClass方法
    @Override
    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 t1 = System.nanoTime();
                //如果包名是com开头的,调用自定义类的findClass方法,否则调用父类的loadClass方法
                if(name.startsWith("com")){
                    c = this.findClass(name);
                }else{
                    c=this.getParent().loadClass(name);
                }
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
线程上下文类加载器
案例

JDBC中使用了DriverManager来管理项目中引入的不同的数据库的驱动,比如mysql和oracle驱动。

DriverManger属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制。

SPI机制
  • jdk内置的一种服务提供发现机制
  • spi的工作原理
    • 在classpath路径下的meta-inf/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。
    • 使用ServiceLoader加载实现类

    • spi中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。
public static<S> ServiceLoader<S> load(Class<S> service){
    ClassLoader c1 = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service,c1);
}

jdk8及之前的类加载器

  • jdk8及之前的版本中,扩展类加载器和应用加载器的源码位于rt.jar包中的sun.misc.Launcher.java中。
  • jdk9引入了module的概念,类加载器在设计上发生了很多变化
    • 启动类加载器使用java编写,位于jdk.internal.loader.ClassLoaders类中,java中的bootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。
    • 扩展类加载器被替换成了平台类加载器,平台类加载器遵循模块化的字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值