JavaSE学习笔记 详解Java反射机制(一)--类的加载过程,Java类加载器以及双亲委托模式

2 篇文章 0 订阅
1 篇文章 0 订阅


在学习Java反射相关的知识前,我们需要对Java类的加载、连接和初始化的知识进行学习了解。这些知识可能比较枯燥,但却是底层的知识。掌握了这些底层运行原理,对于后续的学习的有更好的帮助。


1.类的加载、连接和初始化

当程序主动使用某个类时,如果该类没有加载到内存中,系统会通过加载、连接、初始化三个步骤对该类进行初始化。

一般情况下,JVM会连续完成这三个步骤,所以有时也将类的加载、连接与初始化三个步骤统称为类加载或者类初始化。

在这里插入图片描述
下面将主要对类的加载、连接和初始化三个步骤进行解析。


1.1 类的加载

系统可能在第一次使用某个类时加载该类,但也可能采用预先加载机制来预加载某个类,总之类的加载必须由类加载器完成。

类加载器通常由JVM提供,由JVM提供的类加载器通常称为系统类加载器。此外,开发过程中也可以通过继承ClassLoader基类创建自己的类加载器。

通常使用不同的类加载器,可以从不同来源加载类二进制数据,通常有如下几种来源:

从不同来源加载类的二进制数据
1.从本地系统直接读取.class文件,这是绝大多数类的加载方法;
2.从zip或者jar包等文件中加载.class文件,这种方式也比较常见;
3.网络下载.class文件;
4.从专有数据库中提取.class数据;
5.将Java源文件数据上传到服务器中动态编译为.class文件,并执行加载。
  • 需要注意的是:不管类的字节码内容从哪里加载,加载的结果是一样的。这些字节码内容加载到内存后,都将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。所有需要访问和使用类数据只能通过这个Class对象。

1.2 类的连接

当类被加载后,生成一个代表这个类的java.lang.Class对象,接着会进入连接阶段。类连接又可以分为三个阶段:验证、准备以及解析。

类的连接的三个阶段
(1)验证:确保加载的类信息符合JVM规范。例如:以cafe开头,表示没有安全的问题。
(2)准备:正式为类变量(被static修饰的变量)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
(3)解析:虚拟机常量池内的符号引用(变量名)替换为直接引用(地址)的过程。

1.3 类的初始化

类的初始化主要就是对静态的类变量进行初始化:

  • (1)执行类构造器clint()方法的过程。
  • (2)当初始化一个类时,如果发现这个类的父类还没有进行初始化,则需要先完成对父类的初始化。
  • (3)虚拟机会保证一个类的clint()方法在多线程环境下能够被正常的加锁和同步。
//类的初始化
class Base{
    private static int a=getNum();
    //静态代码块
    static {
        ++a;
        System.out.println("(2)a="+a);
    }
    static {
        ++a;
        System.out.println("(3)a="+a);
    }
    public static int getNum(){
        System.out.println("(1)a="+a);
        return 1;
    }
}

class TestClint extends Base{
    private static int b=getNum();
    static {
        ++b;
        System.out.println("(5)b="+b);
    }
    static {
        ++b;
        System.out.println("(6)b="+b);
    }
    public static int getNum(){
        System.out.println("(4)b="+b);
        return 1;
    }
}

public class MyTest {
    public static void main(String[] args) {
        TestClint testClint = new TestClint();

    }
}

运行后的结果为:
在这里插入图片描述


1.3.1 会触发类的初始化操作

虽然类的加载和初始化在一起执行的,但实质上类的加载不一定触发类的初始化。

当Java程序首次通过下面这6种方式来使用某个类时,系统就会初始化该类。

会发生类的初始化操作
(1)当虚拟机启动时,先初始化main()方法所在的类。
(2)当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类。
(3)new一个类的对象时。
(4)使用一个类的静态的成员(包含静态变量与静态方法),但这个静态变量不能是final的。
(5)使用java.lang.reflect包的方法对类进行反射调用。
/***
 *类的加载包含类的初始化
 * (1)当虚拟机启动时,先初始化main()方法所在的类
 * (2)当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类。
 * (3)new一个类的对象时
 * (4)使用一个类的静态的成员(包含静态变量和静态的方法),但是这个静态变量不能是final的。
 * (5)使用了java.lang.reflect包的方法对类进行反射调用。
 */

