GVKun编程网logo

JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制

26

这篇文章主要围绕JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制展开,旨在为您提供一份详细的参考资料。我们将全面介绍JAVA基础|从Class.forName初始化数据库

这篇文章主要围绕JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制展开,旨在为您提供一份详细的参考资料。我们将全面介绍JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制,同时也会为您带来ClassLoader & 双亲委派 & 类初始化过程、ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制、ClassObject.getClass,ClassName.class和Class.forName(“ ClassName”)之间的区别、Java class.forname 详解的实用方法。

本文目录一览:

JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制

JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制

代码托管在:https://github.com/fabe2ry/classloaderDemo

初始化数据库

如果你写过操作数据库的程序的话,可能会注意,有的代码会在程序的开头,有Class.forName("com.mysql.jdbc.Driver");的代码,并且告诉你这是在进行数据库的初始化,注册jdbc的驱动;但是其实如果你去掉这段代码,并不会影响程序的正常运行,当然这是需要在JDK6之后才行这样

import java.sql.*;
 
public class MySQLDemo {
 
    // JDBC 驱动名及数据库 URL
    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";  
    static final String DB_URL = "jdbc:mysql://localhost:3306/RUNOOB";
 
    // 数据库的用户名与密码,需要根据自己的设置
    static final String USER = "root";
    static final String PASS = "123456";
 
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try{
            // 注册 JDBC 驱动
            Class.forName("com.mysql.jdbc.Driver");
        
            // 打开链接
            System.out.println("连接数据库...");
            conn = DriverManager.getConnection(DB_URL,USER,PASS);
        
            // 执行查询
            System.out.println(" 实例化Statement对象...");
            stmt = conn.createStatement();
            String sql;
            sql = "SELECT id, name, url FROM websites";
            ResultSet rs = stmt.executeQuery(sql);
        
            // 展开结果集数据库
            while(rs.next()){
                // 通过字段检索
                int id  = rs.getInt("id");
                String name = rs.getString("name");
                String url = rs.getString("url");
    
                // 输出数据
                System.out.print("ID: " + id);
                System.out.print(", 站点名称: " + name);
                System.out.print(", 站点 URL: " + url);
                System.out.print("\n");
            }
            // 完成后关闭
            rs.close();
            stmt.close();
            conn.close();
        }catch(SQLException se){
            // 处理 JDBC 错误
            se.printStackTrace();
        }catch(Exception e){
            // 处理 Class.forName 错误
            e.printStackTrace();
        }finally{
            // 关闭资源
            try{
                if(stmt!=null) stmt.close();
            }catch(SQLException se2){
            }// 什么都不做
            try{
                if(conn!=null) conn.close();
            }catch(SQLException se){
                se.printStackTrace();
            }
        }
        System.out.println("Goodbye!");
    }
}

com.mysql.jdbc.Driver

首先我们要知道Class.forName()与ClassLoader.loadClass()的区别,二者都可以返回一个类对象

  • Class.forName()根据重载形式的不同,分别为public Class forName(String name)来初始化类,根据public Class forName(String name, boolean init, ClassLoader classLoader);来选择对于的classloader进行加载,并且是否需要初始化;二者没有互相调用的关系
  • 而ClassLoader.loadCLass()根据重载形式的不同,分别为public CLass loadClass(String name);和protect Class ClassLoader(String name, boolean reslove);,前者是我们调用加载器的方法,后者则是我们应该继承重写的方法,方法对应的第二个参数的意思是是否需要解析,这个解析就是我们在类加载机制中的一个环节了;二者的关系是前者默认调用带false常数的后者

知道了这个区别之后,就应该了解使用Class.forName是希望完成类从加载,连接(包括验证,准备和解析)以及初始化的全过程,但是代码之后也没有使用过这个方法加载出来的类对象,说明使用这个方法,目的就是完成类的初始化,所以查看一下com.mysql.jdbc.Driver这个类的实现

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can''t register driver!");
        }
    }
}

可以看到static代码块里有初始化的操作,使用这种方式初始化,可以保证初始化一次(jvm加载类的时候,默认会进行加锁同步,避免多线程下加载多个一样的类,自然也只有一次初始化操作)

为什么需要SPI(service provider interface)

首先我们可以看到如果没有引入spi,我们必须显示的调用Class.forName()来调用数据库初始化的操作,并且这种操作,有以下的问题

  • 使用String的字面值,存在可能写错的情况,毕竟不是强类型的操作,像idea这些编译器也不能提前发现,只有程序运行才能检测出了
  • 需要硬编码到程序中,如果我更改了另一个数据库的驱动,需要修改到代码,你可能会说这只是改动一下,没什么关系,但是如果很蛋疼的是,你实际项目中,可能测试环境用一种驱动,生成环境用另一个驱动,你这会不就需要重复更改代码了么,而且更改代码还意味着需要重写编译,当项目很大的时候,这么做就会浪费很长的时间了

那么有没有一种方法,只需要我们引入了某个驱动的jar包,程序就知道自动加载驱动,也就是帮我们根据jar包来调用Class.forName()的操作呢

有,这就是spi的作用,下面我们通过一个例子,写一个自己的api接口,并且另外写两个jar包,分别提供不同的api接口的实现,使用spi来,帮助我们达到我们自动初始化的目的

