在Java的开发环境中,运行java文件需要使用:java xx.java 命令,运行java命令后,便会启动JVM,将字节码文件加载到JVM中,然后开始运行;当运行java命令时,该命令将会启动一个JVM进程,在这个JVM进程中,会保存有该JVM创建的所有线程、变量、对象,这些线程、变量、对象会共享该JVM的内存区域。
当出现以下情况时,JVM进程会退出:
- 程序正常执行结束,JVM正常退出;
- 使用System.exit(0)方法,JVM正常退出;
- 程序运行出现异常,并且没有对异常进行处理(捕获、或者抛出)时;
- 操作系统强制结束JVM进程,比如:关机、通过任务管理器强制结束等;
JVM进程一旦退出,该进程中内存区域保存的数据(线程、变量、对象等数据)将会丢失,因为其都是保存在内存中的,并没有写入到硬盘。
类加载机制
在Java 类的整个“漫长的”生命周期中,类的加载仅仅只是个开始,因为类在加载后还要经历一系列的处理,才能被JVM接受,并到处运行。
根据JVM规范,Java 程序的整个生命周期会经历5个阶段:加载 -> 链接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载;因为在链接阶段会有验证、准备、解析三个步骤,所以也可以说Java 程序的生命周期会经历7个阶段(使用验证、准备、解析三个步骤来替代链接阶段)。
当JVM要使用某个类某,而该类还未被加载进JVM内存中时,JVM会通过加载,链接,初始化三个步骤来对该类进行初始化操作,以便于后期运行。
1.类的加载
类的加载是指将类的class文件(字节码文件)载入JVM内存中,并为之创建一个java.lang.Class对象,也就是字节码对象。
类的加载过程由类加载器(ClassLoader)完成,类加载器由JVM提供,我们称之为系统类加载器,同时,我们也可以继承ClassLoader类来提供自定义类加载器。详情可查看:Java基础之堆、栈、方法区、类加载器——JVM内存模型分析
类的加载过程中也会对字节码文件进行验证,不过这里的验证并不会深入,而是把重点放在字节码文件的格式上;类加载器不仅可以加载本地字节码文件,加载jar包中的字节码,更是可以通过网络加载字节码。
2.类的链接
当类的字节码文件被加载进JVM内存之后,JVM便会创建一个对应的Class对象(也可以叫字节码对象),把字节码指令中对常量池中的索引引用转换为直接引用,接着把类的字节码指令合并到JRE中。链接包含三个步骤:
- 验证:检测被加载的类是否有正确的内部结构。
- 准备:负责为类的static变量分配内存,并根据变量的数据类型设置默认值。
- 解析:把字节码指令中对常量池中的索引引用转换为直接引用。
3.类的初始化
在完成类的链接之后,JVM便会对类进行初始化,这里的初始化主要是对static变量进行初始化,并非是类的实例化,因为类的实例化属于“使用”阶段。
类的初始化一个类包含以下几个步骤:
- 如果该类还未被加载和链接,则程序先加载并链接该类。
- 如果该类的直接父类还未被初始化,则先初始化其父类,若是父类的父类还未初始化,就会先初始化父类的父类,依次类推,直至没有父类为止。
- 如果类中有初始化语句(静态代码块),则JVM会依次执行这些初始化语句。
运行时信息
运行时信息,即Runtime Type Information,是指程序在运行时的信息。在Java 中有两种方式可以得到运行时信息:
- 一是通过RTTI,即Run-Time Type Identification,这种方式假设我们在程序编写时就已经知道了所有对象的类型,主要是通过字节码对象Class来获取;
- 二是通过“反射”机制,反射提供一组api,通过调用api,便能获取运行时的类信息;Java 中通过java.lang.reflect类库来支持反射;
Class 对象
Class类:我们平时在开发中定义的类是用来描述业务逻辑的;比如Teacher.java,Student.java等,而Class类用来描述我们所定义的业务逻辑的类,也就是描述类的类。
Class类的实例:其实就是JVM中的字节码对象,一个字节码文件有一个字节码对象,一个Class实例表示在JVM中的某个类或者接口,当然,也包括枚举和注解,因为枚举是一种特殊的类,注解是一种特殊的接口。
每一个类都有都有一个Class对象,也就是说每个类都有一个字节码对象,有一个字节码文件。当第一次使用类的时候,该类的字节码文件会被加载到JVM中,创建一个字节码对象;此时,该字节码对象就是一个Class实例。
既然每一个类都有一个Class对象,那么这些Class对象之间是如何区分它所表示的是哪一个类的字节码的呢?为了解决这个问题,Java 为Class提供了泛型:Class。
- java.lang.String类的字节码类型:Class;
- java.util.Date类的字节码类型:Class;
- java.util.ArrayList类的字节码类型:Class;
创建Class对象
创建字节码对象,有3种方式:(使用java.util.Date类做演示)
1.使用类字面量,即使用类的class属性;
Class clazz1 = java.util.Date.class;
2.使用对象的getClass();方法;
java.util.Date date = new java.util.Date();Class> clazz2 = date.getClass();
3.使用Class.forName(String className);方法;
Class> clazz3 = Class.forName("java.util.Date");
在上述的代码案例中使用了通配符“?”,通配符“?”表示“任何类”,通配符的使用使得Class对象的类型更加宽泛。
因为同一个类在JVM中只存在一个字节码对象,因此在上述的代码中存在:
clazz1 == cclazz2 == clazz3;
基本数据类型的字节码对象
在上述讲了三种获取Class对象的方式,都是类或者对象获取Class对象;但是基本数据类型不能表示为对象,不能使用getClass的方式;基本数据类型没有类名,所以也不能使用Class.forName的方式,那么该如何表示基本类型的字节码对象呢?
Java 中,所有的数据类型都有class属性,包括基本数据类型;如何获取呢?使用字面量的方式,这种方式不仅可以应用于类,还可以应用于接口、数组、和基本数据类型。语法如下:
Class clazz = 数据类型.class;
详情如下:
// 基本数据类型字节码对象// byte 字节码对象byte.class;// short 字节码对象short.class;// char 字节码对象char.class;// float 字节码对象float.class;// int 字节码对象int.class;// long 字节码对象long.class;// double 字节码对象double.class;// boolean 字节码对象boolean.class;// void 字节码对象void.class;
预加载
在JVM启动期间,会预先加载一部分核心类库的字节码文件到在JVM中,其中就包括了九大基本数据类型。并且在8大基本数据类型的包装类中,都有一个常量:TYPE,用于返回该包装类对应基本类的字节码对象;
那么如下的代码案例便解释得通了,因为Integer和int是不同的数据类型,所以Integer.class == int.class结果为false;
System.out.println(Integer.TYPE == int.class); // trueSystem.out.println(Integer.class == int.class); // false// 需要注意:Integer和int是不同的数据类型
数组的字节码对象
数组其实是对象,所以数的Class实例便可以使用引用数据类型的方式获取:
- 方式1:数组类型.class;
System.out.println(int[].class);
- 方式2:数组对象.getClass();
int[] intArr = {1, 2, 3};System.out.println(intArr.getClass());
注意:所有的具有相同的维数和相同元素类型的数组共享同一个字节码对象,和元素没有关系.
int[] intArr1 = {1, 2, 3, 4, 5};int[] intArr2 = {1, 2, 3};System.out.println(intArr1.getClass()); // class [ISystem.out.println(intArr2.getClass()); // class [I// intArr1 和 intArr2共享一同一个字节码对象,跟数组的元素没有关系
Class类和Object类
Object:描述所有的 对象,其拥有所有 对象的共同的方法。Class:描述所有的 类型,其拥有所有 类型的相同的方法;需要注意: Class类也是Object的子类。每个类都有其字节码对象,通过字节码对象能获取到类的很多信息,在Class类中提供了很多的api来获取类的运行时信息,以下是一些常用的:
// forName() 这是一组静态方法,用于获取指定类的字节码对象static Class> forName(String className); // className 类的全限定名称// className 类的全限定名称,initialize 是否初始化,loader 指定的类加载器static Class>forName(String name, boolean initialize, ClassLoader loader)// 获取类的全限定名String getName();// 获取类的简单名称,比如:Integer,String等String getSimpleName();// 获取类的完整名称String getCanonicalName();// 获取当前类的所实现的所有接口Class>[] getInterfaces();// 获取当前类的父类Class super T>getSuperclass();// 判断是否是数组booleanisArray();// 判断是否是接口booleanisInterface()
但是,上述的通过字节码对象获取类的信息是建立在我们提前知道确切的数据类型的基础之上的,如果我们事先不知道数据类型,那么便不能通过获取类的字节码对象来获取类的运行时信息;在这样的情况下,又该如何获取对象的运行时信息,该如何动态的创建类的对象?
对于这个问题,java.lang.reflect类库中提供了一个叫做“反射”的组件,可以配合Class类来共同提供动态信息获取,下面,就来一起看看吧!
反射
众所知周,对象有编译类型和运行类型。现有如下的代码案例:
Object obj = new java.util.Date();// 编译类型: Object// 运行类型: java.util.Date
需求:通过obj对象,调用java.util.Date类中的toLocaleString方法:obj.toLocaleString(); ,此时编译报错,如何解决编译错误?
案例分析:编译时,会检查该编译类型中是否存在toLocaleString方法;如果存在,编译成功,否则编译失败;此时编译失败是因为在Object类中没有toLocaleString方法,因此要将obj转换为java.util.Date类型,才能正确调用toLocaleString方法。
解决方案:因为obj的真实类型是java.util.Date类,所以我们可以把obj对象强制转换为java.util.Date类型:
java.util.Date d = (java.util.Date)obj;d.toLocaleString();
上述解决方案是在知道obj真实类型的情况下解决的,那么如果不知道obj的真实类型,就不能进行强转,也不能使用Class对象;此时,问题又该如何解决?
遇到此等问题,就不得不祭出法宝——反射了,在java.lang.reflect类库中提供了一组api用于提供对反射的支持。
反射:获取类的元数据的过程,在运行期间,动态的获取类中的成员信息,包括构造器、方法、字段、内部类、父类、接口等;并且把类中的每一种成员,都分别描述成一个新的类:
- Class:表示所有的类;
- Constructor:表示所有的构造器;
- Method:表示所有的方法;
- Field:表示所有的字段;
元数据:描述类的类的数据。
在Java 中,Class类和java.lang.reflect类库共同提供了对“反射”的支持,在java.lang.reflect类库中就包含了Constructor、Method、Field等类,用于描述类的元数据,在字节码对象中提供了获取这些元数据的api:
// 获取所有字段Field[] getFields();// 获取所有构造器Constructor>[] getConstructors();// 后去所有方法Method[]getMethods();
反射之构造器
获取构造器
在Class类中提供了获取类的构造器的方法:
// 该方法只能获取当前Class所表示类的public修饰的构造器public Constructor>[] getConstructors();// 获取当前Class所表示类的所有的构造器,和访问权限无关,可用于获取私有的构造器public Constructor>[] getDeclaredConstructors();// 获取当前Class所表示类中指定的一个public的构造器public Constructor getConstructor(Class>... parameterTypes);
在上述方法中:
- Constructor类表示类中构造器的类型,Constructor的实例就是某个类中的某一个构造器;
- 参数 parameterTypes表示:构造器参数的Class类型,方法可借由传入的参数类型来获取对应的构造器;
在Constructor类提供了用于通过反射获取到的构造器的来创建实例的方法:newInstance();
// 获取构造器public Student(String name) {}import java.lang.reflect.*;public class ReflectDemo { public static void main(String[] args) throws Exception {// 获取Studentr类的字节码对象Class clazz = Student.class;// 获取Student类的构造器:public Student(String name) {}Constructor userConstructor = clazz.getConstructor(String.class);System.out.println("userConstructor:" + userConstructor); // 通过获取的公共构造器来创建实例 Student student = userConstructor.newInstance("老夫不正经");} }
由于上述方法时在Class类中,所有在调用这些方法前需要先获取类的字节码对象,再通过字节码对象来调用获取构造器的方法。
先有Student类,在Student类中有三个构造器,分别是:无参构造器,公共带参构造器,私有构造器;代码如下:
无参构造器:
// 获取构造器public Student(String name) {}import java.lang.reflect.*;public class ReflectDemo { public static void main(String[] args) throws Exception {// 获取Studentr类的字节码对象Class clazz = Student.class;// 获取Student类的构造器:public Student(String name) {}Constructor userConstructor = clazz.getConstructor(String.class);System.out.println("userConstructor:" + userConstructor); // 通过获取的公共构造器来创建实例 Student student = userConstructor.newInstance("老夫不正经");} }
公共构造器:
// 获取构造器private Student(String name, Integer id) {}import java.lang.reflect.*;public class ReflectDemo { public static void main(String[] args) throws Exception {// 获取Studentr类的字节码对象Class clazz = Student.class;// 获取Student类的构造器:private Student(String name, Integer id) {}Constructor userConstructor = clazz.getDeclaredConstructor(String.class, Integer.class);System.out.println("userConstructor:" + userConstructor); // 通过获取的私有构造器来创建实例 // 设置构造器可以访问 userConstructor.setAccessible(true); Student student = userConstructor.newInstance("老夫不正经", 1);} }
私有构造器:在使用私有构造器创建实例时,由于私有构造器不能被外界访问,所以在创建实例前先设置其可以访问,案例代码如下:
// 获取构造器private Student(String name, Integer id) {}import java.lang.reflect.*;public class ReflectDemo { public static void main(String[] args) throws Exception {// 获取Studentr类的字节码对象Class clazz = Student.class;// 获取Student类的构造器:private Student(String name, Integer id) {}Constructor userConstructor = clazz.getDeclaredConstructor(String.class, Integer.class);System.out.println("userConstructor:" + userConstructor); // 通过获取的私有构造器来创建实例 // 设置构造器可以访问 userConstructor.setAccessible(true); Student student = userConstructor.newInstance("老夫不正经", 1);} }
如果一个类中的构造器是外界可以直接访问(非private 的),同时没有参数,那么可以直接使用Class类中的newInstance方法创建对象示例,其效果等同于使用new关键创建对象。
反射之方法
获取方法
在Class类中提供了获取类的方法的api,由于方法也是在Class类中,所以在获取方法之前,也是需要先获取其所在类的字节码对象,再来获取方法,最后再来执行方法。
Class类中获取类的方法的api:
// 获取包括自身和继承过来的所有的public方法public Method[] getMethods(); // 获取自身类中所有的方法(不包括继承的,和访问权限无关)public Method[] getDeclaredMethods();// 表示调用指定的一个公共的方法(包括继承的)public Method getMethod(String methodName, Class>... parameterTypes);// methodName:表示被调用方法的名字// parameterTypes:表示被调用方法的参数的Class类型,如String.class// 表示调用指定的一个本类中的方法(不包括继承的)public Method getDeclaredMethod(String methodName, Class>... parameterTypes);// methodName:表示被调用方法的名字// parameterTypes:表示被调用方法的参数的Class类型,如String.class
上述这些api的返回值均为Method类,Method类是用于描述反射中的方法的类,在Method类中,提供了执行反射获取的方法的方法invoke():
// 表示调用当前Method所表示的方法public Object invoke(Object obj, Object... args); // obj: 表示被调用方法底层所属对象 // args:表示调用方法是传递的实际参数// 方法返回值Object为:底层方法的执行结果
下面,通过一个案例来实践上述方法的使用:
同样,会有Student类,类中有public方法,static方法、private方法,分别用于演示通过反射获取类中public方法,static方法,private方法;
Student类:
public class Student {public void publicMethod(String name) { System.out.println("public method " + name); } public static void staticMethod(String name) { System.out.println("static method " + name); } private void privateMethod(String name) { System.out.println("private method " + name); }}
获取所有方法
// 表示调用当前Method所表示的方法public Object invoke(Object obj, Object... args); // obj: 表示被调用方法底层所属对象 // args:表示调用方法是传递的实际参数// 方法返回值Object为:底层方法的执行结果
调用public方法
import java.lang.reflect.*;public class ReflectMethodDemo {public static void main(String[] args) { Class clazz = Student.class; // 获取public 方法 Method publicMethod = clazz.getMethod("publicMethod", String.class); // 执行方法 Object result = publicMethod.invoke(calzz.newInstance(), "老夫不正经"); } }
调用static方法,使用反射调用静态方法时,由于静态方法不属于任何对象,它只属于类本身。所以在执行静态方法时,传入invoke方法的第一个参数就不能是任何对象,在这里需要将其设置为null。
import java.lang.reflect.*;public class ReflectMethodDemo {public static void main(String[] args) { Class clazz = Student.class; // 获取static 方法 Method staticMethod = clazz.getMethod("staticMethod", String.class); // 执行方法,这里将对象设置为null Object result = staticMethod.invoke(null, "老夫不正经"); } }
调用private方法,在调用私有方法之前,要设置该方法为可访问的,因为Method是AccessibleObject子类,所以在Method对象中可以通过调用setAccessible(true);来设置访问可以访问。
import java.lang.reflect.*;public class ReflectMethodDemo {public static void main(String[] args) { Class clazz = Student.class; // 获取private 方法 Method publicMethod = clazz.getMethod("privateMethod", String.name); // 设置方法为可访问的 staticMethod.setAccessible(true); // 执行方法 Object result = staticMethod.invoke(calzz.newInstance(), "老夫不正经"); } }
可变参数,方法的可变参数在底层是其实是作为数组处理的,所以在执行可变参数的方法时,可直接传入数组参数,但传参数,须得区分引用数据类型和基本数据类型;
import java.lang.reflect.*;public class StaticMethodDemo {// 可变参数是引用类型public static void doSomething(Stirng... args) { System.out.println("静态方法的可变参数,参数是引用类型:" + args); } // 可变参数是基本数据类型public static void doAnything(int... args) { System.out.println("静态方法的可变参数,参数是基本数据类型:" + args); } public static void main(String[] args) { Class clazz = StaticMethodDemo.class; // 获取 引用类型参数的方法 Method method1 = clazz.getMethod("doSomething", String[].class); // 执行方法 Object result = method1.invoke(null, new Object[] {new String[]{"lao", "fu"}}); // 正确// Object result = method1.invoke(null, new String[]{"lao", "fu"}); // 错误// Object result = method1.invoke(null, "lao", "fu"); // 错误// 获取 引用类型参数的方法 Method method1 = clazz.getMethod("doAnything", int[].class); // 执行方法 Object result = method1.invoke(null, new Object[] {new int[]{13, 14}}); // 正确Object result = method1.invoke(null, new int[]{13, 14}); // 正确// Object result = method1.invoke(null, 13, 14); // 错误 }}
在本文中,介绍了类的加载过程,详细描述的了JVM对字节码文件的处理过程,从中也看到了不少JVM底层的处理细节。
字节码对象的创建使得RTTI得以获取大量被底层屏蔽的信息,通过这些信息能让我们更加了解Java 的设计思想。
通过反射提供的强大功能,不仅可以访问到类的运行时信息,还可以访问到类中不允许被外界访问的private属性和方法,这无异于又打开了另一个新世界的大门,我们能通过反射编写动态代码,使得编程更加灵活。这也是Java 语言的一大特色,能够很好的与C、C++这样的语言区分开来。
到这里就结束了,能完整看完的小伙伴,已实属不易,给你们点赞!由于作者知识有限,若有错误之处,还请多多指出!
完结。老夫虽不正经,但老夫一身的才华