class Father{
    static {
        //当初始化一个类时,如果父类没有被初始化,则先会初始化它的父类
        System.out.println("main方法所在的父类(1)");
    }
}
public class MyTest1 extends Father {
    static {
        //当虚拟机启动时,先初始化main()方法所在的类
        System.out.println("main()方法所在的类(2)");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        new A();//第一次使用A就是在创建它的对象
        //使用一个类的静态的成员,但是这个静态变量不能是fianl的
        B.test();
       //使用java.lang.reflect包的方法对类进行反射调用
        Class<?> aClass = Class.forName("org.westos.demo.C");
    }
}


class A{
    static {
        System.out.println("A类初始化");
    }
}


class B{
    static {
        System.out.println("B类初始化");
    }
    public static void test(){
        System.out.println("B类的静态方法");
    }
}

class C{
     static {
         System.out.println("C类初始化");
     }

}

运行后的结果为:
在这里插入图片描述


1.3.2 不会触发类的初始化操作

说完了会触发类的初始化操作,我们接下来聊聊不会触发类的初始化的操作。

不会触发类的初始化操作
1.引用静态常量(被fianl修饰)不会触发类的初始化
2.当访问一个静态域时,只有真正声明这个域的类才会被初始化。例如:当通过子类引用父类的静态变量,不会导致子类初始化
3.通过数组定义类引用,不会触发此类的初始化
/***
 * 类的加载过程中,没有带上类的初始化
 * (1)引用静态常量不会触发此类的初始化
 * (2)当访问一个静态域(静态变量,静态方法),只有声明这个域的类才会被初始化。
 *    即当通过子类引用父类的静态变量,静态方法时,不会导致子类初始化。
 *
 *  (3)通过数组定义类引用时,不会触发此类的初始化。
 *
 *
 *
 */
public class MyTest2 {
    public static void main(String[] args) {
        System.out.println(D.num);//D类不会初始化,因为num是final的

        System.out.println(F.num);
        F.test(); //F类不会初始化,E类会初始化,因为num和test()是在E类中声明的

        //G类会初始化,此时还没有正式用的G类
        G [] arr=new G[5];//没有创建G的对象,创建的是准备用来装G对象的数组对象
    }
}

class D{
    public static final int num=10;
    static{
        System.out.println("D类的初始化");
    }
}

class E{
    static int num=10;
    static {
        System.out.println("E父类的初始化");
    }
    public static  void test(){
        System.out.println("E父类的静态方法");
    }
}

class F extends E{
    static {
        System.out.println("F子类的初始化");
    }
}

class   G{
    static {
        System.out.println("G类的初始化");
    }
}


2.详解类加载器(面试常问)

前面我们所提到的类加载的过程是通过类加载器来完成的。所以这里很有必要介绍下Java类加载器。


2.1 Java类加载器

Java类加载的分类具体解释
1.引导类加载器(Bootstrap ClassLoader),又称为根类加载器它负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar等或sun.boot.class.path路径下的内容),是用原生代码(C/C++)来实现的,并不继承自java.lang.ClassLoder,所以通过Java代码获取引导类加载器对象将会得到null。
2.扩展类加载器(Extension ClassLoader)它由sun.misc.Launcher$ExtClassLoader实现,是java.lang.ClassLoader的子类,负责加载Java的扩展库(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)。
3.应用程序类加载器(Application Classloader)它由sun.misc.Launcher$AppClassLoader实现,是java.lang.ClassLoader的子类,负责加载Java应用程序类路径(classpath、java.class.path)下的内容。通俗的讲:项目路径bin文件夹下的字节码,以及如果你配置环境变量classpath。
4.自定义类加载器一般什么情况下需要定义自定义类加载器:(1)字节码文件需要加密与解密操作;(2)字节码的路径不在常规路径,有自定特定的路径。例如:tomcat

2.2 Java中类加载器的双亲委托模式

类加载器负责加载所有的类,系统会为所载入内存中的类生成一个java.lang.Class实例。

