文章目录

  • Java类加载机制
  • 1.概述
  • 1.1类的加载在整个java程序运行期间处于的环节
  • 1.2类加载在JVM中处于的位置
  • 1.3类的生命周期
  • 2.从什么地方加载类
  • 3. 加载类的时机
  • 4.类加载的过程
  • 5.类加载器
  • 5.1概述
  • 5.2类加载器的种类
  • 5.3类加载机制
  • 5.3.1双亲委派(父类委托)
  • 5.3.2 双亲委派的优点
  • 5.2.3 为什么要打破双亲委派。
  • 5.2.4 怎么打破双亲委派:
  • 5.2.5 全盘负责机制:
  • 5.2.6缓冲机制:


Java类加载机制


类的加载是指将类加载到内存,而将 类的加载,链接,初始化这三个步骤称为 类加载。其实称什么也无所谓,主要是别搞糊涂了。

1.概述


1.1类的加载在整个java程序运行期间处于的环节


java类加载机制不了 java类加载机制过程_类加载器

1.2类加载在JVM中处于的位置


java类加载机制不了 java类加载机制过程_类加载器_02

1.3类的生命周期


java类加载机制不了 java类加载机制过程_java类加载机制不了_03

2.从什么地方加载类


  • 本地磁盘
  • 网上加载.class文件(Applet)
  • 从数据库中
  • 压缩文件中(ZAR,jar等)
  • 从其他文件生成的(JSP应用)

3. 加载类的时机


对于初始化阶段,虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  • 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。
  • 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)
  • 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),
    虚拟机会先初始化这个主类。

4.类加载的过程


java类加载机制不了 java类加载机制过程_加载_04

主要有三个步骤,加载,链接,初始化。简单介绍一下各个步骤的功能

  • 加载:将类加载到内存(这个类从哪里来,在上面已经讲了),将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的javalang.Class对象,作为方法区中类数据的访问入口〈即引用地址) 。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与
  • 链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。
  • 验证:验证字节码的正确性,即确保加载的类信息符合JVM的规范。确保被加载的类有正确的内部结构,能与其他类协调一致。
  • 准备:负责为类的类变量(也就是Static的变量)分配内存,并为之设置默认初值,这些内存都在方法区中
  • 解析:装入类所引用的其他类,把符号引用替换为直接引用包含 静态链接
  • 初始化:执行类的静态代码块,为静态变量赋初始值。
  • 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。 〈类构造器是构造类信息的,不是构造该类对象的构造器)
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。

5.类加载器

5.1概述


  1. 类加载器是加载类的一个工具,但是实际上,加载类并不是由它来负责,是由JVM负责的。它是负责找到这个类。
  2. 在java中,我们使用全限定类名(包名+类名)来唯一标示一个类,而在JVM中,我们使用全限定类名+加载器名来唯一标示一个类。
  3. 类加载的作用: 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为法区中类数据的访问入口。

5.2类加载器的种类


  • Bootstrap ClassLoader(根类加载器):用C++ 编写的,是JVM自带的类加载器,负责加载JRE的核心类库,比如 rt.jar, charsets.jar。该加载器无法直接获取
  • Extention ClassLoader(扩展类加载器):负责加载JRE扩展目录ext中jar类包。是用java实现的,是ClassLoader的实例
  • Application ClassLoader(系统类加载器):负责加载ClassPath下的类包,是用户用的最多的。是用java实现的,是ClassLoader的实例
  • User ClassLoader(自定义加载器):负责加载用户自定义下的类包。自定义的加载器就是去继承ClassLoader,重现一下它的方法就可以。这里不多讲。
public class ReflectionTest {

    public static void main(String[] args) throws Exception {
        ClassLoaderTest(});输出
    }
    public static void ClassLoaderTest(){
        //获取我们这个自定义类的加载器,将返回系统类加载器
        ClassLoader classLoader = ReflectionTest.class.getClassLoader();
        System.out.println(classLoader);
        //调用系统类加载器的getParent方法,将返回扩展类加载器
        ClassLoader parentLoader = classLoader.getParent();
        System.out.println(parentLoader);
        //调用扩展类加载器的getParent方法,按理来说应该获取到的是根类加载器,但根类加载器是无法获取的,所以返回null
        ClassLoader supparentLoader = parentLoader.getParent();
        System.out.println(supparentLoader);
//获取String,java的核心类,一个是返回根类加载器,但是返回的也是null
        ClassLoader stringloader = String.class.getClassLoader();
        System.out.println(stringloader);
    }
}
//输出:
//jdk.internal.loader.ClassLoaders$AppClassLoader@3d4eac69
//jdk.internal.loader.ClassLoaders$PlatformClassLoader@38af3868
//null
//null

5.3类加载机制

5.3.1双亲委派(父类委托)

先让父加载器试图加载该类,只有父加载器无法加载该类时,才尝试从自己的类路径中加载该类。它是java类加载的默认机制

比如:

我使用Appliction ClassLoader加载一个类,它会看一下Extention ClassLoader 加载器能不能加载,而Extention ClassLoader又会问一下Bootstrap ClassLoader能不能加载,如果加载的类是JRE的核心类库,Bootstrap ClassLoader会很高兴,是它负责的范围,Bootstrap ClassLoader会加载该类,如果不是则会返回给Extention ClassLoader加载,如果该类是JRE扩展目录ext中的,Extention ClassLoader会很高兴,加载该类,否则扔给 Appliction ClassLoader加载。

双亲委派机制的实现:

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
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
5.3.2 双亲委派的优点

  1. 可以避免重复加载。因为之前讲过,JVM中使用全限定类名+加载器名来唯一标示一个类。就算是同一个类用不同的加载器加载也是不同的。通过双亲委派,就防止了一个类被不同的加载器加载。比如使用扩展类加载器加载一个ext包的类,再到系统类加载器来加载这个类,如果没有双亲委派机制,则这个类被加载两次,如果使用了,则在系统类加载器来加载这个类时,会问扩展类加载器,发现扩展类加载器加载了这个类,于是不会再加载。
  2. 沙箱安全机制,保护java核心类库:防止开发人员篡改java核心类库。修改Java的核心类库是很危险的。例如,开发人员自己定义一个含有恶意代码String类,在双亲委派机制下,开发人员加载自定义String类,最终到了根类加载器,该类加载器在JRE核心包下搜索String类,发现有,但不是自定义String类,因为自定义的类是不会出现在JRE核心包下。所以它只会加载原有的String。
5.2.3 为什么要打破双亲委派。

比如 tomcat就打破了双亲委派,因为tomact要隔离jar包。tomcat是个web容器,一个web容器可能要部署两个或者多个应用程序,不同的应用程序,因此要保证每一个应用程序的类库都是独立、相互隔离的。所有应用都在一个容器上跑,同一个内存,同一个JVM,但不同的应用要用到的类可能版本不同,使用双亲委派的话就无法保证每一个应用程序的类库都是独立、相互隔离的。

5.2.4 怎么打破双亲委派:

我们只要自定义一个用户类加载器,重写loadClass,在重写时,不是用双亲委派的算法就可以了。

5.2.5 全盘负责机制:

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

5.2.6缓冲机制:

机制将会保证所有加载过的累都被缓存当程序中需要使用某个累时,加载器先从缓存区中寻找,只有当缓存区中,不存在类时才会加载。