先回顾一下要执行java程序,需要经过哪些步骤
1、2两步是需要开发人员参与的,而第3步是JVM的行为,对开发人员透明
详细看下第三点,class载入JVM过程
从内存空间视角,会分配到各个空间:
每个内存空间详情可参考:《GC及JVM参数》
从类生命周期角度,分阶段:
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
在加载阶段,虚拟机需要完成以下3件事情:
加载.class文件的方式
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中, 而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
这些内容,需要再去分析class文件详细结构,后续再学习了
类加载的最后一个阶段,除了加载阶段我们可以通过自定义类加载器参与之外,其余完全又JVM主导。到了初始化阶段,才真正开始执行程序,也就是由java转换成的class
JVM负责对类进行初始化,主要对类变量进行初始化。
在Java中对类变量进行初始值设定有两种方式:
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化
Java程序对类的使用方式可以分为两种:
/**
* 主动 被动使用问题测试
* Created by Jack on 2018/9/28.
*/
public class ClassInitTest3 {
public static void main(String[] args) {
String x = F.s;
}
}
class F {
//因为UUID.randomUUID().toString()这个方法,是运行期确认的,所以,这不是被动使用
static final String s = UUID.randomUUID().toString();
static {
//这儿会被输出
System.out.println("Initialize class F");
}
}
在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init
clinit指的是类构造器,这个构造器是jvm自动合并生成的,在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行
它合并static变量的赋值操作
在实例创建出来的时候调用,也就是构造函数,包括:
/**
* <clinit> 与 <init> 区别
*/
public class ClassInitTest2 {
static {
System.out.println("cinit");
i = 3;//可以赋值
//System.out.println(i);//但不能使用,语法错误
}
private static int i = 1;
{
System.out.println("init");//实例化构造器,
}
public static void main(String [] args) {
new ClassInitTest2();
new ClassInitTest2();
String str = "str";
System.out.println(str);
}
}
// 输出
cinit
init
init
str
static 与 static final 对初始化的区别
/**
* static 与 static final 对初始化的区别
*/
public class ClassInitFinalTest {
public static int age = 20;
static {
//如果age定义为static final,这儿就不会执行
System.out.println("静态初始化!");
}
public static void main(String args[]){
System.out.println(ClassInitFinalTest.age);
}
}
在如下几种情况下,Java虚拟机将结束生命周期
看到一段代码,很有意思
/**
* 测试类加载及初始化顺序问题
* Created by jack01.zhu on 2018/9/28.
*/
public class ClassInit {
private static ClassInit singleton = new ClassInit();
public static int counter1;
public static int counter2 = 0;
private ClassInit() {
counter1++;
counter2++;
}
public static ClassInit getSingleton() {
return singleton;
}
}
/**
* 通过输出结果,推测类加载过程
* Created by jack01.zhu on 2018/9/28.
*/
public class ClassInitTestMain {
public static void main(String []args) {
ClassInit classInitTest = ClassInit.getSingleton();
System.out.println("counter1="+classInitTest.counter1);
System.out.println("counter2="+classInitTest.counter2);
}
}
这段代码输出的结果是什么?
counter1=1
counter2=0
public static int counter1 = 0;
public static int counter2 = 0;
private static ClassInit singleton = null;
public static int counter1 ; // 由于 counter1没被赋值,所以不会被合并进去
public void clinit() {// 伪代码:<clinit>方法体内容
ClassInit singleton = new ClassInit();//(1)
int counter2 = 0;// (2)
}
以上,就是一个类的生命周期,这篇重点就是加载部分,如上面所说,加载阶段相对别的阶段,对开发人员而言有更强的可控性;下面学习一下类加载器相关知识
在JVM中,如何确定一个类型实例:
同一个Class = 相同的 ClassName + PackageName + ClassLoader
在JVM中,类型被定义在一个叫SystemDictionary 的数据结构中,该数据结构接受类加载器和全类名作为参数,返回类型实例。
SystemDictionary 如图所示:
双亲委托的工作过程:如果一个类加载器收到了一个类加载请求,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类)时,子加载器才会尝试着自己去加载
javac –verbose查看运行类是加载了jar文件
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 首先检查,jvm中是否已经加载了对应名称的类,findLoadedClass(String )方法实际上是findLoadedClass0方法的wrapped方法,做了检查类名的工
//作,而findLoadedClass0则是一个native方法,通过底层来查看jvm中的对象。
Class c = findLoadedClass(name);
if (c == null) {//类还未加载
try {
if (parent != null) {
//在类还未加载的情况下,我们首先应该将加载工作交由父classloader来处理。
c = parent.loadClass(name, false);
} else {
//返回一个由bootstrap class loader加载的类,如果不存在就返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);//这里是我们的入手点,也就是指定我们自己的类加载实现
}
}
if (resolve) {
resolveClass(c);//用来做类链接操作
}
return c;
}
从上面的方法也看出我们在实现自己的加载器的时候,不要覆盖locaClass方法,而是重写findClass(),这样能保证双亲委派模型,同时也实现了自己的方法
既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader
很简单的两个类,方法中打印出各自的类加载器
public class LoaderClass {
public void loader(){
System.out.println("LoaderClass:"+this.getClass().getClassLoader());
LoaderClass1 class1 = new LoaderClass1();
class1.loader();
}
}
public class LoaderClass1 {
public void loader() {
System.out.println(this.getClass().getName() + " loader:"+this.getClass().getClassLoader());
}
}
自定义加载器
public class MyClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
String root = "d:/";
byte[] bytes = null;
try {
//路径改到根目录下
String file = root + name.substring(name.lastIndexOf(".")+1) + ".class";
InputStream ins = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
bytes = baos.toByteArray();
ins.close();
} catch (Exception e) {
e.printStackTrace();
}
return defineClass(name, bytes, 0, bytes.length);
}
}
测试类
public class ClassLoaderTest {
public static void main(String[]args) throws Exception {
ClassLoaderTest test = new ClassLoaderTest();
System.out.println(test.getClass().getClassLoader());//输出sun.misc.Launcher$AppClassLoader
System.out.println(test.getClass().getClassLoader().getParent());//输出sun.misc.Launcher$ExtClassLoader
System.out.println(test.getClass().getClassLoader().getParent().getParent());//输出null
//=====测试重复加载,类路径中LoaderClass.class存在=================
//======虽然指定了classloader,但依然输出的是LoaderClass:sun.misc.Launcher$AppClassLoader
//==删除类路径下的LoaderClass.class,才会输出LoaderClass:com.jack.classloader.MyClassLoader
//并且loaderclass中创建的对象类加载器也是MyClassLoader
MyClassLoader classLoader = new MyClassLoader();
Class<?> loadClass = Class.forName("com.jack.classloader.LoaderClass", true, classLoader);
Method startMethod = loadClass.getMethod("loader");
startMethod.invoke(loadClass.newInstance());
//===当类加载器不一样时,两个class不相等
MyClassLoader classLoader1 = new MyClassLoader();
Class<?> loadClass1 = Class.forName("com.jack.classloader.LoaderClass", true, classLoader1);
System.out.println(loadClass.equals(loadClass1));//输出false
}
}