一旦一个类被载入JVM中,同一个类就不能再次被载入。但问题出现了,我们如何辨识"同一个类"呢?在JVM中,一个类使用其全限定类名和其类加载器作为唯一标识。换句话说,同一个类如果使用两个类加载器分别进行类加载,JVM将视为"不同的类",它们是互不兼容的。

类加载器在执行任务时,如何确保一个类的全局唯一性?

在设计Java虚拟机时,设计者们通过一种称为"双亲委派模型"的委派机制来约定类加载器的加载机制。

按照双亲委派模型的规则,除了引导类加载器(根类加载器)之外,程序中的每一个类加载器都应该有一个超类加载器。比如应用程序类加载器的超类加载器是引导类加载器,而应用程序类加载器的超类加载器是扩展类加载器,而自定义加载器的超类就是应用程序类加载器。

在这里插入图片描述

Java类加载器双亲委托模式具体处理详解:

  • 当一个类加载器接收到一个类加载任务时,它不会立即展开加载,会先检测这个类是否加载过,在方法区寻找该类的Class对象是否存在。如果存在就加载过了,直接返回Class对象。否则会将加载任务委托给它的超类加载器来执行,一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止,如果超类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器去执行加载,如果所有的类加载器都加载失败,就会报java.lang.ClassNotFoundException或java.lang.NoClassDefFoundError。

具体的处理流程也可以见下图:

在这里插入图片描述

需要注意的是:

  • 由于Java虚拟机规范并没有要求类加载器的加载机制一定要使用双亲委托模式,只是建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器就接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
/**Java中类加载器的双亲委托模式
 * 1.类加载器设计时,这四种类加载器是有层次结构的,但是这种层次结构不是通过继承关系来实现的。
 * 是通过组合的方式,来实现“双亲”的认亲过程。
 * 例如:应用程序类加载器把扩展类加载器称为“父加载器”,在应用程序类加载器中保留应用程序类加载器的一个引用(成员变量),
 * 把变量名称设计为parent。所有的类加载器有一个getParent(),即获取父加载器的方法。
 *
 *2.有什么用?
 * 目的是为了安全,而且各司其职的作用
 *
 * 当应用程序类加载器接到加载某个类的任务时,例如:java.lang.String
 * (1)会先在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载了。
 * (2)如果没有找到,即没有加载过。会把这个任务先提交给“父加载器”
 *
 *
 *当扩展类加载器接到加载某个类的任务时,例如:java.lang.String
 * (1)会先在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载了。
 *(2)如果没有找到,即没有加载过。会把这个任务先提交给“父加载器”
 *
 *当引导类加载器接到加载某个类的任务时,例如:java.lang.String
 * (1)会先在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载了。
 * (2)如果没有找到,即没有加载过。会在它负责的范围内尝试加载。
 *     如果找到了,那么就返回这个类的Class对象,就结束了。
 *     如果没有找到,那麽就把这个任务往回传,让“子加载器”扩展类加载器去加载。
 *
 *“子加载器”扩展类加载器接到“父加载器”返回的任务,去它负责的范围内进行加载。
 *    如果找到了,那么就返回这个类的Class对象,就结束了。
 *    如果没有找到,那麽就把这个任务往回传,让“子加载器”应用程序类加载器去加载。
 *
 *
 * “子加载器”应用程序类加载器接到“父加载器”返回的任务,去它负责的范围内进行加载。
 *   如果找到了,那么就返回这个类的Class对象,就结束了。
 *   如果没有找到,那麽就报错ClassNotFoundException或java.lang.NoClassDefError。
 *
 *
 *
 * Java中是认为不同的类加载器,加载的类名相同的类,识别为不同的类。
 *
 *
 */
public class MyTest {
    public static void main(String[] args) {
        //1.先获取这个类的Class对象
        Class<MyTest> myClass = MyTest.class;
        //Class<String> myClass = String.class;

        //2.获取它的类加载器对象
        ClassLoader classLoader = myClass.getClassLoader();
        System.out.println("当前类的加载器:"+classLoader);
        ClassLoader parentloader = classLoader.getParent();
        System.out.println("父加载器:"+parentloader);
        ClassLoader grand = parentloader.getParent();
        System.out.println("爷爷加载器:"+grand);
    }

}