实现spi

我们先在项目一中新建一个接口Angle,并且写一个AngleManager管理类,这个类保存着我们的实现类,实现类需要向该类注册;再新建项目二与三,分别实现接口,并且打包成为jar包,同样,因为实现接口前,必须知道接口是啥,我们使用maven管理jar包,同时在项目二和三把项目一的jar给引入;最后,我们在项目四,引入项目一,并且根据需求,引入项目二或者项目三,来进行测试

项目一

Angle.java

package api;

public interface Angle {
    void love(String singleDog);
    void hate(String coupleDog);
}

AngleManager.java

package api;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

public class AngleManager {

    private static int angleIndex = 0;
    private static List<Angle> angles = new ArrayList<Angle>();

    /**
     * 提供注册功能
     * @param angle
     */
    public static void registerAngle(Angle angle){
        angles.add(angle);
    }

    /**
     * 获取一个接口实现
     * @return
     */
    public static Angle angleFall(){
        if(angles.size() > 0 && angleIndex < angles.size()){
            return angles.get(angleIndex ++);
        }
        return null;
    }

    /**
     * 提供初始化操作,里面使用spi,来发现第三方的接口实现
     */
    private static void angleManagerInit() {
        ServiceLoader<Angle> angleServiceLoader = ServiceLoader.load(Angle.class);
        Iterator<Angle> angleIterator = angleServiceLoader.iterator();
        while(angleIterator.hasNext()) {
//            这里会调用Class.forName(name, init, classloader);
            angleIterator.next();
        }
    }

