JVM运行时需要把class文件加载到内存中,将class文件内容解析成jvm认识的数据结构,比如把类和类成员变量,类方法,运行时常量,类对应的class对象,并根据代码逻辑对类进行初始化,这一整个过程即是JVM的类加载过程。整个过程分为装载 验证 准备 解析 初始化五步。
装载:JVM通过类的全限定名读取class文件到内存,转化为特定的数据结构存储到方法区之中,并生成类对应的Class对象放在内存中,作为方法区该类数据的访问入口,Hotspot虚拟机的类Class对象和类的数据一起放在方法区的。
验证:JVM对读取到的类数据并不是完全放心的,所以接下来对类数据进行验证。包含文本格式验证 元数据验证 字节码验证 符号引用验证。
准备:为类变量(static修饰的变量)分配内存设置类变量的默认初始值(零值,非用户程序赋值的初始值)。如果是final static修饰的常量,在编译阶段就会当成常量,此时JVM会为其直接赋值为用户程序初始值。
解析:将常量池中的符号引用解析成直接引用。即解析前,运行时常量池里的符号引用只是一个符号,用来描述目标。解析后的直接引用,可以直接找到对应的目标。
初始化:JVM执行用户编写的初始化程序代码。即static块和static变量的赋值。编译器会自动生成这两个部分的代码生成类的构造方法 < clinit >,初始化时执行该方法。
JVM规定了有且只有五种情况下会去初始化一个类。
1、遇到new getstatic putstatic invokestatic 这四条字节码指令时,会去对类进行初始化。从用户编写程序代码层面看,在使用new关键字创建对象时 读取或者设置一个static修饰的类变量时,以及调用一个类的static方法时。 个人理解:当进行这些操作时,目标类必须是一个完整的类,即类的初始状态已经准备就绪,否则得先去进行初始化操作。
2、使用反射技术去调用的时候,比如Class.forName() 这时是通过想要通过类的Class对象获取对该类进行操作,同理,该类必须是一个初始化状态的就绪的类。
3、在初始化一个子类时,会先初始化其父类。
4、启动类,即包含入口main()方法的类会先进行初始化。指的是该类的main方法会作为程序入口方法。如果只是存在一个main方法,但是不是作为入口方法,也不会初始化,spring项目中定义一个类包含main方法,但是不去启动main方法,用tomcat启动时并不会初始化。
public class Test {
static {
System.out.println("Test 初始化静");
}
/**
* 4、启动类,即包含入口main()方法的类会先进行初始化。
* 指的是该类的main方法会作为程序入口方法。
* 如果只是存在一个main方法,但是不是作为入口方法,也不会初始化,
* spring项目中定义一个类包含main方法,但是不去启动main方法,用tomcat启动时并不会初始化。
*
* 直接执行该方法会初始化
* @param args
*/
public static void main(String[] args) {
}
}
5、动态链接时,需要先去初始化目标类。
双亲委派模型
启动类加载器 扩展类加载器 应用类加载器
启动类加载器是JVM实现的一部分,程序员无法干预的。用来加载JAVA_HOME/lib下的基础jar,如rt.jar.或者启动参数-Xbootclasspath指定路径下被JVM认可的类。
扩展类加载器,加载JAVA_HOME/lib/ext下的扩展jar,或者java.ext.dirs系统变量指定路径中的类库.
应用类加载器,系统类加载器Launcher.AppClassLoader。由ClassLoader.getSystemClassLoader()方法返回,用来加载用户路径下的类,既我们编写的类默认都是由它加载。
/**
* 类加载时调用该方法去加载类。
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 加锁 保证只会有一个线程的加载器进入该方法
synchronized (getClassLoadingLock(name)) {
// 查询已加载的Class
Class<?> c = findLoadedClass(name);
//为空 表示未加载过 开始加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 父类加载器存在 交给父加载器加载
c = parent.loadClass(name, false);
} else {
//父加载器不存在 查询启动类加载器 去加载
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.
long t1 = System.nanoTime();
// 调用自己的findClass方法加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 已加载 去初始化 返回该类的Class对象
if (resolve) {
resolveClass(c);
}
return c;
}
}
当类加载器加载一个类时,首先把加载请求委托给它的父类加载器,父类加载器无法加载时,才自己去加载。保证JVM中不会加载完全相同的两个类。
全限定名相同的类只会加载第一个加载到的,之后的类不会加载。
能不能自己写个类叫java.lang.Math?
答案是不能。1、如果使用默认加载器加载,会委托给启动类加载器加载rt.jar下的java.lang.Math.不会再加载自定义的这个Math。
package java.lang;
/**
* @author neil
* @date 2019-07-18
*/
public class Math {
public static void main(String[] args) {
System.out.println("123");
}
}
执行main方法报错,表明未加载该自定义的java.lang.Math
2、如果指定自定义的类加载器去加载,抛出异常java.lang.SecurityException。
package org.neil.util;
import java.io.InputStream;
/**
* @author neil
* @date 2019-07-18
*/
public class ClassLoaderTest extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try{
String className = null;
if(name.startsWith("java.java.lang")){
className = "/" + name.replace('.', '/') + ".class";
}else{
className = name.substring(name.lastIndexOf('.') + 1) + ".class";
}
System.out.println(className);
InputStream is = getClass().getResourceAsStream(className);
System.out.println(is);
if(is == null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myLoader = new ClassLoaderTest();
Object obj = myLoader.loadClass("java.java.lang.Math").newInstance();
System.out.println(obj);
}
3、自建一个包名为java.lang,也会抛出异常java.lang.SecurityException。
package java.lang;
/**
* @author neil
* @date 2019-07-18
*/
public class MyMath {
public static void main(String[] args) {
System.out.println("123");
}
}
禁止使用包名java.