运行后的结果为:
在这里插入图片描述


总结

本节主要介绍了反射前的预备性知识,例如类的加载过程,Java类加载器以及双亲委托模式等介绍,值得注意的是Java类加载器的双亲委托模式是面试中的高频问点,要做到详细了解。

在这里插入图片描述

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
### 回答1: Java的日期常用的格式化模式有很多,可以使用SimpleDateFormat来输出时间。 示例代码: ``` SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String format = formatter.format(new Date()); System.out.println(format); ``` 上面代码的"yyyy-MM-dd HH:mm:ss"即为格式化模式,其: - yyyy表示四位数的年份 - MM表示两位数的月份 - dd表示两位数的日 - HH表示24小时制的两位数小时数 - mm表示两位数的分钟数 - ss表示两位数的秒数 更多关于SimpleDateFormat的格式化模式详见:https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/text/SimpleDateFormat.html ### 回答2: 时间在我们生活起着极其重要的作用,无论是记录重要事件,还是安排自己的日程,都需要用到时间。在Java,日期是处理时间相关操作的一个重要工具,而日期一个应用广泛的功能就是时间的格式化输出。 Java的日期格式化输出使用的是SimpleDateFormat来实现的。在这个,有一个format方法,可以将日期转化为特定的格式字符串,以便输出。 Java日期格式化输出的格式有很多种,这些格式都是通过特定的字符来表示的。下面,我们来介绍一下常见的日期格式化字符: - yyyy:年份,比如2018。 - MM:月份,比如08表示8月。 - dd:日期,比如28表示28号。 - HH:小时,24小时制,比如17表示下午5点。 - mm:分钟,比如30表示30分。 - ss:秒,比如05表示5秒。 此外,还有一些其他的格式化字符,具体可以参考SimpleDateFormat的官方文档。 为了使输出时间符合我们的需求,我们需要将日期对象先变为SimpleDateFormat对象,然后再调用format方法来进行格式化。如下所示: ```java import java.text.SimpleDateFormat; import java.util.Date; public class Test{ public static void main(String[] args){ //获取当前时间 Date date = new Date(); //使用SimpleDateFormat格式化输出时间 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(date)); } } ``` 上面的代码,我们先获取了当前时间,然后使用SimpleDateFormat进行格式化输出。输出的时间格式为“年-月-日 小时:分钟:秒”。 除了上面的格式之外,还有许多其他的时间格式,可以根据具体的需求来选择不同的格式。掌握Java日期格式化输出的方法,可以更好地进行日期时间处理。 ### 回答3: 在Java,有一个日期可以用来操作日期和时间,它就是java.util.Date。但是直接使用Date的输出并不符合常规的日期时间格式,因此我们需要使用DateFormat来进行格式化输出。 DateFormat是一个抽象,不能直接实例化,常用的实现有SimpleDateFormat,可以通过SimpleDateFormat对象的format方法对Date进行格式化输出。 在格式化时,可以使用特定的字符来表示特定的日期时间部分: - y:年份,如2021,表示为yyyy; - M:月份,如1月表示为M,01月表示为MM; - d:天数,如10号表示为d,10日表示为dd; - H:小时数(24小时制),表示为HH; - h:小时数(12小时制),表示为hh; - m:分钟数,表示为mm; - s:秒数,表示为ss; - S:毫秒数,表示为SSS。 在格式化字符串,还可以添加其他字符,如“年”、“月”、“日”、“时”、“分”、“秒”等文字符,用于增强输出可读性。 下面是一个示例代码: ```java import java.text.SimpleDateFormat; import java.util.Date; public class Main { public static void main(String[] args) { Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss"); String dateString = sdf.format(date); System.out.println(dateString); } } ``` 以上代码将当前日期格式化输出为:2021年08月12日 15:00:00。 需要注意的是,如果format方法的参数日期为null,则会抛出NullPointerException异常。在实际开发,我们可以先判断日期是否为null,然后再进行格式化输出。 总之,对于Java日期的格式化输出,我们需要通过DateFormat的实现(如SimpleDateFormat)来进行格式化,使用特定的字符来表示特定的日期时间部分,并可以添加其他字符以增加可读性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值