1.21Java-类加载与类加载器

目录

前言

1.21.1类加载过程

1.21.1.1JVM一个类的加载过程

1.21.1.1.1加载

1.21.1.1.2验证

1.21.1.1.3准备

 1.21.1.1.4解析

1.21.1.1.5初始化

1.21.1.1.6使用

1.21.1.1.7卸载

1.21.1.2一个类被初始化的过程

 1.21.1.3继承时父类的初始化顺序

1.21.2什么是类加载

1.21.2.1概念

1.21.2.2JVM有哪些类加载器

1.21.2.3JVM中不同的类加载器加载哪些文件

1.21.3类加载机制

*1.21.3.1双亲委派机制

1.21.3.2其他机制

全盘负责

父类委托

缓存机制


前言

本章节和《1.22Java-反射》是java的重点难点,涉及到底层,属于中级范畴,可选择性学习。

1.21.1类加载过程

1.21.1.1JVM一个类的加载过程

一个类的生命周期包括了 "加载"、"验证"、"准备"、"解析"、"初始化"、"使用"、"卸载" 这七个阶段

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 卸载(Unloading)

其中验证、准备、解析阶段统称为链接

1.21.1.1.1加载

加载(Loading):classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间(JDK1.8前叫方法区、永久代),此阶段程序员可以干预,自定义类加载器。

注意:没有new出来之前,加载的时候ClassLoader会在java堆中生成一个代表这个类的Class对象,作为访问方法区(元空间)中这些数据的入口。new一个类的话,类的引用会在栈里,类的实例会在堆里。

解释

如下图,我们的类编译好后存入target的classes下面,把这些class文件加载到JVM内存中。

 同样,我们导入的jar包,还有网络上、磁盘里的class都可以

1.21.1.1.2验证

验证(Verification):验证Class文件的字节流中包含的信息符合《java虚拟机规范》约束,保证虚拟机的安全

解释

验证阶段就是看看这个class文件是不是符合规范

比如我们随便找一个已经编译好的类 

这个就是这个类的字节码文件,以cafe babe开头(相传这是创始团队最喜欢喝的一种咖啡) 

神秘的字节码文件就这长这个样

1.21.1.1.3准备

准备(Preparation):类变量赋默认初始值,int为0,long为oL,boolean为false,引用类为null,常量为赋值为正式值(常量在准备阶段就已经赋值了)

解释

这个类中的常量a 在准备阶段就已经赋值为123,

而类变量 b,因为是int型,在准备阶段赋值为0

实例变量不会初始化,实例对象只会在new 这个对象时才初始化。

 1.21.1.1.4解析

解析(Resolution):把符号引用翻译为直接引用

解释

把一个class文件读进来后变成一个java.lang.Class对象放入元空间,了解即可。

1.21.1.1.5初始化

初始化(Initialization):当我们new一个类对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化.....

那么这些都会触发类的初始化。

那么初始化的过程和顺序是什么?

1.21.1.1.6使用

使用(Using):使用这个类

1.21.1.1.7卸载

卸载(Unloading):

  1. 该类的实例都已经被GC,也就是JVM中不存在该Class的任何实例
  2. 加载该类的ClassLoader已经被GC
  3. 该类的java.lang.Class对象没有任何在其他地方被引用,如不能在任何地方通过反射访问该类的方法

解释

卸载的条件特别苛刻,以上三个条件都要满足

GC:垃圾回收

1.21.1.2一个类被初始化的过程

这个阶段对程序员来说很重要,这个阶段开始加载我们的代码。

类的初始化阶段,java虚拟机才开始执行类中编写的java程序代码。

准备阶段时,变量已经赋过值一次系统要求的初始零值,而在初始化阶段,才是真正的初始化变量和其他资源 。

静态常量准备阶段赋值
静态变量

准备阶段:赋默认初始值

int为0,

long为oL,

boolean为false,

引用类为null,

常量为赋正式值(常量在准备阶段就已经赋值了)

初始化阶段:赋真正的值
静态代码块初始化阶段执行
变量创建对象时赋值
普通代码块创建对象时执行
构造器创建对象时执行

 1.21.1.3继承时父类的初始化顺序

父类--静态变量

父类--静态初始化块

子类--静态变量

子类--静态初始化块

父类--变量
父类--初始化块

父类--构造器

子类--变量

子类--初始化块

子类--构造器

我们来验证一下,首先有两个类

parent类:

