第一章:反射
一:反射的使用
根据配置文件中的re.peoperties当中的指定信息,创建类的对象并调用对象中的方法?
这样的需求在学习框架的时候特别多,可以通过外部文件配置在修改源码的前提下,控制程序,也符合设计模式的ocp原则(开闭原则)
package com.reflection; public class ReflectionQuestion { private static Properties properties = new Properties(); static { try { System.out.println(new File(".").getAbsolutePath());//D:\DevelopPackage\codeshop\Spring5.x\. InputStream resourceAsStream = ReflectionQuestion.class.getResourceAsStream("/applicationContext.properties"); // InputStream resourceAsStream = new FileInputStream("D:\\DevelopPackage\\codeshop\\Spring5.x\\reflection\\target\\classes\\applicationContext.properties"); System.out.println(resourceAsStream); properties.load(resourceAsStream); resourceAsStream.close(); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { //获取类名和方法名 String classfullpath = properties.getProperty("classfullpath"); String method = properties.getProperty("method"); System.out.println("classfullpath=" + classfullpath); System.out.println("method=" + method); //创建对象 Class clazz = Class.forName(classfullpath); Method method1 = clazz.getDeclaredMethod(method, null); method1.setAccessible(true); method1.invoke(new Cat(), null); //创建对象 Object cat = clazz.newInstance();//Object是他cat变量和存储地址对应对象的编译时的类型。 System.out.println(cat.getClass());//获取此对象的运行时的类型是什么。 Cat cat1 = (Cat)cat; cat1.hi(); } }
二:Class类概述
加载完类之后在堆中就会由JVM创建一个这个类对应的Class类型的对象,这个Class对象包含了类的完整的结构信息,通过这个对象可以拿到该类运行时的任意一个结构。
三:反射原理图
四:Java程序的三个阶段
1:编译阶段
在编译阶段Java文件程序通过编译器javac进行编译成字节码文件,字节码文件是JVM可以读懂的文件内容,包含类的信息,静态信息,成员变量,成员方法,构造方法,泛型,异常,注解等相关信息,
2:类加载阶段
Java程序将字节码文件通过类加载器进行加载到JVM内存中,在堆内存中由JVM创建这个类对应的Class对象,这是一个特殊的数据结构,也包含了该类所有的信息,属性、方法、构造器都是以一种对象数组的形式进行存储。
3:运行阶段
在运行阶段new 直接创建类的对象会造成类的加载,new出来的对象知道他属于那个Class对象
五:反射的相关类
六:反射优缺点
优点:可以动态创建和使用对象,这是框架底层的核心,使用灵活
缺点:使用反射基本上是解释执行,对执行速度有影响。
七:反射调用方法的性能问题
大概是10 ,1200,3000这样数据。
如何优化:
Mehtod,Field,Constructor对象都有一个setAccessible()方法,作用是禁用安全检查开关,提升反射效率,可以设置为TRUE,关闭访问安全检查开关。效率大致提升2到3倍。
例如:getField或者getDeclaredField之后尽行关闭即可。
第二章:Class类
- Class也是类继承Object
- Class对象不是new出来的,而是系统进行创建的。
- 对于某个类的Class对象在内存中只有一份,因此类只加载一次。
- 每个类的实例都会记得自己对应的Class对象
- 通过Class对象可以获取类中的完整结构
- Class对象是放在堆中的
- 类的字节码二进制数据是放在方法区的,有的地方成为类的元数据,(包括:方法代码,变量名,方法名,访问权限等)
Class对象都是通过类加载器ClassLoader中的loadClass方法加载类的class文件进入内存当中,在堆内存中生成Class对象,在方法区中保存类的二进制数据。
二:传统方法new对象和反射方法创建对象进行对比
1:传统new方法
在方法中new一个对象,程序执行到此的时候,也是由类加载器ClassLoader调用loadClass方法加载类进入JVM内存中,保存类的二进制信息到方法区,在堆内存中创建类的Class对象
2:反射的方法
反射的方法也是通过类加载器的loadClass方法将类加载到内存中,如果new的方式不被注释掉的话,反射的方式通过debug是不会追进入loadClass这个方法的,因为类只会被加载一次,前边加载过的话,后边就不会被加载了,通过反射方式或者new的方式都会造成类的加载,加载类的信息到内存中。
(debug这个技能需要好好学一下。)
public class ClassTest { public static void main(String[] args) throws Exception{ Cat cat = new Cat(); //如果上边的一行代码不注释掉的话,这一行的debug是不会进入ClassLoader的loadClass这里的,因为Cat类 //已经在上边加载过一次了,类只会被加载一次。 Class aClass = Class.forName("com.reflection.Car"); } }
某个类的Class对象在内存中只能有一份?
是的:因为某个具体的类都只会被加载一次,加载第一次的时候会将这个类往往是伴随着创建这个类的对象,将这个类的二进制结构数据放到方法区,创建这个Class对象在堆内存中,由于只加载一次,所以在内存中对应的类只有一个Class对象。这个Class对象可以看做是一种特殊的数据结构,属性数组,方法数组,构造器数组,对应每一个类运行时结构对象还包含对应的注解信息,泛型信息。可以进行获取。
也可以通过多个途径获取类的Class对象进行hashcode()进行比较来比较是否相等。
每个对象实例都会记得对应的Class对象是谁。
答:可以通过对象.getClass()获取到对应的Class对象。
如何获取对象变量的实际类型?
有的引用类型的变量,编译类型有可能是事件类型有可能是父类类型,但是可以通过下边的方式求得实际类型。
Object obj = new Cat(); System.out.println(obj.getClass()); //这种情况可以编译过去,但是不能运行过去,必须得是才能发生强制类型转换。 Object c = (B)new A(); System.out.println(c.getClass()); class A{} class B extends A{}
package com.reflection; public class ClassTest { public static void main(String[] args) throws Exception{ Object aClass = Class.forName("com.reflection.Cat"); System.out.println(aClass);//class com.reflection.Cat 他记得自己的Class是cat,这个根编译类型无关,实例记忆的事运行时的类型也就是实际的类型。 System.out.println(aClass.getClass());//class java.lang.Class 他记得自己的类是Class, } }
通过Class对象可以完整地得到一个类的完整结构,还报错一些列运行时结构的API
Class对象是放在堆中的,类的字节码二进制数据是放在方法区的,有的地方叫做类的元数据包括他的,属性名,方法名,访问权限,注解信息,泛型信息。
类加载完毕之后的产物。
JVM通过类加载器加载完毕一个类之后,除了会产生一个Class对象,还会得到一个类的字节码二进制数据在方法区当中,Class对象和类的字节码二进制数据之间是有联系的。那就是方法区中的二进制数据会引用到堆中的Class对象。这个Class对象是一种数据结构,正是有了这样的数据结构才能将运行时的构造内容当做对象一样操作。
Class对象是通过类加载器加载类之后,由系统创建的,他是类加载的产物,而我们各种的获取Class对象的过程并不是构建Class对象的过程而是获取已经创建好的Class对象的过程。
获取Class对象常用方法
Class.forName() 多用于配置文件的读取
类名.class 最为安全可靠,多用于当做参数进行传递。
实例.getClass() 多用于已经有对象实例了。
ClassLoader
类加载器一共有四种,四种创建的Class对象是一个。四个变量指向的地址是一个,他们的hashcode()是一个样的。
每一个对象都有一个类加载器,可以用于加载自身,可以用于加载别人。
public class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = Cat.class.getClassLoader(); //加载别的类 Class<?> aClass = classLoader.loadClass("com.reflection.Cat"); System.out.println(aClass);//class com.reflection.Cat //加载自身 Class<?> bClass = classLoader.loadClass("com.reflection.ClassLoaderTest"); System.out.println(bClass);//class com.reflection.ClassLoaderTest } }
int.class char.calss,boolean.class
Class<Integer> integerClass = int.class; System.out.println(int.class);//int 这里边包含自动拆装箱的问题 Class<Character> characterClass = char.class; System.out.println(char.class);//char
基本类型对应的包装类可以进行 包装类.TYPE就可以了获得Class对象了。
第三章:类加载
一:动态加载和静态加载
反射机制是java实现动态语言的关键,也即是通过反射实现类的动态加载。
1、静态加载:编译时将要加载的类写死在代码中,这样程序启动时进行加载
2、动态加载:运行时到特定的位置加载特定的类,降低了依赖性。
二:类加载的时机
1.通过new的方式创建对象时 //父类,都会被加载。
2.当子类被加载时,该子类所有的父类,父类的父类,一致到Object都会被加载。//静态
3.调用类中的静态成员时 //静态
4.通过反射。//动态加载
三:类加载的流程图
类加载的概述
类的源码经过编译之后生成二进制字节码文件也就是.class文件,class文件在被加载的时候,共有三个过程,分别是加载、链接、初始化。
加载:
通过ClassLoader类加载器把类的信息加载进内存,具体就是通过类加载器将类的class文件读入内存当中,并创建一个Class对象。
链接
链接阶段就是将类的字节码二进制数据合并到jre当中,把这个东西变成一个可运行的状态。
验证:
对class文件进行安全的校验,比方说文件的格式是否正确,元数据验证是否正确,符号引用是否正确,字节码是否正确,
准备:
对静态变量分配内存和默认初始化,这个阶段是只针对静态变量的。
解析:
虚拟机把常量池中的符号引用替换成直接引用。
符号引用和直接引用?
初始化阶段:
真正的执行类中的定义java代码,静态代码块,静态变量的指定赋值等等。这里的过程是显示的指定的初始化,前两个阶段都是由jvm进行控制的,而这个阶段是由java程序员可以进行控制的。
静态资源的加载是和类的加载伴随在一起的。
加载完成后内存布局情况
方法区当中类的字节码以二进制数据的形式保存起来,在堆的里边生成一个类的Class对象,方法区的内容对堆区的内容会有一个引用和访问。他的这个引用也就体现出他的这个反射机制了。堆区的Class对象是一种特殊的数据结构,相当于是数据的访问入口。
类加载各个阶段的详细说明
加载阶段
JVM在该阶段主要的目的是将字节码从不同的数据来源(class文件(自己写的),jar包(别人写的),甚至网络)转换为二进制字节流加载到内存中,并生成一个java.lang.Class对象的一个阶段。
加载进来的时候用的介质是二进制字节流。
连接阶段-验证
目的:保证Class文件的字节流当中包含的信息符合当前虚拟机的要求。
验证内容:文件格式验证(是否以oxcafebabe开头,),元数据验证,字节码验证,符号引用验证等等。
可以考虑使用-Xverify:none参数关闭大部分的类的验证措施,缩减类的加载时间。
连接阶段-准备
JVM在该阶段对静态变量分配内存并默认初始化(数据的默认值0 0l null false)这些变量所使用的内存都将在方法区中得到分配。
准备阶段的属性三种类型的JVM不同的处理方式
连接阶段-解析
虚拟机将常量池的符号引用替换为直接引用的过程。
编译前后,两个类之间的引用还只是一个符号的引用,因为还没有加载到内存,没有替换成真正的地址,而加载之后,两个类的Class对象已经产生,两者有了具体的内存地址引用,可以将他们之间的符号引用替换为地址引用。这些都是JVM搞的这些东西。
初始化
到初始化开始真正部分执行类中定义的java程序代码,此阶段是执行clinit方法的过程,该方法由编译器按语句在源文件出现的顺序,依赖手机类中所有的静态变量赋值动作和静态代码块中的语句进行合并,JVM会保正clinit方法在多线程环境中被正确的枷锁、同步保证只有一个线程去执行clinit方法,其他线程都会阻塞,等线程完毕才会继续执行。正因为有这个机制才能保证我们这个内存中某个类只被只有一份class对象。
类加载的过程和类实例还没有关系,处理的都是静态资源之间的关系。
通过反射获取类运行时结构信息
public class Test { public static void main(String[] args) throws Exception { // Animal ai = new Animal(); Class<?> aClass1 = Animal.class.getClassLoader().loadClass("com.reflection.Cat");//class com.reflection.Cat System.out.println(aClass1); } } class Animal{}