一.类的生命周期
分析:
1)加载loading: 查找和导入class文件
不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可 以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
2)验证verification: 检查class文件的数据的正确性
为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身 的安全。
3)准备preparation: 给类的静态变量分配存储空间,并设置初始值;
这里所说的初始值概念,比如一个类变量定义为:
public static int i = 9999;
实际上变量v在准备阶段过后的初始值为0而不是9999,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器
但是注意如果声明为:
public static final int i = 9999;
在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为9999。
4)解析resolution: 指虚拟机将常量池中的符号引用替换为直接引用的过程
5)初始化 initialization : 除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶 段,才开始真正执行类中定义的Java程序代码。
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
二.类加载器
1.类的装载
将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
2.类加载器
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
两种加载class场景:
- 隐式加载:不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存,例如:当类中继承或者引用某个类时,JVM在解析当前这个类不在内存中时,就会自动将这些类加载到内存中。
- 显示加载:在代码中通过ClassLoader类来加载一个类,例如调用this.getClass.getClassLoader().loadClass()或者Class.forName()。
加载.class文件的方式有:
1. 从本地系统中直接加载 2. 通过网络下载.class文件 3. 从zip,jar等归档文件中加载.class文件 4. 从专有数据库中提取.class文件 5. 将Java源文件动态编译为.class文件
ClassLoader加载类的过程
- 找到.class文件并把这个文件加载到内存中
- 字节码验证,Class类数据结构分析,内存分配和符号表的链接
- 类中静态属性和初始化赋值以及静态代码块的执行
类初始化的触发条件:只有当对类的主动使用的时候才会导致类的初始化。
(1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
(2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
jdk中的ClassLoader的源码实现:
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded 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();
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();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- 而上面的findClass()的实现如下,直接抛出一个异常,并且方法是protected,很明显这是留给我们开发者自己去实现的.
- 首先通过Class c = findLoadedClass(name);判断一个类是否已经被加载过。
- 如果没有被加载过执行if (c == null)中的程序,遵循双亲委派的模型,首先会通过递归从父加载器开始找,直到父类加载器是Bootstrap ClassLoader为止。
- 最后根据resolve的值,判断这个class是否需要解析。
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
双亲委派模型
双亲委派模型过程
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
双亲委派模型的系统实现
在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
三.自定义类加载器
1、为何要自定义类加载器?
JVM提供的类加载器,只能加载指定目录的jar和class,如果我们想加载其他位置的类或jar时,例如加载网络上的一个class文件,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的类加载器。
2、如何实现自定义的类加载器?
我们实现一个ClassLoader,并指定这个ClassLoader的加载路径。有两种方式:
方式一:继承ClassLoader,重写父类的findClass()方法,代码如下:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class PathClassLoader extends ClassLoader
{
public static final String drive = "d:/";
public static final String fileType = ".class";
public static void main(String[] args) throws Exception
{
PathClassLoader loader = new PathClassLoader();
Class<?> objClass = loader.loadClass("HelloWorld", true);
Object obj = objClass.newInstance();
System.out.println(objClass.getName());
System.out.println(objClass.getClassLoader());
System.out.println(obj.getClass().toString());
}
public Class<?> findClass(String name)
{
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);// 将一个 byte 数组转换为 Class// 类的实例
}
public byte[] loadClassData(String name)
{
FileInputStream fis = null;
byte[] data = null;
try
{
fis = new FileInputStream(new File(drive + name + fileType));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int ch = 0;
while ((ch = fis.read()) != -1)
{
baos.write(ch);
}
data = baos.toByteArray();
} catch (IOException e)
{
e.printStackTrace();
}
return data;
}
}
main方法中调用了父类的loadClass()方法,该方法使用指定的二进制名称来加载类,下面是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
{
//parent为父加载器
if (parent != null)
{
//将搜索类或资源的任务委托给其父类加载器
c = parent.loadClass(name, false);
} else
{
//检查该class是否被BootstrapClassLoader加载
c = findBootstrapClassOrNull(name);
}
}
catch (ClassNotFoundException e)
{
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null)
{
//如果上述两步均没有找到加载的class,则调用findClass()方法
long t1 = System.nanoTime();
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();
}
}
if (resolve)
{
resolveClass(c);
}
return c;
}
}
这个方法首先检查指定class是否已经被加载,如果已被加载过,则调用resolveClass()方法链接指定的类,如果还未加载,则先将搜索类或资源的任务委托给其父类加载器,检查该class是否被BootstrapClassLoader加载,如果上述两步均没有找到加载的class,则调用findClass()方法,在我们自定义的加载器中,我们重写了findClass方法,去我们指定的路径下加载class文件。
另外,我们自定义的类加载器没有指定父加载器,在JVM规范中不指定父类加载器的情况下,默认采用系统类加载器即AppClassLoader作为其父加载器,所以在使用该自定义类加载器时,需要加载的类不能在类路径中,否则的话根据双亲委派模型的原则,待加载的类会由系统类加载器加载。如果一定想要把自定义加载器需要加载的类放在类路径中, 就要把自定义类加载器的父加载器设置为null。
方式二:继承URLClassLoader类,然后设置自定义路径的URL来加载URL下的类。
我们将指定的目录转换为URL路径,然后重写findClass方法。
四.结束生命周期
在以下情况的时候,Java虚拟机会结束生命周期
1. 执行了System.exit()方法
2. 程序正常执行结束
3. 程序在执行过程中遇到了异常或错误而异常终止
4. 由于操作系统出现错误而导致Java虚拟机进程终止
参考:
https://www.cnblogs.com/xujian2014/p/5551153.html