public class Parent {
    //静态变量
    public static String p_staticField="父类 静态变量";
    //变量
    public static String p_Field="父类--变量";
    protected int i=0;
    protected int j=0;
    //静态代码块
    static {
        System.out.println(p_staticField);
        System.out.println("父类--静态初始化块");

    }
    //普通的初始化代码块
    {
        System.out.println(p_Field);
        System.out.println("父类--初始化块");
    }
    //构造器
    public Parent(){
        System.out.println("父类--构造器");
        System.out.println("i="+i+",j="+j);
        i=1;
        j=1;
    }


}
 

 child类:

public class Child extends Parent{
    //静态变量
    public static String c_staticField="子类 静态变量";
    //变量
    public static String c_Field="子类--变量";

    //静态代码块
    static {
        System.out.println(c_staticField);
        System.out.println("子类--静态初始化块");

    }
    //普通的初始化代码块
    {
        System.out.println(c_Field);
        System.out.println("子类--初始化块");
    }
    //构造器
    public Child(){
        System.out.println("子类--构造器");
        System.out.println("i="+i+",j="+j);
    }

    public static void main(String[] args) {

//        new Child();
    }

}
 

我们将child类的main方法里面为空,只加载类。按照《1.21.1.3一个类被初始化的过程》所学,我们猜想,肯定会加载静态变量和静态代码块,因为这两个在准备阶段就已经"准备"过了。

运行结果为

1.21.2什么是类加载

1.21.2.1概念

在类“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的“代
码”被称为“类加载器”(Class Loader),这个动作是可以自定义实现的.

类加载器就是把class的字节码文件读取到JVM内存中,本质是程序或代码,由C语言或者java语言编写

1.21.2.2JVM有哪些类加载器

Java 虚拟机的角度来看,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoaaer) ,由c++语言实现,是虚拟机自身的一部分
  2. 其他所有的类加载器,由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader;站在Java开发者的角度来看,自JDK 1.2开始,Java一直保持着三层类加载器架构;

1.21.2.3JVM中不同的类加载器加载哪些文件

1.启动类加载器  (Bootstrap ClassLoader) :(根的类加载器c++语言实现

                <JAVA_HOME>\jre\lib\rt.jar,resources.jar、charsets.jar、被-Xbootclasspath参数所指定的路径中存放的类库;

2.扩展类加载器((Extension ClassLoader) : Java语言实现

                sun. misc.Launcher$ExtClassLoader、<JAVA_HOME>\jre\lib\ext 、被 java.ext.dirs系统变量所指定的路径中所有的类库;

3、应用程序类加载器(Application ClassLoader):(系统的类加载器)  Java语言实现
                sun.misc.Launcher$AppClassLoader、加载用户类路径(ClassPath)上所有的类库。

注:

 <JAVA_HOME>:jdk的环境变量

rt:runtime

我们可以打开JDK下面的\jre\lib\rt.jar,里面有很多jar包,我们选择一个BufferReader.class

通过getClassLoader()方法可以直到谁加载了指定的类,运行结果如图所示,为什么是null? 

因为rt.jar包是由启动类加载器(Bootstrap ClassLoader)加载的,并且这个类加载器(代码/程序)是用c++实现的,java获取不到肯定打印不出来,所以为null。

 我们随便再找一个jar包 

 通过同样的方式来获取类加载器,运行结果如下图所示,我们发现这个jar包的类加载器就是应用程序类加载器(Application ClassLoader)来加载的

 剩下的扩展类加载器,可以去\jre\lib\ext里面自己尝试,方法如上所述。

接着我们观察,这些类加载器为什么都有$,前面还有一串前缀。

 我们打开rt.jar/sun/misc/Launcher,发现这是一个类,并且Launcher类下还有APPClassLoader和ExtClassLoader两个静态内部类,其实这个$就表示静态内部类。

 在IDEA里通过如下操作可以查看继承图

通过查看源码,我们发现,

 AppClassLoader和ExtClassLoader是继承于URLClassLoader,顶级父类是ClassLoader。它们的继承体系如下图所示:

所以AppClassLoader与ExtClassLoader并不是继承关系

 因此,自定义类加载器也是继承于ClassLoader。(自定义类加载器不是继承于ExtClassLoader或AppClassLoader)

1.21.3类加载机制

*1.21.3.1双亲委派机制

 比如要加载String类,这个类在rt.jar中,首先由AppClassLoader自己不加载,而是向上委托ExtensionClassLoader加载,,ExtensionClassLoader同样自己不加载,向上委托BoostrapClassLoader加载,BoostrapClassLoader在“负责”的jar包范围内寻找String,发现有,就加载到JVM内存中。

如果要找的类不在BoostrapClassLoader中,那么BoostrapClassLoader就反馈给下一层的ExtensionClassLoader,ExtensionClassLoader找到就加载到JVM内存中,反正继续向下层反馈,如果最后AppClassLoader也没找到,就会返回类找不到异常。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试
加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因
此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自
己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,下一层类加载器才会尝试自
己去加载;

这里提出一个疑问,依照我们前面的推论,AppClassLoader与ExtClassLoader并不是继承关系。那为什么还叫双亲委派?

注意:这里各加载器之间的层级关系并不是以继承方式存在的,而是以组合方式处理的。

那么为什么要这样设计:

  1. 确保安全,避免Java核心类库被修改
  2. 避免重复加载
  3. 保证类的唯一性

接下来,我们在IDEA看JDK双亲委派的源码

注意:如果你发现你的<18>下面没有rt.jar,那就说明你的JDK里面没有JRE,因为rt.jar就在JRE中。

解决办法:IDEA中换JDK路径,选一个或者下载一个新的JDK。

无JRE的JDK

 IDEA中的路径:External Libraries/<18>/rt.jar/sun/misc/Launcher

因为BootClassLoader用C++编写的,所以在这里我们看不到

 我们打开AppClassLoader类

因为源码太长,为了更好的表现类的结构,我们截取部分代码

我们很容易的发现,AppClassLoader是Launcher的静态内部类,

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

---------省略--------

  static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

    


}

我们打开AppClassLoader的结构,看看它有哪些方法,其中最重要的是loadClass()这个方法

loadClass方法

 public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) {
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) {
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }

