1. Class类的理解
针对于编写好的 .java 源文件进行编译(使用 javac.exe),会生成一个或多个 .class 字节码文件。接着,我们使用 java.exe 命令对指定的 .class 文件进行解释运行。这个解释运行的过程中,我们需要将 .class 字节码文件加载到内存中。加载到内存中的 .class 文件对应的结构即为 Class 的一个实例。
比如:加载到内存中的Person类或String类,都作为CLass的一个一个的实例
Class clazz1 = Person.class; // 运行时类
Class clazz2 = String.class;
运行时类在内存中会缓存起来,在整个执行期间,只会加载一次。
2. 获取Class实例的四种方式
方式 1:要求编译期间已知类型
前提:若已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序性能最高
Class clazz = String.class;
方式 2:获取对象的运行时类型
前提:已知某个类的实例,调用该实例的 getClass() 方法获取 Class 对象
Class clazz = "字符串的实例".getClass();
方式 3:可以获取编译期间未知的类型
前提:已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法 forName() 获取,可能抛出 ClassNotFoundException
Class clazz = Class.forName("java.lang.String");
方式 4:使用类的加载器
前提:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass("java.lang.String");
3. 哪些类型可以有 Class 对象
所有 Java 类型
- class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
- interface:接口
- []:数组
- enum:枚举
- annotation: 注解@interface
- primitive type:基本数据类型
- void
Class c1 = Object.class;
Class c2 = Comparable.class;
Class c3 = String[].class;
Class c4 = int[][].class;
Class c5 = ElementType.class;
Class c6 = Override.class;
Class c7 = int.class;
Class c8 = void.class;
Class c9 = Class.class;
int[] a = new int[10];
int[] b = new int[100];
Class c10 = a.getClass();
Class c11 = b.getClass();
// 只要元素类型与维度一样,就是同一个 Class
System.out.println(c10 == c11);
4. 类的加载
类在内存中完整的生命周期:加载-->使用-->卸载。其中加载过程又分为:装载、链接、初始化三个阶段。
装载(Loading)
- 将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象。此过程由类加载器完成
链接(Linking)
- 验证(Verify):确保加载的类信息符合 JVM 规范,例如:以 cafebabe 开头,没有安全方面的问题。
- 准备(Prepare):正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- 解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化(Initialization)
- 执行类构造器 <clinit>() 方法的过程。类构造器 <clinit>() 方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确加锁和同步。
5. 类的加载器classloader
将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口。
类缓存:标准的 JavaSE 类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过 JVM 垃圾回收机制可以回收 这些 Class 对象。
类加载器的分类(JDK8 为例)
JVM 支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader、启动类装载器)和自定义类加载器(User-Defined ClassLoader,包括扩展类、应用程序类、用户自定义类)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
(1)启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用 C/C++语言实现的,嵌套在 JVM 内部。获取它的对象时往往返回 null
- 它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路径下的内容)。用于提供 JVM 自身需要的类。
- 并不继承自 java.lang.ClassLoader,没有父加载器。
- 出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
(2)扩展类加载器(Extension ClassLoader)
- Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现。
- 继承于 ClassLoader 类
- 父类加载器为启动类加载器
- 从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载
(3)应用程序类加载器(系统类加载器,AppClassLoader)
- java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现
- 继承于 ClassLoader 类
- 父类加载器为扩展类加载器
- 它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
- 应用程序中的类加载器默认是系统类加载器。
- 它是用户自定义类加载器的默认父加载器
- 通过 ClassLoader 的 getSystemClassLoader()方法可以获取到该类加载器
(4)用户自定义类加载器
- 在 Java 的日常应用程序开发中,类的加载几乎是由上述 3 种类加载器相互配合执行 的。在必要时,我们还可以自定义类加载器,来定制类的加载方式
- 体现 Java 语言强大生命力和巨大魅力的关键因素之一便是,Java 开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的 JAR 包,也可以是网络上的远程资源。
- 同时,自定义加载器能够实现应用隔离,例如 Tomcat,Spring 等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比 C/C++程序要好太多,想不修改 C/C++程序就能为其新增功能,几乎是不可能 的,仅仅一个兼容性便能阻挡住所有美好的设想。
- 自定义类加载器通常需要继承于 ClassLoader
// 获取默认的系统类加载器
ClassLoader classloader = ClassLoader.getSystemClassLoader();
// 查看某个类是哪个类加载器加载的
ClassLoader classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader();
// 如果是根加载器加载的类,则会得到 null
ClassLoader classloader1 = Class.forName("java.lang.Object").getClassLoader();
// 获取某个类加载器的父加载器
ClassLoader parentClassloader = classloader.getParent();