Java的核心是 JVM ,了解并熟悉JVM对于我们理解Java语言非常重要。
一、类加载机制
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。
JVM把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是JVM的类加载机制。
1、JVM和类
当调用Java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。
同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。
当系统出现以下几种情况时,JVM进程将被终止:
- 程序运行到最后正常结束。
- 程序运行到使用
System.exit()
或Runtime.getRuntime().exit()
处程序结束。 - 程序执行过程中遇到未捕获的异常或错误而结束。
- 程序所在平台强制结束了JVM进程。
从上面介绍可以知道,当Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。
2、类的加载
类加载:将类的class文件读入内存,并为之创建一个java.lang.Class 对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承 ClassLoader 基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
- 从本地文件系统加载class文件。
- 从 JAR 包加载class文件。
- 通过网络加载class文件。
- 把一个 java 源文件动态编译,并进行加载。
在加载阶段虚拟机需要完成以下三件事:
- 通过一个类的全限定名称来获取此类的二进制字节流,并加载到内存中(需要使用类加载器)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
3、类的链接
将Java类的二进制数据合并到 JVM 的运行状态之中。
类的连接分为三个阶段:
验证:验证被加载后的类是否有正确的结构,类数据是否符合虚拟机的要求,确保不会危害虚拟机安全。
包含四个阶段的校验动作:a.文件格式验证;b.原数据信息进行语义校验;c.字节码验证;d.符号引用验证。准备:为类的静态变量(static filed)在方法区分配内存,并设置默认初始值(0值或null值),这些内存都将在方法区分配。对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,对于静态变量,这个操作是在初始化阶段进行的。解析:将类的二进制数据内的符号引用替换为直接引用。
4、类的初始化
在该阶段,虚拟机负责对类进行初始化,主要是对类变量进行初始化。
在Java类中,对类变量指定初始值有两种方式:
(1)在声明类变量时指定初始值。
(2)在使用静态初始化块时,为类变量指定初始值。
JVM会按照这些语句在程序中的排列顺序依次执行他们。
JVM初始化一个类的步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类。
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类。若该直接父类又有直接父类,依次类推。所以JVM最先初始化的总是 java.lang.Object 类。
当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。 - 假如类中有初始化语句,则系统依次执行这些初始化语句。
5、类初始化的时机
当Java程序首次通过下面的 6种 方式来使用某个类或接口时,系统就会初始化该类或接口,也称为主动初始化。
触发类加载的条件:
- 创建类的实例。
- 使用 new 来创建实例。
- 通过反射创建实例。
- 通过反序列化来创建实例。
- 调用类的类变量(静态属性),或为该类变量赋值。
- 调用类的静态方法。
- 通过class文件反射创建对象。
例如:Class.forName("Person");
- 初始化一个子类的时候,该子类的所有父类都会被初始化。
- java虚拟机启动时被标记为启动类的类,就是 main 方法所在的类。
同时还需要注意几点:
- 在同一个类加载器下面只能初始化类一次,如果已近初始化了就不要初始化了。
因为累加载的最终结果就是在堆中存有唯一的一个Class对象,这样通过Class对象就能找到类的相关信息。 - 在编译时能够确定下来的 final修饰的静态变量(编译常量)不会对类进行初始化。
- 在编译时无法确定下来的 final修饰的静态变量(运行时常量)会对类进行初始化。
- 如果这个类没有被加载和连接,那就需要进行加载和连接。
- 如果这个类有父类并且这个父类没有被初始化,则先初始化父类。
- 如果类中存在初始化语句,依次执行初始化语句。
一个有关的小例子:
public class Single {
private static Single single = new Single();
public static int counter1;
public static int counter2 = 0;
private Single () {
counter1++;
counter2++;
}
public static Single getSingle() {
return single;
}
}
public class SingleTest {
public static void main(String[] args) {
Single single = Single.getSingle();
System.out.println("counter1=" + single.counter1);
System.out.println("counter2=" + single.counter2);
}
}
输出是:
counter1=1
counter2=0
例子分析:
- 在执行SIngleTest第一句的时候,还没有对Single类进行加载和连接,所以首先需要对它进行加载和连接。
在连接——准备阶段,要给静态变量赋默认的初始值。
singel=null
counter1=0
counter2=0
- 加载和连接完毕之后,再进行初始化工作。这时会依次执行。
首先第一个静态属性single = new Single();
会执行构造方法内部的逻辑操作,此时
counter1=1
counter2=1
接下来第二个静态属性counter1,程序并没有对它进行初始化赋值,所以它没办法进行初始化。
第三个属性counter2我们初始化复制为0,因此可以初始化为 counter2=1。
- 初始化完毕之后,就调用了静态方法
Single.getSingle();
放回的single
已经初始化了。
输出的内容也理所当然就是counter1=1,counter2=0
二、类加载器
类加载器负责将 .class 文件加载到内存中,并为之生成对应的 Class 对象。
在JVM中,一个类用其全限定类名和其类加载器作为唯一的标识。这样保证同一个类不会再次被载入。
1、类加载器的层级结构
引导类加载器(Bootstrap ClassLoader)
它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.Path路径下的内容),是用C++代码来实现的,并不继承自java.lang.Classloader。
加载扩展类和应用程序类加载器,并指定他们的父类加载器。
启动类加载器无法被Java程序直接引用
扩展类加载器(Extension ClassLoader)
- 用来加载Java的扩展库(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)。 Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载Java类。
- 由sun.misc.Launcher$ExtClassLoader实现。
应用程序类加载器(Application ClassLoader)
- 它根据Java应用的类路径(classpath,java.class.path类。 一般来说,Java应用的类都是由它来完成加载的。
- 由sun.misc.Launcher$AppClassLoader实现。
自定义类加载器
开发人员可以用过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的要求
2、类加载机制——双亲委派模式
几个类加载器实现类加载过程时相互配合协作的流程。
从JDK1.2开始,java虚拟机规范就推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:
- 如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
- 每一层的类加载器都把类加载请求委派给父类加载器,依次向上,直到所有的类加载请求都传递给顶层的启动类加载器。
- 如果顶层的启动类加载器无法完成加载请求,子类加载器才会尝试自己去加载该类,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。
双亲委派模式的类加载机制的优点:
不同层次的类加载器具有不同优先级,比如所有Java对象的超级父类java.lang.Object,位于rt.jar,无论哪个类加载器加载该类,最终都是由启动类加载器进行加载,保证安全。即使用户自己编写一个java.lang.Object类并放入程序中,虽能正常编译,但不会被加载运行,保证不会出现混乱。
注意:
- 并不是所有的类加载器都采用双亲委托机制。
- tomcat服务器类加载器也是用代理模式,所不同的是它首先尝试去加载某个类,如果找不到再找代理给父类加载器。这与一般类加载器的顺序是相反的。
双亲委派模型的代码实现
ClassLoader中loadClass方法实现了双亲委派模型
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果该类没有加载,则进入该分支
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
long t1 = System.nanoTime();
c = findClass(name); //用户可通过覆写该方法,来自定义类加载器
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
整个流程大致如下:
a.首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
b.如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
c.如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
3、自定义类加载器
通过扩展 ClassLoader 的子类,重写 ClassLoader 所包含的方法来实现自定义的类加载器。
ClassLoader 类有如下两个关键方法:
-
loadClass(String name, boolean resolve)
:该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用 ClassLoader 的该方法来获取指定类对应的 Class 对象。 -
findClass(String name)
:根据指定名称来查找类。
通常推荐重写 findClass() 方法。
在 ClassLoader 类中还有一个核心方法:
-
Class defineClass(String name, byte[] b, int off, int len)
:该方法负责将指定类的字节码文件(即Class文件,如:Hello.class)读入字节数组byte[] b
内,并把它转换为 Class 对象。
无须重写该方法,因为该方法是 final 的。
除此之外,ClassLoader 类还有一些普通方法:
-
findSystemClass(String name)
:从本地系统装入文件。 -
static getSystemClassLoader()
:用于返回系统类加载器。 -
getParent()
:获取该类加载器的父类加载器。 -
resolveClass(Class<?> c)
:链接指定的类。 -
findLoadClass(String name)
:如果Java虚拟机已经加载了名为 name 的类,则直接返回该类对应的 Class 实例,否则返回 null 。该方法是 Java 类加载缓存机制的体现。
整个的函数调用流程:
我们可以简单地自定义一个类加载器,用于加载某个class
public class FileSystemClassLoader extends ClassLoader {
//文件的根目录
private String rootDir;
public FileSystemClassLoader(String rootDir){
this.rootDir=rootDir;
}
//重写findClass方法
@Override
protected Class<?> findClass(String s) throws ClassNotFoundException {
Class c=findLoadedClass(s);
if (c!=null){
return c;
}else {
ClassLoader parent=this.getParent();
//parent获取不到class时会抛出异常,为了继续执行使用try catch包裹
try{
c=parent.loadClass(s);
}catch (Exception e){
}
if (c!=null){
return c;
}else {
byte[] classData=getClassData(s);
if (classData==null){
throw new ClassNotFoundException();
}else {
//将字节数组转为Class
c=defineClass(s,classData,0,classData.length);
}
}
}
return c;
}
//将文件转为字节数组
private byte[] getClassData(String className) {
//改为文件地址
String path=rootDir+"/"+className.replace(".","/")+".class";
System.out.println(path);
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
InputStream inputStream=null;
try {
inputStream=new FileInputStream(path);
byte[] buffer=new byte[1024];
int temp=0;
while ((temp=inputStream.read(buffer))!=-1){
byteArrayOutputStream.write(buffer,0,temp);
}
return byteArrayOutputStream.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
if (inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (byteArrayOutputStream!=null){
try {
byteArrayOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
public class UseCustomClassLoader {
public static void main(String[]args){
FileSystemClassLoader loader=new FileSystemClassLoader("/home/xjk");
FileSystemClassLoader loader2=new FileSystemClassLoader("/home/xjk");
try {
Class clazz1=loader.findClass("com.jk.bean.Emp");//本项目自定义的类调用AppClassLoader
System.out.println(clazz1.getClassLoader());
Class clazz2=loader.findClass("java.lang.String");//rt.jar里的类调用BootstrapClassLoader
System.out.println(clazz2.getClassLoader());
Class clazz3=loader.findClass("com.company.Main");//项目外的类调用自定义的FileSystemClassLoader
System.out.println(clazz3.getClassLoader());
Class clazz4=loader2.findClass("com.company.Main");//使用不同类加载器,Class对象不一致
System.out.println(clazz4.getClassLoader());
System.out.println(clazz3==clazz4);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果
sun.misc.Launcher$AppClassLoader@18b4aac2
null
com.jk.jvm.FileSystemClassLoader@1d44bcfa
com.jk.jvm.FileSystemClassLoader@6f94fa3e
false
因为BootstrapClassLoader无法被Java程序直接引用,所以显示为空。
使用自定义的类加载器,可以实现如下常见的功能:
- 执行代码前自动验证数字签名。
- 根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译 *.class 文件。
- 根据用户需求来动态的加载类。
- 根据用户需求把其他数据以字节码的形式加载到应用中。