一个加载类的请求被AppClassLoader所接收,并掉用loadclass方法

我们加断点,然后调试看看程序运行的情况

程序执行如下代码

这里的UCP是URLCLASSPATH

 打开this.ucp,这里面存放的是JRE目录下的jar包、 自己编译的(/target )的classpath的jar包以及依赖的jar包、idea工具所需要的jar包等等。

this.ucp 局部截图

我们加载的jar包都在ucp里,如果找不到,就会进入循环中,执行findLoadedClass(),顾名思义Loaded(过去分词),代表已经加载过的。如果找得到,就执行父类的loadClass方法。

if (this.ucp.knownToNotExist(var1)) {
    Class var5 = this.findLoadedClass(var1);

        --------省略---------
} else {
          return super.loadClass(var1, var2);
       }

根据前面的讲解,我们知道AppClassLoader的父类加载器是URLClassLoader,我们打开URLClassLoader,会发现URLClassLoader里面根本没有loadClass(),所以我们进入SecureClassLoader源码中,我们仍会发现没有loadClass()。我们再进入ClassLoader中

URLClassLoader源码

SecureClassLoader源码

,我们再进入ClassLoader中,是个顶级父类。

在ClassLoader中我们跟踪到了LoadClass方法 。注意这个方法里面实现了双亲委派!

 **来看双亲委派的源码

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
//锁用于同步
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
//看看这个类是否已经加载过  Class是加载到元空间中
            Class<?> c = findLoadedClass(name);
//如果为空,代表没有加载过,双亲委派开始
            if (c == null) {
                long t0 = System.nanoTime();
                try {
//如果父类不为空   注意这里的父类与子类是动态性的,比如在当前示例中我们是从AppClassLoader进来的,所以这个parent就是ExtClassLoader(idea也可自动显示parent的信息,如图1所示)。
                    if (parent != null) {
//注意,这里的loadClass方法,仍然是最上面的loadClass,这里用了递归,
                        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;
        }
    }

我们需要注意的点是,当类没有加载时,会调用父类的加载器, c = parent.loadClass(name, false);,这里的loadClass(),在ExtClassLoader()中,只有顶级类加载器ClassLoader中才有,而且,c = parent.loadClass(name, false);这行代码就在loadClass()中

待更新--------------------------------------------------------------------------------------

1.21.3.2其他机制

全盘负责

就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

父类委托

就是当一个类加载器负责加载某个Class时,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

缓存机制

保证所有加载过的Class都会被缓存,当程序需要使用某个Class对象时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存储到缓存区

上一篇:1.20Java-网络编程
下一篇:1.22Java-反射​​​​​​​

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老李头喽

高级内容,进一步深入JA领域

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值