1. 程序运行阶段
- 代码阶段(硬盘阶段):比如编写一个Person.java,编译成字节码Person.class,这都是存在硬盘的,而程序运行是要在内存中的
- 类加载器ClassLoader阶段:在第一阶段和第二阶段中间,也就是本文要讲的内容,先卖个关子,知道有这个阶段
- Runtime运行时阶段:也就是new Person(),那么此时已经在内存中
2. “第二阶段”:Class类对象阶段
通过第一第三阶段,可以推测出第二阶段其实是把硬盘上的class文件加载进内存变成java可以识别的内容(术语叫做Class类对象)的过程,随后第三阶段便可读取。那么究竟是如何做的呢?
在这个阶段会把Person.class(泛指所有.class文件)文件分解成三部分加载进内存(当然不是加载成Person这个类,程序没有new,java是不知道你要什么类的,所以他是加载成上面说的统一的“Class类对象”),这个对象包括了三个东西:
- Filed[] fields:里面存放.class分解出来的成员变量
- Constructor[] cons:存放构造方法
- Method[] methods:存放成员方法
这个从.class文件分解为各个部分并封装为Class类对象的过程其实就叫反射
这不是很抽象的东西,来个具体应用举例:idea对着一个String对象敲下.,会提示chatAt等一堆可以用的方法,这方法从哪来?idea内部判断了String然后提示?怎么可能,其实这就是运用了反射,读取了Method[]列表,显示出来。可以把idea看作是一个运行中的java程序,因此反射可以在运行的时候获取类的信息
3. api
既然知道了反射就是为了获取”Class类对象“(再次重申,不是Person类对象,他就叫Class类对象!!),那么我们可以看如何获取了,刚刚说了三个阶段,那么每个阶段都有对应的api用于获取Class类对象。
Class.forName(“全类名”):第一阶段,将字节码文件加载进内存,返回Class类对象(全限定类名:包名+类名,这个阶段没有类名,因此必须手动写全限定类名,)
类名.class:第二阶段,通过类名的class属性获取(这一阶段jvm已经加载好了,知道了类名,因此需要的话只要直接用类名.属性名就可以了)
对象名.getClass():第三阶段,这时候你已经new Person()了,已经有对象了,getClass()是Object类提供的方法,因此你的Person类必有这个方法
1 | //1.自己写一个Person类,随便给个name和age属性,略 |
同一个字节码文件只会被加载一次Class类对象,三种方式获得的地址都是同一个
4. 双亲委派机制
如果自己建立一个名为java.lang.String的包和类,能不能替换掉真正的String类呢?答案是不行的。
ClassLoader有三种
BootstrapClassLoader ==> $JAVA_HOME/jre/lib
ExtClassLoader ==> $JAVA_HOME/jre/lib/ext
AppClassLoader ==> 第三方和自己项目下的类
所谓双亲委派机制,就是当程序读取String的时候,先从BootstrapClassLoader开始,层层往下读取,如果能够加载String,那么交由该类加载器加载,如果到AppClassLoader都没有发现该类,会报ClassNotFoundException
String在$JAVA_HOME/jre/lib/rt.jar下,所以被BootstrapClassLoader加载。
需要查看是什么类加载器,Class类提供了getClassLoader()方法,如下
1 | Person p = new Person(); |
如果要看上一级,就通过类加载器的getParent()
1 | System.out.println(p.getClass().getClassLoader().getParent()); |
要注意的是,由BootstrapClassLoader加载的类getClassLoader()会为null,不是没有该类(没有会报错),而是显示不出来,已经去了c写的native里面