    static {
        System.out.println("angleManagerInit");
        angleManagerInit();
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.fabe2ry</groupId>
    <artifactId>paradise</artifactId>
    <version>1.0-SNAPSHOT</version>


</project>

写完install下,发布到本地仓库,给后面项目引入

项目二

FireAngle.java

package impl;

import api.Angle;
import api.AngleManager;

public class FireAngle implements Angle {

    static {
//        自定义的初始化操作
        System.out.println("i am fire angle, i init");
        AngleManager.registerAngle(new FireAngle());
    }

    public void love(String singleDog) {
        System.out.println("single dog is happy, very very happy");
    }

    public void hate(String coupleDog) {
        System.out.println("Burning coupleDog");
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.fabe2ry</groupId>
    <artifactId>fire</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.fabe2ry</groupId>
            <artifactId>paradise</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>


</project>

同时为了使用SPI,我们还需要遵守规范,在resource文件下,新建META-INF/services文件夹,在下面以接口名称命名一个文本文件,在文件内写入接口实现类的全限定名称

项目三

项目三就只贴实现类了

Lucifer.java

package hell;

import api.Angle;
import api.AngleManager;

public class Lucifer implements Angle {
    static {
    //	自定义的初始化操作
        System.out.println("i am lucifer, i init");
        AngleManager.registerAngle(new Lucifer());
    }

    public void love(String s) {
        System.out.println("Lucifer love single dog");
    }

    public void hate(String s) {
        System.out.println("Lucifer hate couple dog");
    }
}

现在同样将项目二和三给install一下,发布到本地仓库

项目四

TestMain.java

import api.Angle;
import api.AngleManager;

public class TestMain {
    public static void main(String[] args) {
        Angle who = AngleManager.angleFall();
        who.love("zxzhang");
        who.hate("tr3eee");
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.fabe2ry</groupId>
    <artifactId>world</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.fabe2ry</groupId>
            <artifactId>paradise</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>


        <dependency>
            <groupId>com.fabe2ry</groupId>
            <artifactId>fire</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--注释掉-->
        <!--<dependency>-->
            <!--<groupId>com.fabe2ry</groupId>-->
            <!--<artifactId>hell</artifactId>-->
            <!--<version>1.0-SNAPSHOT</version>-->
        <!--</dependency>-->
    </dependencies>

</project>

当项目运行后

修改pom文件,引入项目三的jar包

可以看到我们全程面对接口编程,在没有修改代码的情况下,就更改的代码的实现,可以说一种控制反转了吧,同时第三方开发api接口实现类,需要做的初始化操作,全部通过静态代码块的方式执行了,用户完全不用参与

破坏双亲委托机制

明白了SPI的作用后,再来看看为什么说SPI会破坏双亲委托机制呢

类加载器分工

当一个类(A类)使用到另一个类(B类)的时候,被使用到的类(B类)如果没有被加载,这时候,应该由哪个类加载器来加载这个类呢?结论是由使用类(A类)的类加载器,下面我们用代码验证一下

我们自定义一个类加载器MyClassLoader,这个类加载负责加载D盘下的class文件(不再classpath底下),同时我们定义A和B类,在A类中引用B类,然后看看B是会被哪个类加载器加载

Entry.java

import java.lang.reflect.Method;

public class Entry {
    public static void main(String[] args) throws Exception{
        MyClassLoader secondClassLoader = new MyClassLoader();
        Class aClazz = Class.forName("test.AClass", true, secondClassLoader);
        System.out.println("!!!!");
        Object a = aClazz.newInstance();
        System.out.println("!!!!");
        Method printMethod = aClazz.getMethod("print");
        printMethod.invoke(a);
    }
}

A.java

package test;

public class AClass {
    static {
        System.out.println("AClass init");
    }

    private BClass b;

    public AClass(){
        b = new BClass();
    }

    public void print(){
        System.out.println(this.getClass().getClassLoader().getClass().getName());
        System.out.println(b.getClass().getClassLoader().getClass().getName());
    }

}

B.java

package test;

public class BClass {
    static {
        System.out.println("BClass init");
    }
}

MyClassLoader.java

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class MyClassLoader extends ClassLoader{
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        System.out.println("my class loader load class:" + name);
        File file = getClassFile(name);
        try {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }catch (Exception e){
        }

//        这里不用这个的话,会出现加载问题
        return super.loadClass(name);
    }

    private File getClassFile(String name){
        name = name.substring(name.lastIndexOf(''.'') + 1);
        File file = new File("D:/" + name + ".class");
        return file;
    }

    private byte[] getClassBytes(File file) throws Exception{
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
}

这个加载器会加载D盘下的class文件,如果找不到才会交给父加载器加载,显然是不遵守双亲委托机制的;对于为什么需要调用super.loadClass(name)这个方法的,需要知道,对于类的解析过程,在这个过程中,会将符号引用转换为直接引用,对于类和接口的解析过程,是需要将递归解析父类的,如果父类没有进行加载,就会加载父类,如果这里我们在D盘找不到,就返回null的话,然后程序在解析的过程中就就会运行不起来,因为所有类的父类Object这个类加载器是加载不到的,所以必须调用super.loadClass(name)

错误示范(将super.loadClass(name)改为null)

到这里,其实已经可以证明我们的观点了,Object被AClass的类加载器引用,而不是使用应用程序加载器

继续原来的步骤

我们修改会类加载器的代码,让它在找不到的时候,在委托给父类查找,保证程序正常运行

同时,我们手动编译AClass.java和BClass.java,将class文件放入D盘(当然你也可以在idea里面写好AClass和BClass,然后运行一下,可以在target目录下找到编译的class文件,就不用手动编译了)

现在运行代码

了解SPI的实现过程

现在我们明白了一个类使用到另一个的类的时候,会用自己的类加载器去加载该类,那么就不难理解SPI破坏双亲委托机制了;不过先来了解一下,SPI做了什么

可以看到我们之前是通过以下代码,来实现SPI的功能的

//		导入类
    import java.util.ServiceLoader;
    
    /**
     * 提供初始化操作,里面使用spi,来发现第三方的接口实现
     */
    private static void angleManagerInit() {
        ServiceLoader<Angle> angleServiceLoader = ServiceLoader.load(Angle.class);
        Iterator<Angle> angleIterator = angleServiceLoader.iterator();
        while(angleIterator.hasNext()) {
//            这里会调用Class.forName(name, init, classloader);
            angleIterator.next();
        }
    }

通过打断点,调试,可以发现在angleIterator.next();的时候,会进入到ServiceLoader的匿名内部类Iterator

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

正常情况下,没有加载过,就会到lookupIterator.next();这个方法也是进入ServiceLoader的另一个内部类,最终会跳转到下面,完成类的加载

 		private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

可以看到使用了c = Class.forName(cn, false, loader);来进行类的加载,并且实例化了该类S p = service.cast(c.newInstance());,并且加入了缓存中;这里调用的loader是哪里来的呢?

在ServiceLoader<Angle> angleServiceLoader = ServiceLoader.load(Angle.class);过程,除了设置了对于api接口,其实也就是对于我们META-INF/services底下的文件名称,还在函数内部获取了线程上下文类加载器,并设置为了loader

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

结合之前的结论

到这里,基本就清楚了整个流程,首先,ServiceLoader是一个基础类,因为它所在的包名称java.util.ServiceLoader;这个类是使用bootstrap classloader来加载的,你可以在代码中使用获取它的类加载器,会出现空指针,因为bootstrap classloader是c++实现的,java中获取不到这个对象

ServiceLoader.class.getClassLoader().getClass().getName()

而结合我们之前得出的结论,一个类的加载会被使用它的类所属的类价值器加载的话,那么ServiceLoader使用Class.forName(name)来加载类对象,而不是Class.forName(cn, false, loader)指定类加载器加载对象的话,那么就会出现无法找到类对象的问题,因为bootstrap classloader找的路径是JDK\jre\lib,所以就需要使用线程上下文类加载器,通过线程先获取到当前的类加载器,这个加载器具体在什么时候设置进入的话,暂时不清楚,但是可以确定如果没有通过Thread.currentThread().setContextClassLoader();去修改过的话,那么这个类加载器,会是应用程序加载器(application classloader),接下来,如果你的实现类在classpath(引入jar就会包含在这里),就可以被正常加载

回顾一下

在《深入理解JVM》这本书中,提过第二次破环是该双亲委托模弊端引起的

一个例子:JNDI服务,它的代码由启动类加载器加载(在rt.jar中),但JNDI目的就是对整个程序的资源进行几种管理和查找,需要调用由每个不同独立厂商实现并且部署在应用程序的ClassPath下的JNDI接口提供者的代码。但是在应用启动时候读取rt.jar包时候,是不认识这些三方厂商定义的类的,那么如何解决?

java设计团队引入了一个新设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时候,还未设置,将会从父线程中继承一个。如果在应用程序全局范围都没有设置,默认是appClassLoader类加载器。

这次破坏双亲委托并不是通过修改了loadClass方式,而是说对于类加载的行为,我们不是让其使用默认的类加载器,而是显示的指定类加载器加载,并且这个类加载器通过线程上下文加载器来传递的

无关的几个问题

是否可以实现一个自己的java.lang.String类

你可以看到网络上的回答是可能可以,打破类双亲委托的机制,是可能可以进行加载

首先我们看看如果可以加载进入会发生什么问题

  • 首先重复加载了String类,虽然这个类是不一样的实现
  • 假设可以加载进去,那么在类加载过程中,解析的时候,如何将符号引用转化为正确的直接应用呢,现在堆里面有两个全限定名称都堆java.lang.String的类对象,应该指向那一个,而且在进行方法的动态绑定的过程中,自己实现的String类没有对应的方法,就会出现程序异常,不能正常运行了

这么严重的问题,显然是不可能让它发生的,那么java是怎么避免这些问题发生呢

  • java在加载以java.或者javax.开头的类,是不让你命名的,如果你这么命名,你编译能过,但是你的这个代码是不由bootstrap classloader加载的,其他类加载器会对这个命名进行检测,抛出异常java.lang.SecurityException: Prohibited package name

结论就应该是不可以,其实类加载也只是仅仅只能控制类加载过程个一部分,类加载过程中加载的部分可以细分分为3步:

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

而classloader.loadClass只可以控制第一点,后面两点都是通过调用defineClass来达成的,这个方法里面,就有对上面提到的包名的检测,并且它最终是调用native方法来实现的,你不能跳过它

下面的程序是否可以正常运行

这个我在写demo的时候,发生的一个问题,将AngleManager代码修改成如下,然后重新打包,运行程序,就会出现以下错误和空指针的问题

Exception in thread "main" java.util.ServiceConfigurationError: api.Angle: Provider hell.Lucifer could not be instantiated

代码如下

package api;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

public class AngleManager {
//	新位置
    static {
        System.out.println("angleManagerInit");
        angleManagerInit();
    }

    private static int angleIndex = 0;
    private static List<Angle> angles = new ArrayList<Angle>();

    /**
     * 提供注册功能
     * @param angle
     */
    public static void registerAngle(Angle angle){
        angles.add(angle);
    }

    /**
     * 获取一个接口实现
     * @return
     */
    public static Angle angleFall(){
        if(angles.size() > 0 && angleIndex < angles.size()){
            return angles.get(angleIndex ++);
        }
        return null;
    }

    /**
     * 提供初始化操作,里面使用spi,来发现第三方的接口实现
     */
    private static void angleManagerInit() {
        ServiceLoader<Angle> angleServiceLoader = ServiceLoader.load(Angle.class);
        Iterator<Angle> angleIterator = angleServiceLoader.iterator();
        while(angleIterator.hasNext()) {
//            这里会调用Class.forName(name, init, classloader);
            angleIterator.next();
        }
    }
    
//	旧位置
//    static {
//        System.out.println("angleManagerInit");
//        angleManagerInit();
//    }


}

看到两次位置的对比,你基本就应该可以猜到发生问题的原因了,这里就不说明了

原文出处:https://www.cnblogs.com/faberry/p/10594002.html

ClassLoader & 双亲委派 & 类初始化过程

ClassLoader & 双亲委派 & 类初始化过程

1.class sycle

  类加载的生命周期:加载(Loading)–> 验证(Verification)–> 准备(Preparation)–> 解析(Resolution)–> 初始化(Initialization)–> 使用(Using)–> 卸载(Unloading)。

 

 

 

  关注点 1: loading 将 class 二进制文件加载到内存中

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

  加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中。

  关注点 2: verifaction 这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求

    •   文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理
    •   元数据验证:对字节码描述的信息进行语义分析,以确保其描述的信息符合 java 语言规范的要求。
    •   字节码验证:这个阶段的主要工作是进行数据流和控制流的分析。任务是确保被验证类的方法在运行时不会做出危害虚拟机安全的行为。
    •   符号引用验证:这一阶段发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),主要是对类自身以外的信息进行匹配性的校验。目的是确保解析动作能够正常执行。

  关注点 3: preparation 对静态变量赋默认值,而不是初始值(目标指),准备阶段是正式为静态变量分配内存并设置初始值,这些内存都将在方法区中进行分配,这里的变量仅包括类变量(静态变量)不包括实例(成员)变量。
  关注点 4: resolution :解析是虚拟机将常量池的符号引用替换为直接引用的过程
     

    •   符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
    •   直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接饮用是与内存布局相关的。
    •   类或接口的解析
    •   字段的解析
    •   类方法解析
    •   接口方法解析

  关注点 5: initializing 负责执行类中的静态初始化代码、构造器代码以及静态属性的初始化 (目标值) initializing 负责执行类中的静态初始化代码、构造器代码以及静态属性的初始化 (目标值)

 

 

  •   遇到 new、getstatic、putstatic、invokestatic 这 4 个字节码指令时,如果类没有进行过初始化,出发初始化操作。 访问 final 变量除外 ??
  • 使用 java.lang.reflect 包的方法对类进行反射调用时。
  • 当初始化一个类的时候,如果发现其父类还没有执行初始化则进行初始化。
  • 虚拟机启动时用户需要指定一个需要执行的主类,虚拟机首先初始化这个主类。
  • 动态语言支持 java,lang.invoke.MethodHandle 解析结果为 REF_getstatic REF_invokestatic 的方法句柄时,该类必须初始化。

注意:接口与类的初始化规则在第三点不同,接口不要气所有的父接口都进行初始化。

2 不同类加载器说明

引导类加载器 (BootStrap) :
主要负责加载 JVM 自身需要的类,该加载器由 C++ 实现,加载的是 <JAVA_HOME>/lib 下的 class 文件,或者 -Xbootclasspath 参数指定的路径下的 jar 包,注意必须由虚拟机按照文件名识别加载 jar 包,如 rt.jar,如果文件名不被虚拟机识别,即使把 jar 丢到 lib 目录下也是没有最用的(出于考虑,Bootstrap 启动类加载器只加载 java、javax、sun 开头的类),引导类加载器在 hotspot 虚拟中使用 C++ 语言实现,它是虚拟机的一部分。除了引导类加载器之外,其他类加载器都是由 Java 语言实现,并且全部集成自 java.lang.ClassLoader,他们是独立于虚拟机外部的。

扩展类加载器 (Extension) :
扩展类加载是指 Sun 公司实现的类,它是由 Sun 的 ExtClassLoader 实现的,是 Lancher 类的静态内部类。他负责加载 <JAVA_HOME>/lib/ext 目录下或有系统变量 - Djava.ext.dir 指定路径中的类库,开发者可以直接使用标准扩展类加载器

 1 public class Launcher {
 2   ......
 3 static class ExtClassLoader extends URLClassLoader {
 4         private static volatile Launcher.ExtClassLoader instance;
 5 
 6         public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
 7             if (instance == null) {
 8                 Class var0 = Launcher.ExtClassLoader.class;
 9                 synchronized(Launcher.ExtClassLoader.class) {
10                     if (instance == null) {
11                         instance = createExtClassLoader();
12                     }
13                 }
14             }
15 
16             return instance;
17         }
18 }
19   .....
20 }
21
View Code

 

 

系统类加载器(应用程序加载器 AppClassLoader):
它是由 Sun 的 AppClassLoader 实现的,它负责加载系统路径 java -classpath 或者 - D java.class.path 指定路径下的类库,也就是我们经常使用到的 classpath 路径,开发者直接使用系统类的加载器,一般情况下该类加载器是程序组中默认的类加载器,通过 ClassLoad.getSystemClassLoader () 方法可以获取到该类的加载器。

自定义类加载器 (Custom ClassLoader):
在程序运行期间,通过 java.lang.ClassLoader 的子类动态加载 class 文件,体现 java 动态实时类装入特性
 

3.ClassLoader 加载类过程(双亲委派)

JVM 在加载类时默认采用的双亲委派机制。通俗讲,就是某个特定的类加载器在接到类加载器的请求时,受限将加载任务委传给父类加载器, 依次递归,如果父类加载器可以完成类的加载任务,就返回成功;只有父类加载器无法完成此加载器任务时,才去自己加载

 

 

 

4.ClassLoader 加载类过程(双亲委派流程图)

 

 

 

5. 为什么需要双亲委派机制?

为了系统类的安全,类似 “java.lang.Object” 这种核心类,JVM 需要保证他们生成的对象都会被认定为同一类型 ,如果用户编写了一个 lava.lang.Object 的同名类并放在 ClassPath 中,多个类加载器都去加载这个类到内存中,系统中会出现多个不同的 Object 类,那么类之间的比较傲结果以及唯一性将无法保证,并且如果不使用这种双亲委派模型将会给虚拟机的安全带来安全隐患。所以要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。即 “通过代理模式,对于 java 核心类库的类的加载工作由引导类加载器统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是相互兼容的”

好处是防止内存中出现多份相同的字节码。

6. 能不能自己写个类叫 java.lang.System?答案:通常不可以,但可以采取另类方法达到这个需求。

解释:为了不让我们写 System 类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而 System 类是 Bootstrap 加载器加载的,就算自己重写,也总是使用 Java 系统提供的 System,自己写的 System 类根本没有机会得到加载。

但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器加载一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

7. 如何自定义类加载器

  • 继承 ClassLoader
  • overwrite findClass()

8. 如何打破双亲委派?

  • 集成 ClassLoader
  • 重写 loadClass 方法

 

原文出处:https://www.cnblogs.com/yueyazhishang/p/12508986.html

ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制

ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制

 ClassLoader类加载器

Java 中的类加载器大致可以分成两类:

 

一类是系统提供的:

 

  • 引导类加载器(Bootstrap classloader:它用来加载 Java 核心库(如rt.jar),是用原生代码而不是java来实现的,并不继承自java.lang.ClassLoader,除此之外基本上所有的类加载器都是java.lang.ClassLoader类的一个实例
  • 扩展类加载器Extension classloader:它用来加载 Java 扩展库Java 虚拟机的实现会提供一个扩展库目录(一般为%JRE_HOME%/lib/ext)。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器System classloaderApp classloader:它根据当前Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。

 

另外一类则是由 Java 应用开发人员编写的:

开发人员可以通过继承java.lang.ClassLoader 类的方式实现自己的类加载器,重写 findClass()方法,以满足一些特殊的需求。

 

当一个类加载和初始化的时候,类仅在有需要加载的时候被加载,类加载都是在运行期间执行的;

 

比如我们自定义一个类People,然后用该类创建对象,就首先会加载改类到方法区

 

 

假设你有一个应用需要的类叫作 Abc.class,首先加载这 个类的请求由 应用类加载器委托给它的父类加载器 Extension 类加载器, 然后再委托给 Bootstrap 类加载器;Bootstrap 类加载器 会先看看 rt.jar 中有没有 这个类,因为并没有这个类,所以这个请求又回到 Extension 类加载器,它会查 看 jre/lib/ext 目录下有没有这个类,如果这个类被 Extension 类加载器找到了, 那么它将被加载,而 应用类Application类加载器不会加载这个类;而如果这个类没有 被 Extension 类加载器找到,那么再由 应用类Application 类加载器从 classpath 中寻找, 如果没找到,就会抛出ClassNotFoundException异常。

如下图所示:

 

 

 

双亲委托模型

从1.2版本开始,Java引入了双亲委托模型,从而更好的保证Java平台的安全

在此模型下,当一个装载器请求装载某个类时,它首先委托自己的parent去装载,若parent能装载,则返回这个类所对应的Class对象;若parent不能装载,则由parent的请求者去装载。

为什么双亲委托模型更加安全?

因为在此模型下用户自定义类装载器不可能装载应该由父类加载器载的可靠类(如Extension加载器(jre/lib/ext)和Bootstrap加载器(rt.jar)下的class),从而防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。

比如用户自定义自己的一个名为String的恶意类,想要替换rt.jar下面java.lang.String,加载时,由于双全委托模型,首先请求到App ClassLoader,然后再到Extension ClassLoader,再到Bootstrap ClassLoader,由于已经加载过java.lang.String, java.lang包的String类不会再替换。

也就是重要的核心类和公共类都被Bootstrap和Extension加载了,不会被恶意类来替换这两个加载器加载的类。

 

Java类加载机制

 

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示。

 

 

 

 

加载

 

在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下3件事情:

 

1.通过一个类的全限定名(包名.类名吧)来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);

 

2.将这个字节流所代表的静态存储结构转化为方法区运行时数据结构

 

3.在内存中生成一个代表这个的java.lang.Class对象,作为方法区中这个各种数据访问入口;

 

验证

 

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

 

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:确保解析动作能正确执行。

 

 

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

 

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.

 

解析

解析阶段是虚拟机将常量池内符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

 

初始化

类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码

准备阶段,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的计划去初始化类变量其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法的过程.
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:

1

2

3

4

5

6

7

8

9

public class Test

{

    static

    {

        i=0;

        System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)

    }

    static int i=1;

}

 

 

 

一个例子

class SingleTon {

    private static SingleTon singleTon = new SingleTon();

    public static int count1;

    public static int count2 = 0;

 

    private SingleTon() {

        count1++;

        count2++;

    }

 

    public static SingleTon getInstance() {

        return singleTon;

    }

}

 

public class Test {

    public static void main(String[] args) {

        SingleTon singleTon = SingleTon.getInstance();

        System.out.println("count1=" + singleTon.count1);

        System.out.println("count2=" + singleTon.count2);

    }

}

分析:

1:SingleTon singleTon = SingleTon.getInstance();调用了类的SingleTon调用了类的静态方法,触发类的初始化
2:类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0
3:类初始化阶段,为类的静态变量赋值和执行静态代码块。singleton赋值为new SingleTon()调用类的构造方法
4:调用类的构造方法后count=1;count2=1
5:继续为count1与count2赋值,此时count1没有赋值操作,所以count1为1,但是count2执行赋值操作就变为0

 

Java反射中Class.forName和classloader的区别

 

JavaClass.forNameclassloader都可以用来对类进行加载。

 

Class.forName除了将类的.class文件加载到jvm之外,还会对类进行解释,执行类中的static

ClassLoader 只干一件事情,就是将.class文件加载到jvm不会执行static中的内容,只有在newInstance才会去执行static块。

获得一个ClassLoader loader = ClassLoader.getSystemClassLoader();

 

Class.forName(name,initialize,loader)带参数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。

 

static关键字父类子类静态代码块

https://www.cnblogs.com/theRhyme/p/7538020.html

根据例子总结:
结果是集合{父类static,子类static,父类大括号,父类构造函数,子类大括号,子类构造函数}的一个子集。

核心就是类加载static块优先初始化,子类初始化会触发父类的初始化。

一个类中,被加载过的静态代码不会被重复加载

{}大括号里的是初始化块,这里面的代码在创建java对象时执行,而且在构造器之前其实初始化块就是构造器的补充,初始化块是不能接收任何参数的,定义的一些所有对象共有的属性、方法等内容时就可以用初始化块了初始化!! 好处是可以提高初始化块的复用,提高整个应用的可维护性。

 

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

 

@Autowired static变量

@Component

public class A{

  @Autowired

  private static RestTemplate restTemplate;

}

在Springframework里,我们是不能@Autowired一个静态变量,使之成为一个Spring bean的。因为当类加载器加载静态变量时,Spring上下文尚未加载。所以类加载器不会在bean中正确注入静态类,并且会失败。

解决思路:不要在加载器加载静态变量时通过@Autowired初始化该静态变量,可以在后续@Autowired初始化该静态变量,比如构造方法,@PostConstruct

 

双亲委派模式破坏-JDBC

 

 

JDBC之所以要破坏双亲委派模式是因为,JDBC的核心在rt.jar中由启动类加载器加载,而其实现则在各厂商实现的的jar包中,根据类加载机制,若A类调用B类,则B类由A类的加载器加载,也就是说启动类加载器要加载jar包下的类,我们都知道这是不可能的,启动类加载器负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,那么JDBC是如何加载这些Driver实现类的?
通过Thread.currentThread().getContextClassLoader()得到线程上下文加载器来加载Driver实现类。
TODO待写

 

 

 参考来源:

https://blog.csdn.net/RogueFist/article/details/79575665

http://blog.csdn.net/stypace/article/details/40613953

http://www.importnew.com/18548.html

http://www.cnblogs.com/mangosoft/p/6485790.html

https://www.cnblogs.com/theRhyme/p/7538020.html

https://blog.csdn.net/sinat_34976604/article/details/86723663

https://www.cnblogs.com/aspirant/p/8991830.html

 

ClassObject.getClass,ClassName.class和Class.forName(“ ClassName”)之间的区别

ClassObject.getClass,ClassName.class和Class.forName(“ ClassName”)之间的区别

我想了解Class.forName(“
ClassName”)和ClassObject.getClass是否都返回该类的运行时实例。然后为什么在比较从这两个对象获得的结果Class对象时获取一个布尔值false(如果我们使用==或equals进行比较)。
我也想知道在类名上调用.class方法的确切用途。我读到它是在编译时等确定的,但目的是什么。Class.forName(“ ClassName”)是否足够?
谢谢

答案1

小编典典

Class.forName加载并初始化类。obj.getClass()返回加载到permgen中的类对象。如果该类由同一类加载器加载,==则必须返回true。当您看到false==比较时,意味着它们由不同的类加载器加载。

Java class.forname 详解

Java class.forname 详解

    第一次见到 class.forName(String className) 这句代码应该是在使用 JDBC 方式连接数据库的时候。

实例

    Class.forName 传入 com.mysql.jdbc.Driver 之后,就知道我连接的数据库是 mysql,这是为什么呢,看看源代码:

 

    发现它调用了 forName0 方法,继续跟踪再看看:

 
    native 方法,源码也只能到此结束了。再看看看官方文档的描述: https://docs.oracle.com/javase/7/docs/api/java/lang/Class.html#forName(java.lang.String)
 

Returns the Class object associated with the class or interface with the given string name, using the given class loader. Given the fully qualified name for a class or interface (in the same format returned by getName) this method attempts to locate, load, and link the class or interface. The specified class loader is used to load the class or interface. If the parameter loader is null, the class is loaded through the bootstrap class loader. The class is initialized only if the initialize parameter is true and if it has not been initialized earlier.

    返回一个给定类或者接口的一个 Class 对象,如果没有给定 classloader, 那么会使用根类加载器。如果 initalize 这个参数传了 true,那么给定的类如果之前没有被初始化过,那么会被初始化。我们在 JDBC 第一步的时候,传入的参数是 com.mysql.jdbc.Driver。也就是说这个类会被初始化,我们看一下这个类里面的内容:

    一个构造函数和一个静态代码块。我们知道,类在初始化的时候,静态代码块的内容会被执行的。

    也就是说我们 Class.forName 和直接写 DriverManager.registerDriver(new Driver) 两者功能是等同的。

    换成这种写法,再试试看:

    发现代码,还是正常的执行了。

    总结一下: Class.forName 方法的作用,就是初始化给定的类。而我们给定的 MySQL 的 Driver 类中,它在静态代码块中通过 JDBC 的 DriverManager 注册了一下驱动。我们也可以直接使用 JDBC 的驱动管理器注册 mysql 驱动,从而代替使用 Class.forName

 
 
 

Class 类概念

    Class 也是一个 Java 类,保存的是与之对应 Java 类的 meta信息(元信息),用来描述这个类的结构,比如描述一个类有哪些成员,有哪些方法等,一般在反射中使用。

    详细解释:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例(Class 对象)。也就是说,在 Java 中,每个 java 类都有一个相应的 Class 对象,用于表示这个 java 类的类型信息。

类加载概念

    当使用一个类的时候(比如 new 一个类的实例),会检查此类是否被加载到内存,如果没有,则会执行加载操作。

    读取类对应的 class 文件数据,解析此数据,构造一个此类对应的 Class 类的实例。此时JVM就可以使用该类了,比如实例化此类,或者调用此类的静态方法。

    Java 也提供了手动加载类的接口,class.forName()方法就是其中之一。(说来说去,其实就是生成这个类的 Class)

类加载器的概念

    顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance() 方法就可以创建出该类的一个对象。 基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。

类初始化概念

    类被加载之后,jvm 已经获得了一个描述类结构的 Class 实例。但是还需要进行类初始化操作之后才能正常使用此类,类初始化操作就是执行一遍类的静态语句,包括静态变量的声明还有静态代码块。

Class.forName() 方法

    此方法含义是:加载参数指定的类,并且初始化它。

    在 jdbc 连接数据库中的应用

    在使用 jdbc 方式连接数据库时,为什么要执行 Class.forName(''驱动类名'') 方法了:将驱动类的 class 文件装载到内存中,并且形成一个描述此驱动类结构的 Class 类实例,并且初始化此驱动类,这样 jvm 就可以使用它了,这就是 Class.forName() 方法的含义。

    有数据库开发经验朋友会发现,为什么在我们加载数据库驱动包的时候有的却没有调用 newInstance() 方法呢?

    即有的 jdbc 连接数据库的写法里是 Class.forName(xxx.xx.xx); 而有一 些:Class.forName(xxx.xx.xx).newInstance(),为什么会有这两种写法呢?

    Class.forName(""); 的作用是要求 JVM 查找并加载指定的类,如果在类中有静态初始化器的话,JVM 必然会执行该类的静态代码 段。而在 JDBC 规范中明确要求这个 Driver 类必须向 DriverManager 注册自己,即任何一个 JDBC Driver 的 Driver 类的代码都必须类似如下:

public class MyJDBCDriver implements Driver { 
    static { 
        DriverManager.registerDriver(new MyJDBCDriver()); 
    } 
} 

    既然在静态初始化器的中已经进行了注册,所以我们在使用 JDBC 时只需要 Class.forName(XXX.XXX); 就可以了。

 

Class.forName() 与 xxx.class的区别

    所有的类都是在对其第一次使用时,动态加载到 JVM。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这也证明了构造器也是类的静态方法,即使在构造器之前并没有 static 关键字。因此,使用 new 操作符创建类的对象也会被当作对类的静态成员的引用。

    Java 程序在开始运行之前并非被完全加载,其各个部分是在需要时才加载的。

    类加载器首先检查 这个类的 Class 对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名称查找 .class 文件。在这个类的字节码被加载时,它们会接收验证,以确保其没有被破坏。

    Class 对象尽在需要的时候才被加载。static 初始化是在类加载时进行的。

    对于 Class.forName(),这个方法是 Class 类的一个 static 成员。Class 对象就和其他对象一样,我们可以获取并操作他的引用。forName() 是取得 Class 对象引用的一种方法。

    它是用一个包含目标类的文本名的 String 作为输入参数,返回一个 Class 对象的引用。forName() 方法发现某个累还没被加载,就会主动去加载这个类,在加载过程中,该类的是 static 子句将会被执行。

    Java 还提供了另一种方法来生成对 Class 对象的引用,即使用类字面常量。形如:xxx.class 的形式。但是这种形式创建 Class 对象引用时,不会自动初始化 Class 对象。

    初始化被延迟到了对静态方法或者非常数静态域首次引用时才执行。

public class Initable {  
    static final int staticFinal = 47;  
    static final int staticFianl2 = ClassInitialization.rand.nextInt(1000);  
    static {  
        System.out.println("Initializing Initable");  
    }  
}
public class Initable2 {  
    static int staticNonFinal = 147;  
    static {  
        System.out.println("Initializing Initable2");  
    }  
}
public class Initable3 {  
    static int staticNonFinal = 74;  
    static {  
        System.out.println("Initializing Initable3");  
    }  
}
public class ClassInitialization {  
    public static Random rand = new Random(47);  
    public static void main(String[] args) throws ClassNotFoundException {  
        Class initable = Initable.class;  
        System.out.println("after creating Initable ref");  
        //Does not trigger initialization  
        System.out.println(Initable.staticFinal);  
        //Does trigger initialization  
        System.out.println(Initable.staticFianl2);  
        //Does trigger initialization  
        System.out.println(Initable2.staticNonFinal);  
        Class initable3 = Class.forName("chapter13.t2.Initable3");  
        System.out.println("after creating Initable3 ref");  
        System.out.println(Initable3.staticNonFinal);  
    }  
}

    执行输出结果:

    从对 initable 引用的创建中可以看到,仅使用 .class 语法来获取对类的引用不会发生初始化。但是 Class.forName() 立即就进行了初始化。

    如果一个 static final 值是“编译期常量”,就像 Initable.staticFinal 那样,那么这个值不需要对 Initable 类进行初始化就可以被读取。

    但是如果只是将一个域设置为 static 和 final 的,还不足以确保这种行为,例如 Initable.staticFianl2 的访问将强制进行类型的初始化,因为他不是一个编译器常量

 

我们今天的关于JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制的分享已经告一段落,感谢您的关注,如果您想了解更多关于ClassLoader & 双亲委派 & 类初始化过程、ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制、ClassObject.getClass,ClassName.class和Class.forName(“ ClassName”)之间的区别、Java class.forname 详解的相关信息,请在本站查询。

本文标签: