1、概述:
如何将类加载虚拟机中
加载、验证、准备、解析、初始化、使用和卸载七个阶段
虚拟机规范则是严格规定只有四种情况会立即对类进行初始化
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,使用new关键实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
2)使用java.lang.reflect包的方法对类进行发射调用的时候
3)当初始化一个类的时候,如果发现其父类还没有进行初始化、则需要先触发其父类的初始化
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机先初始化这个主类
以上为主动引用,除此之外所有引用类的方式,都不会触发初始化,称为被动引用
例子1:通过子类引用父类的静态字段、不会导致子类初始化
package learn;
/**
* 被动使用类字段演示一:
* 通过子类引用父类的静态字段、不会导致子类初始化
* @author Administrator
*
*/
public class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
package learn;public class SubClass extends SuperClass {static {System.out.println("SubClass init");}}
package learn;public class NotInitialization {public static void main(String[] args){System.out.println(SubClass.value);}}
输出结果:
SuperClass init
123
例子2:通过数组定义来引用类,不会触发此类的初始化
@Test
public void noInitialization(){
SuperClass[] sca = new SuperClass[10];
}
什么也没有输出
例子3:访问用final修饰字段不会初始化该类
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发常量的类的初始化
package learn;
public class ConstClass {
static{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
@Test输出:
public void noInitialization(){
System.out.println(ConstClass.HELLOWORLD);
}
hello world
2、类加载的过程
2.1 加载
加载阶段是类加载过程的一个阶段。
完成三件事
1)通过一个类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在java堆中生成一个代表和这个类的java.lang.Class对象,作为方法区的这些数据的访问入口
2.2、验证
2.2.1、文件格式验证
验证字节流是否符合Class文件格式的规范
2.2.2、元数据验证
对字节码描述的信息进行语义分析
2.2.3 、字节码验证
主要安全方面的校验
2.2.4. 符合引用验证
将符合引用转换为直接引用的时候
3、准备
准备阶段是正式为类变量分配内存并设置类变量初始化的阶段。
仅包含类变量(static修饰的变量),不包含实例变量。
public static int value = 123;
value在准备阶段过后的初始值为0而不是123,最后通过putstatic指令将123赋值给value,存在于<clinit>()方法中,
但是如果:public final static int value = 123; 这是就会赋值为123,它会放入常量池中
数据类型 | 零值 |
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
4、解析
虚拟机将常量池内符合引用替换为直接引用的过程,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info出现
符合引用:符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量,只要使用能无歧义定义到目标即可,符号引用与虚拟机实现的内存无关,引用的目标并不一定已经加载到内存中
直接引用:直接引用可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果是直接引用,那么引用的目标必定已经存在内存中。
5、初始化
初始化阶段执行类构造器<clinit>()方法过程
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中语句合并产生的
<clinit>()方法与类的构造函数不同,他不需要显示调用父类构造器,所以第一个被执行<clinit>()方法的类一定是java.lang.Object
<clinit>()方法先执行,意味着父类的定义的静态语句块有优于子类的变量赋值操作。
<clinit>()方法对于类或接口不是必须,判断是否存在静态语句块
虚拟机会保证一个类的<clinit>()方法的在多线程环境下被正确地加锁和同步
有可能会导致阻塞
package learn;输入内容: 一直阻塞
class DeadLoopClass {
static {
//如果不加上和这个if语句,编译器将提示“Initializer does not completed normally”
if(true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while(true){
}
}
}
public static void main(String[] args){
Runnable script = new Runnable(){
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.print(Thread.currentThread()+ " run Over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
Thread[main,5,main]init DeadLoopClass
6、类加载器
6.1 类与类加载器
类和类加载器一一对相应,否则加载相同对象,不同类加载器,这个两个对象是不同等。
package learn;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException{
ClassLoader myLoader = new ClassLoader(){
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try{
String fileName = name.substring(name.lastIndexOf(".")+1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
Object obj = myLoader.loadClass("learn.ConstClass").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof learn.ConstClass);
}
}
输入日志:
ConstClass init!
class learn.ConstClass
false
6.2 双亲委派模型
从虚拟机角度分为两种不同的类加载器:一种启动类加载器(Boostrap ClassLoader) 另一种就是其他所有外部类加载器。全部继承java.lang.ClassLoader
从java开发人员的角度有三种;
启动类加载器:加载<JAVA_HOME>\lib目录中的或者被-Xbootclasspath参数指定路径中,按文件名识别
扩展类加载器: sun.misc.Launcher.java.ext.dirs 负责<JAVA_HOME>\lib\ext目录中或者被java.ext.dirs
应用程序类加载器:sun.misc.Launcher$AppClassLoader来实现,负责加载第三方库
双亲委派模型,就是将任务往上提,如果上面可以加载就让它加载,否则自己加载。
6.3 破坏双亲委派模型
为了热部署 OSGI,不重启加载新的类