一:类加载器深入剖析
1.java虚拟机与程序的生命周期
2.在如下几种情况下,java虚拟机将结束生命周期
-- 执行了System,exit()方法
--程序正常执行结束
--程序在执行过程中遇到了异常或者错误而异常终止
--由于操作系统出现错误而导致java虚拟机进程终止
二:类的加载,连接与初始化
--加载:查找并加载类的二进制数据
--连接:
--验证:确保被加载的类的正确性
--准备:为类的静态变量分配内存,并将其初始化为默认值
--解析:把类中的符号引用转换为直接引用
--初始化:为类的静态变量赋予正确的初始化值
public class Test
{
//先是a=0
private static int a = 3;
}
--java程序对类的使用方式可以分为两种
--主动使用(六种)
-创建类的实例
-访问某个类或者接口的静态变量,或者对该静态变量赋值
-调用类的静态方法
-反射(如Class.forName("com.dfhgjkdh.Test"));
-初始化一个类的子类
-java虚拟机启动时被标明为启动类的类
--被动使用
除了以上六种情况,其他使用java类的方式都被看作是对类的被动使用,都不会导致类的初始化
--所有的java虚拟机实现必须在每一个类或者接口被java程序“首次主动使用”时才初始化他们
三:类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区中的方法区,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
说明:
类的加载的最终产品是位于堆区中的Class对象
Class对象封装了类在方法区内的数据结构,并且向java程序员提供了访问方法区的数据结构的接口
类加载器的类型:
有两种类型的类加载器
--java虚拟机自带的加载器
--根类加载器(Bootstrap):c++编写,无法再java代码直接使用
--扩展类记载器(Extension):java代码编写
--系统类加载器(System):使用java代码编写
--用户自定义的类加载器
--java.lang.ClassLoader的子类
--用户可以定制类的加载方式
public class JvmTest {
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("java.lang.String");
System.out.println(clazz.getClassLoader());//null 说明String这个类是由根类加载器加载的
}
}
package com;
public class Test {
public static void main(String[] args)throws Exception {
Class clazz = Class.forName("com.C");
//sun.misc.Launcher$AppClassLoader@18b4aac2 //系统类加载器加载
System.out.println(clazz.getClassLoader());
}
}
class C{}
类加载器并不需要等到某个类被“首次主动使用”时候再加载它
jvm规范允许类加载器在预料某个类将要被使用的时候就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时候才报告错误(LinkageError错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
四:类的验证
1.类被加载后,就进入了连接阶段。连接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行时环境中去。
2.类的验证的内容
--类文件的结构检查
确保类文件遵从java类文件的固定格式
--语义检查
确保类本身符合java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖
--字节码验证
确保字节码流可以被java虚拟机安全的执行,字节码流代表java方法(包括静态方法和实例方法),它是由被称作操作码的 单字节指令组成的序列,每一个操作码后都跟着一个或多个的操作数,字节码验证步骤会检查每一个操作码是否合法,即是 否有者合法的操作数。
--二进制兼容性的验证
确保相互引用的类之间协调一致,例如在Worker类的gotoWork()方法中会调用Car类的run()方法,java虚拟机在验证Worker类的时候,会检查在方法区内是否存在Car类的run()方法,假如不存在(当Worker类和Car类的版本不兼容,就会出现这种情况)就会抛出NoSuchMethodError错误。
五:类的准备阶段(为静态变量赋予默认值)
六:类的解析
在解析阶段,java虚拟机会把类的二进制数据中的符号引用替换为直接引用。例如在Worker类的gotoWork()方法中会引用Car()类的run()方法
public void gotoWork(){
car.run();//这段代码在Worker类的二进制数据中表示为符号引用
}
在Worker类的二进制数据中,包含了一个对 Car类的run()方法的符号引用,它由run()方法的全名和相关描述组成。在解析阶段,java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区中的内存位置,这个指针就是直接引用。
七:类的初始化:在初始化阶段,java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:一是在静态标量的声明处进行初始化,二是在静态代码块的进行初始化。
七:例子(这两道题理解要从上面的知识去理解原理)
public class JvmTest {
public static void main(String[] args) throws Exception{
Singleton singleton = Singleton.getSingleton();
System.out.println("counter1 = " + singleton.counter1);//1
System.out.println("counter2 = " + singleton.counter2);//0
}
}
class Singleton{
/**
* 类在准备阶段singleton=null,counter1=0,counter2=0,
* 在new Singleton()的时候执行构造方法,这个时候counter1=1,counter2=1,
* 在Singleton类的初始化阶段,counter2的值被赋予成0,所以此时counter1=1,counter2=0
*/
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2=0;
private Singleton(){
counter1++;
counter2++;
}
public static Singleton getSingleton(){
return singleton;
}
}
public class JvmTest {
public static void main(String[] args) throws Exception{
Singleton singleton = Singleton.getSingleton();
System.out.println("counter1 = " + singleton.counter1);//1
System.out.println("counter2 = " + singleton.counter2);//1
}
}
class Singleton{
/**
* 类在准备阶段singleton=null,counter1=0,counter2=0,
* 在Singleton类初始化的时候counter1=0,counter2=0
* 在new Singleton()的时候执行构造方法,这个时候counter1=1,counter2=1
*/
public static int counter1;
public static int counter2=0;
private static Singleton singleton = new Singleton();
private Singleton(){
counter1++;
counter2++;
}
public static Singleton getSingleton(){
return singleton;
}
}
八:类的初始化步骤
1.假如这个类还没有被加载和连接,那就先进行加载和连接
2.假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类
3.假如类中存在初始化语句,那就依次执行这些初始化语句
九:例子(类的初始化)
package com;
public class FinalTest {
public static final int x = 6 / 3;
static {
System.out.println("FinalTest static block");
}
}
class Test2
{
public static void main(String[] args) {
/**
* 输出的结果是2,表明了什么?
* 答:表明了FinalTest不会被初始化,这是因为加了final并且(public static final int x = 6 / 3;)是编译时候的常量,在
* 编译的时候就能确定值,无需进行类的初始化就确定值,所以FinalTest不会被初始化
*/
System.out.println(FinalTest.x);
}
}
package com;
import java.util.Random;
public class FinalTest2 {
public static final int x = new Random().nextInt(100);
static {
System.out.println("FinalTest2 static block");
}
}
class Test3
{
public static void main(String[] args) {
/**
* 输出的结果是FinalTest2 static block 16(一个随机数),为啥呢?
* 答:(public static final int x = new Random().nextInt(100);)不是编译时候在编译的时候不能确定值
* 只有在程序运行的时候才能确定值,所以就要初始化FinalTest2,在初始化阶段就会先输出
* "FinalTest2 static block"
*/
System.out.println(FinalTest2.x);
}
}
十:接口的初始化时机
1.当java虚拟机初始化一个类的时候,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
2.在初始化一个类的时候,并不会先初始化它所实现的接口。
3.在初始化一个接口的时候,并不会先初始化它的父接口
4.因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量的时候,才会导致该接口的初始化
十一:例子
package com;
class Parent{
static int a = 3 ;
static {
System.out.println("Parent static block");
}
}
class Child extends Parent{
static int b = 4;
static {
System.out.println("child static block");
}
}
public class Test4 {
static {
System.out.println("Test4 static block");
}
public static void main(String[] args) {
/**
* 输出的结果并不是"Test4 static block" 4,而是下面的结果,说明(static int b = 4;)在编译的时候不能确定值,在初始化才能确定值
* Test4 static block
* Parent static block
* child static block
* 4
*/
System.out.println(Child.b);
}
}
package com;
class Parent{
static int a = 3 ;
static {
System.out.println("Parent static block");
}
}
class Child extends Parent{
static int b = 4;
static {
System.out.println("child static block");
}
}
public class Test4 {
static {
System.out.println("Test4 static block");
}
public static void main(String[] args) {
/**
* 输出的结果是:
* Test4 static block
* Parent static block
* 3
* 说明子类并不会初始化,对父类的初始化并不会使得子类初始化
*/
System.out.println(Child.a);
}
}
package com;
class Parent2{
static int a = 3;
static {
System.out.println("Parent2 static block");
}
}
class Child2 extends Parent2{
static int b = 4;
static {
System.out.println("Child2 static block");
}
}
public class Test5 {
static {
System.out.println("Test5 static block");
}
public static void main(String[] args) {
/* Parent2 parent;
System.out.println("-------------------------------");
System.out.println(Child2.b);
输出结果是:(说明子类的初始化使得父类进行了初始化)
Test5 static block
-------------------------------
Parent2 static block
Child2 static block
4
*/
Parent2 parent;
System.out.println("-------------------------------");
System.out.println(Parent2.a);
System.out.println(Child2.b);//上一行代码对父类进行了初始化,本行就不会再次对父类进行初始化
}
}
package com;
class Parent3{
static int a = 3;
static {
System.out.println("Parent3 static block");
}
static void doSomething(){
System.out.println("do something");
}
}
class Child3 extends Parent3{
static {
System.out.println("child3 static block");
}
}
public class Test6 {
public static void main(String[] args) {
/**
* Parent3 static block
*3
*do something
* 结论:
* 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义的时候,
* 才可以认为是对类或者接口的主动使用
*/
System.out.println(Child3.a);
Child3.doSomething();
}
}
十二:类的初始化时机
1.调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化,例如:
package com;
class CC{
static {
System.out.println("CC static block");
}
}
public class Test7 {
public static void main(String[] args)throws Exception {
/**
* -----------------
* CC static block
*/
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = classLoader.loadClass("com.CC");
System.out.println("-----------------");
clazz = Class.forName("com.CC");
}
}