之前谈到过字节码文件,可以说字节码文件是虚拟机运行程序的基础。但是有了字节码文件就可以开始运行我们的Java程序吗?其实不然,对于从开发人员编写好Java程序到在虚拟机中正在开始运行的过程来说,编译或编写生成字节码文件只是万里长征的第一步,接下来我们将谈谈字节码文件在虚拟机运行程序时是怎样的存在。
本文的主要内容是虚拟机的类加载机制,双亲委派模型以及如何打破双亲委派模型。
首先我们来看看下面这张图,这张图表示的是类加载的整个生命周期。
类被加载到虚拟机内存开始直到卸载出内存为止,整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载。我们将验证,准备,解析三过程合在一起称之为连接,而其中加载,验证,准备,初始化,卸载的顺序是固定的,也就是说对于解析,执行的步骤也许不是按照上述的顺序执行的,因为在某些情况下有可能解析是在初始化之后进行的。
那么对于何时开始执行初始化过程而言,出现以下情况时,就需要立即开始执行:
下面我们来依次看看各阶段都完成了什么事情。
在类加载阶段,虚拟机会完成以下三件事情:
类的加载要分为数组类的加载和非数组类的加载:
关于数组类的加载,不通过类加载器创建,由虚拟机直接创建。
关于非数组类的加载,开发人员可以使用自定义类加载器,只需要继承java.lang.ClassLoader
,并根据需要重写其中的findCLass方法或者loadClass方法即可。关于两种方法有如下区别:
关于自定义类加载器可以参考以下代码:
public class MyClass {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public MyClass(int id, String name) {
this.id = id;
this.name = name;
System.out.println("==============MyClass init with id:" + this.id + " and name:" + this.name+"=============");
}
public MyClass() {
System.out.println("=============MyClass init=============");
}
public void sayHelloString(){
System.out.println("Hello MyClass.");
}
public void sayHiString(){
System.out.println("Hi MyClass.");
}
}
public class MyClassLoader extends ClassLoader {
// classpath是编译后class文件的目录,我这里使用的maven进行项目管理,所以是在项目里的target目录下,如果将这三个文件单独拿出来通过命令行编译测试,就需要改为相应class文件所在的目录。
private String classPath = "D:\\项目\\IntelliJIDEA\\Test\\target\\classes\\";
/**
* 重写loadClass方法,需要根据传入的全限定名找到对应的字节码文件,然后将字节码文件读入到字节数组中存储,最后根据父类的defineClass找到对应的类实例对象。
*
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
public Class<?> findClass(String name) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] classBytes = null;
// 1.根据全限定名找到字节码文件
try {
FileInputStream in = new FileInputStream(classPath + name.replace(".","\\")+".class");
int len = 0;
// 2.将字节码文件读入到字节数组中存储
while (-1 != (len = in.read())) {
out.write(len);
}
classBytes = out.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 3.根据父类的defineClass找到对应的类实例对象
return defineClass(name, classBytes, 0, classBytes.length);
}
}
public class Demo2 {
public static void main(String[] args) {
MyClassLoader loader=new MyClassLoader();
// findClass方法的参数是类的全限定名,这里带有我项目里的包路径,如果单独测试,则修改为对应的全路径即可
Class<?> myClass=loader.findClass("com.earl.jvm.classloader.MyClass");
Method[] methods=myClass.getMethods();
try {
Object object=myClass.newInstance();
for(Method method:methods){
if(method.getName().contains("say")){
System.out.println(method.getName());
method.invoke(object);
System.out.println("------------");
}
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
运行main方法可以看到输出了“Hello MyClass.”和“Hi MyClass.”,说明加载MyClass是通过自定义类加载器进行加载的。
验证是连接阶段的第一步,目的是确保字节码文件的字节流中包含的信息符合当前虚拟机要求,并且不会危害虚拟机自身安全。如果不做验证工作的话,对输入的字节流完全信任,那么就有可能因为恶意代码而导致载入有害的字节流从而使系统崩溃。
验证阶段主要验证以下几点:
文件格式验证:主要验证字节码文件是否符合规范,即字节码文件格式中所规定的内容。当文件格式验证通过后,字节流才会进入内存中的方法区进行存储,后续的验证都是基于方法区的存储结构进行的,不再直接操作字节流。
元数据验证:主要对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范,不会在后续的验证中出现不符合Java语法规范的元数据信息。
字节码验证:主要对类的方法体进行校验分析,确保被校验的类的方法体在运行时不会做出危害虚拟机安全的事件。
如果一个类方法体的字节码没有通过字节码验证,那么肯定字节码文件是有问题的,但是如果一个方法体通过了字节码验证,也不能说明其一定是安全的。因为通过程序去校验程序逻辑是无法做到绝对准确的。
符号引用验证:主要对类自身以外的,即常量池中的各种符号引用的信息进行匹配性检查,确保后续的解析动作能够正常执行。
虽然对于虚拟机的类加载机制而言,验证阶段非常重要,但是验证的工作不一定是必需的。如果所运行的全部代码已经经过反复验证和使用过后,那么在实施阶段可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
这一阶段主要是为被static修饰的变量设置零值,例如public static int value=123
,这里对value的设值不是设置为123,而是将其设为初始的0。因为为value赋值为123的putstatic指令是在程序编译后,在初始化阶段执行的, 所以在这一阶段是不会对其设值的。
但是存在有一点特殊情况,如果静态变量也被final修饰,也就是说这个变量会被放入到常量池中的话,那么在准备阶段就会被初始化赋值为指定的值,而不是零值。
解析是将常量池中的符号引用替换为直接引用的过程,主要是针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符这7类符号引用进行替换。
初始化是类加载过程的最后一步,在上述的各个步骤中,只有在加载阶段可以有程序员自定义类加载器进行加载,其余阶段均由虚拟机完全主导和控制。在初始化阶段,才是真正开始执行Java程序代码。
虚拟机的初始化执行阶段,其实是虚拟机执行()方法的过程。在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。
双亲委派模型并不是强制的约束,而是推荐给开发者的一种类加载实现方式。
setContextClassLoader(ClassLoader cl)
,使用线程的上下文类加载器。