一 类的加载、连接、和初始化
1 JVM和类
当调用java命令运行某个程序时,就会自动开启一个进程。无论程序多么复杂,该程序启动了多少线程,他们都在这个JVM的进程里,只有当出现下面四种情况,进程才会停止:
- 程序运行到最后,正常结束
- 遇到了System.exit()或Runtime.getRuntime().exit()代码结束程序
- 遇到了未捕获的异常或错误的结束程序
- 当JVM所在平台强制结束程序
2 类的加载
当程序主动使用某个类时,若该类还未被加载到内存中,则JVM或通过加载、连接、初始化三个阶段进行该类的初始化。类的加载就是将该类读入内存,并创建一个java.lang.Class的对象。类的来源主要有以下四种
- 本地系统中的
- jar包中的
- 网络上的
- 把一个java源文件动态编译,并执行加载
3 类的连接
当类被加载之后,会产生一个对应的Class对象,接着将会进入连接阶段,链接阶段主要是将类的二进制数据合并到JRE中。连接阶段由可分为验证、准备、解析三个阶段
- 验证:检查被加载的是否有正确的结构
- 准备:为该类的类变量分配内存,并设置默认初始值
- 解析:将类的二进制数据中的符号引用替换成直接引用
4 类的初始化
对类变量初始化主要有两种方式:1)声明变量时初始化,2)静态语句块中初始化
5 类的初始化时机
java程序首次通过下面6种方式来使用某个类或接口时,就会初始化该类或接口
- 创建某个类的对象时,创建对象有三种方式1)通过new关键字2)通过反射来创建对象3)通过反序列化创建对象
- 调用某个类的静态方法
- 访问某个类的静态变量时,或为某个静态变量赋值时
- 通过反射来创建某个类的class文件对象时(forName方法)
- 初始化某个类的子类时
- 直接食用java.exe命令来运行某个主类时(在终端使用输入java命令来运行一个类)
值得注意的是:一个被final修饰的类变量,如果能在编译阶段就确定了值,这个静态变量就会变成宏变量,也就是说编译器会将程序所有使用该变量的地方都换成该变量的值,所以即使访问了这个静态变量也不会初始化该类
6 类加载器ClassLoader
类加载器是将类的class文件(本地磁盘或者网络上的)加载到内存中并生成Class对象。当JVM启动时会由三种类加载器组成初始化类加载器的层次结构。
- Bootstrap ClassLoader:根类加载器,负责加载java核心类($JAVA_HOME中jre/lib/rt.jar里所有的class),由C++实现不属于ClassLoader
- Extension ClassLoader:扩展类加载器,负责加载JRE扩展目录中的JAR包的类($JAVA_HOME中jre/lib/ext.jar里所有的class)
- Application ClassLoader:系统类加载器,加载classpath中指定的jar包及目录中class
7 类加载机制
双亲委派机制工作原理
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
8 获取class对象的三种方法
- 通过Class类的forName(String clazzName)该方法是通过传入一个类的全限定类名(完整包名+类名)来获取class对象
- 通过某个类的class属性来获取该类的class对象,例如:Person.class
- 调用某个对象的getClass()方法,例如new Person().getClass
第一种方法和第二种方法都是通过类来得到该类的class对象,相比之下第二种方式比第一种方式有以下两种好处
- 更安全,第二种方法在编译时期就可以检查该类的对象是否存在
- 效率更高,因为不需要调用方法所以效率高
但是如果想要通过类的名字创建该类的class对象,只能通过第一种方法,系统会根据名字来查找是否存在这个类的class文件如果不存在则会抛出ClassNotFoundException
9 获取Class的信息
可以通过getConstructor()、getDeclaredConstructor()、getMethod()、getDeclaredMethod()等方法获取类的构造方法、普通方法、成员变量、内部类等信息,详情参看API文档