final 修饰的非静态全局变量只能通过直接赋值、构造函数中赋值、代码块{}中赋值三种方式赋值,且必须要赋值。
final 修饰的静态全局常亮不能通过构造函数赋值,因为静态常亮优先于构造函数初始化,只能通过直接赋值、静态代码块中赋值。
其中,后三者是在调构造器时执行的。示例代码如下:
public class OrderInLoadTestDemo {
/* 静态变量 */
public static String staticField = "静态变量";
/* 静态初始化块 */
static {
System.out.println(staticField);
System.out.println("静态初始化块");
}
/* 变量 */
public String field = "变量";
/* 初始化块 */
{
System.out.println(field);
System.out.println("初始化块");
}
/* 构造器 */
public OrderInLoadTestDemo() {
System.out.println("构造器");
}
public static void main(String[] args) {
new OrderInLoadTestDemo();
}
}
父类--静态变量
父类--静态初始化块
子类--静态变量
子类--静态初始化块
子类main方法
父类--变量
父类--初始化块
父类--构造器
子类--变量
子类--初始化块
子类--构造器
分析:
- 访问SubClass.main(),(这是一个static方法),于是装载器就会为你寻找已经编译的SubClass类的代码(也就是SubClass.class文件)。在装载的过程中,装载器注意到它有一个基类(也就是extends所要表示的意思),于是它再装载基类。
- 不管你创不创建基类对象,这个过程总会发生。如果基类还有基类,那么第二个基类也会被装载,依此类推。
- 执行根基类的static初始化,然后是下一个派生类的static初始化,依此类推。这个顺序非常重要,因为派生类的“static初始化”有可能要依赖基类成员的正确初始化。
- 当所有必要的类都已经装载结束,开始执行main()方法体,并用new SubClass() 创建对象。
- 类SubClass存在父类,则调用父类的构造函数,你可以使用super来指定调用哪个构造函数。基类的构造过程以及构造顺序,同派生类的相同。首先基类中各个变量按照字面顺序进行初始化,然后执行基类的构造函数的其余部分。
- 对子类成员数据按照它们声明的顺序初始化,执行子类构造函数的其余部分。
另外:
-
通过子类引用父类的静态字段,不会导致子类初始化;
-
通过数组定义来引用类,不会触发此类的初始化;
-
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;
第3种情况代码演示:
public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world"; } public class Test { public static void main(String[] args) { // 只会打印:hello world // 不会打印:ConstClass init! System.out.println(ConstClass.HELLOWORLD); } }
类从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用和卸载。
a.通过一个类的全限定名来获取定义此类的二进制字节流。 从ZIP包中读取 从网络中获取 运行时计算生成(Java动态代理技术) 由其他文件生成(由JSP文件生成对应的Class类) 从数据库中读取 ...... b.将二进制字节流存储在方法区中(按照虚拟机所需的格式存储)。 c.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。Class对象比较特殊,存放在方法中(HotSpot虚拟机)。
加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
a.格式语义校验: 是否以0xCAFEBASE开头 主、次版本号是否在当前虚拟机处理范围内 元数据验证 对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。 字节码验证 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 符号引用验证 确保解析动作能正确执行。 b.代码逻辑校验
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备阶段正式为静态变量分配内存并设置初始值,这些静态变量在方法区中分配内存。
准备阶段,JVM只会为静态变量(static修饰)分配内存,不包括实例变量,实例变量将会在对象实例化时随对象一起分配在Java堆中。
准备阶段,只会为value分配内存,不会为name分配内存
public static int value = 123; private String name = "Tom";
设置初始值:
通常情况(无final):零值
准备阶段,未执行任何Java方法,而value赋值为123指令是程序编译后,存放于类构造器方法中,在初始化阶段才会执行,因此准备阶段,会设置零值。
public static int value = 123;
特殊情况(有final):实际值
public static final int value = 123;
解析阶段是虚拟机将常量池内的符号引用替换为直接在其内存中的直接引用的过程。
JVM主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行解析。
在介绍初始化时,要先介绍两个方法: 和 : 在编译生成class文件时,会自动产生两个方法,一个是类的初始化方法, 另一个是实例的初始化方法 : 在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行 : 在实例创建出来的时候调用,包括调用 new 操作符; 调用 Class 或Java.lang.reflect.Constructor 对象的 newInstance()方法; 调用任何现有对象的 clone()方法; 通过 java.io.ObjectInputStream 类的 getObject() 方法反序列化。
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备极端,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主管计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器()方法的过程.
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块 static{} 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
static {
i = 0;
sout(i);// Illegal forward reference (不合法的向前引用)
}
static int i = 1;
... main (...) {
sout(i); // 结果为1
}
()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法。
虚拟机会保证一个类的()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。
如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
到了初始化阶段,才真正开始执行类中定义的Java程序代码。
JVM规定有且仅有5种情况必须对类进行初始化:
遇到new、 getstatic、 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景为: 使用new关键字实例化对象 读取或设置一个类的静态字段(final修饰的除外) 调用一个类的静态方法 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
当初始化一个类时,需要先初始化父类。
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码。
当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存。
Classloader:
static class AppClassLoader extends URLClassLoader (Launcher 内部静态类)
static class ExtClassLoader extends URLClassLoader (Launcher 内部静态类)
public class URLClassLoader extends SecureClassLoader
public class SecureClassLoader extends ClassLoader
public abstract class ClassLoader
Java 的类加载,就是把字节码格式“.class”文件加载到 JVM 的方法区,并在 JVM 的堆区建立一个java.lang.Class
对象的实例,用来封装 Java 类相关的数据和方法。那 Class 对象又是什么呢?你可以把它理解成业务类的模板,JVM 根据这个模板来创建具体业务类对象实例。
JVM 并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。JVM 类加载是由类加载器来完成的,JDK 提供一个抽象类 ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。
public abstract class ClassLoader {
// 每个类加载器都有个父加载器
private final ClassLoader parent;
public Class<?> loadClass(String name) {
// 查找一下这个类是不是已经加载过了
Class<?> c = findLoadedClass(name);
// 如果没有加载过
if( c == null ){
// 先委托给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找 Bootstrap 加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的 findClass 去加载
if (c == null) {
c = findClass(name);
}
return c;
}
protected Class<?> findClass(String name){
//1. 根据传入的类名 name,到在特定目录下去寻找类文件,把.class 文件读入内存
...
//2. 调用 defineClass 将字节数组转成 Class 对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成一个 Class 对象,用 native 方法实现
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
从上面的代码我们可以得到几个关键信息:
- JVM 的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个 parent 字段,指向父加载器。
- defineClass 是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 通过 JNI 机制调用。
- findClass 方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用 defineClass 方法得到 Class 对象。
- loadClass 是个 public 方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java 类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索 Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。
JDK 中有哪些默认的类加载器?它们的本质区别是什么?为什么需要双亲委托机制?JDK 中有 3 个类加载器,另外你也可以自定义类加载器。BootstrapClassLoader、ExtClassLoader、AppClassLoader。
- BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如rt.jar、resources.jar等。
- ExtClassLoader 是扩展类加载器,用来加载\jre\lib\ext目录下 JAR 包。
- AppClassLoader 是系统类加载器,用来加载 classpath 下的类,应用程序默认用它来加载类。
- 自定义类加载器,用来加载自定义路径下的类。
这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。双亲委托机制是为了保证一个 Java 类在 JVM 中是唯一的,假如你不小心写了一个与 JRE 核心类同名的类,比如 Object 类,双亲委托机制能保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。
这里请你注意,类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。同样的道理,如果你要自定义类加载器,不去继承 AppClassLoader,而是继承 ClassLoader 抽象类,再重写 findClass 和 loadClass 方法即可,Tomcat 就是通过自定义类加载器来实现自己的类加载逻辑。不知道你发现没有,如果你要打破双亲委托机制,就需要重写 loadClass 方法,因为 loadClass 的默认实现就是双亲委托机制。
在实际的应用中双亲委派解决了 java 基础类统一加载的问题,但是却着实存在着一定的缺席。 jdk 中的基础类作为用户典型的 api 被调用,但是也存在被 api 调用用户的代码的情况,典型的如 SPI 代码。
SPI 的全名为 Service Provider Interface,主要是应用于厂商自定义组件或插件中。
java SPI机制的思想: 我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml 解析模块、jdbc 模块等方案。 面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以 jdk 开发人员就引入了线程上下文类加载器(Thread Context ClassLoader),这类类加载器可以通过 java.lang.Thread 类的 setContextClassLoader 方法进行设置,也就是拿到上下文加载器(用户自定义加载器)来加载指定的类。
如:java.sql.DriverManager 中有静态代码块 static {loadInitialDrivers(); ...}
private static void loadInitialDrivers() {
String drivers;
try {
// 先读取系统属性
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 通过SPI加载驱动类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
// 继续加载系统属性中的驱动类
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
注意driversIterator.next() 最终就是调用 Class.forName(DriverName, false, loader) 方法。
对于自己加载不了的类怎么办,直接用线程上下类加载器加载,通过 ClassLoader cl = Thread.currentThread().getContextClassLoader();
这条语句获取本地线程然后实现上下类加载。所以这个地方 Bootstrap Classloader 加载器拿到了 Application ClassLoader 加载器应该加载的类,就打破了双亲委派模型。
热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大。
另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。
热部署步骤:
- 销毁自定义classloader(被该加载器加载的class也会自动卸载);
- 更新class;
- 使用新的ClassLoader去加载class;
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
- 加载该类的ClassLoader已经被GC;
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;
自定义类加载器:
要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流。如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。