1.1 类加载概述
所谓类加载就是将类从磁盘或网络读到JVM内存,然后交给执行引擎执行
类加载器有哪些
主要的就是启动加载器(BootStrap ClassLoader) 和其他所有类加载器
注意:启动类加载器是虚拟机一部分,是由c++实现的,其他类加载器是独立于虚拟机外的一部分,都继承了java.lang.ClassLoader
类加载器主要分为以下四部分
启动加载器:Bootstrap ClassLoader
扩展类加载器: Extension ClassLoader
应用类加载器:Application ClassLoader
用户自定义加载器:User Defined ClassLoader
如图所示:
1.2 Java类声明周期分析
类的生命周期指的是从加载到卸载的基本过程,次过程包含7个阶段,如下图所示
类的加载过程
2.1 加载解析
加载 过程中大致可分为加载,验证,准备,解析,初始化几大阶段,但这几个的执行顺序是怎样的呢?JVM规范的这样的
2.1.1 加载 基本步骤分析
2.1.2 加载路径分析
JVM从何处加载我们要使用的类呢?主要从如下是三个地方
2.13 加载方式及时机分析
JVM中的类加载方式主要两种: 隐式加载和显示加载
1) 隐式加载: 都会初始化静态代码块
a) 访问类的静态成员(例如类变量,静态方法)
b) 构建类的实例对象(例如使用new关键字构建对象和反射构建对象)
c) 构建子类实例对象(构建类的对象是首先会加载父类类型)
2) 显示加载: 默认情况下不初始化静态代码块
a) ClassLoader.loadClass(...)
b) Class.forName(...)
隐式加载代码分析
package cgb.java.jvm.loader;
class Class01{
static int cap=100;
static{
System.out.println("Class01.static");
}
static void doPrint(){
System.out.println("print class info");
}
}
class SubClass01 extends Class01{
static{
System.out.println("SubClass01.static");
}
}
public class TestClassLoader00{
public static void main(String[] args){
//会执行静态代码块
//System.out.println(Class01.cap);
//会执行静态代码块
//Class01.doPrint();
//会执行静态代码块
//new Class01();
//会执行静态代码块,先加载父类,后加载子类
new SubClass01();
}
}
显示加载代码分析:
package cgb.java.jvm.loader;
class ClassA{
static{
System.out.println("ClassA");
}
}
/**显示加载*/
public class TestClassLoader01{
public static void main(String[] args)throws Exception{
//-XX:+TraceClassLoading 查看类加载信息
ClassLoader loader = TestClassLoader01.class.getClassLoader();
//不执行静态代码块
loader.loadClass("cgb.java.jvm.loader.ClassA");
//执行静态代码块
//Class.forName("cgb.java.jvm.loader.ClassA");
//执行静态代码块
//Class.forName("cgb.java.jvm.loader.ClassA",true,loader);
//不执行静态代码块
//当 initialize为false时,在加载类时不会执行静态代码块
Class.forName("cgb.java.jvm.loader.ClassA",true,loader);
}
}
总结: 隐式加载都会执行静态域,显示加载可能执行也可能不执行
2.2 连接分析(linking)
2.2.1 验证(Verification)
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
验证阶段大致会完成4个阶段的检验动作
1) 文件格式的验证: 主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理,例如:主次版本号是否在当前虚拟机处理的范围之内,常量池中是否有不被支持的常量类型,指向常量中的索引值是否存在不存在的常量或不符合类型的常量
2) 元数据验证: 对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范
3) 字节码合法性验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的,主要的的只对元数据验证后堆方法体的验证,保证类方法在运行时不会有危害出现
4) 符号引用验证(class文件中以CONSTANT_Class_info,CONATANT_Fieldref_info 等常量形式出现)
说明: 验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用_Xverify:none参数类关闭大部分的类验证措施,以缩短虚拟机加载的时间
2.2.2 准备(Preparation)
准备阶段是正式为类变量分配内存并设置类变量是初始化的阶段,这些内存都将在方法区中分配
1) 类变量(static) 内存分配
2) 按类型进行初始化默认值(如0,0L,null,false)
例如:
假设一个类变量的定义为:public static int value=3;
那么变量value在准备阶段过后的初始值为0,而不是3,把value赋值为3的动作将在初始化阶段才会执行
3) 如果类字段的字段属性中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value结汇被初始化为ConstantValue属性所指定的值
例如:
假设一个类变量的定义为:public static final int value=3;
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3
2.2.3 解析(Resolution)
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,其中:
1) 符号引用: 就是一组符号(例如:CONSTANT_Fieldref_info) 来描述目标,可以是任何字面量
2) 直接引用: 就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄
说明: 相同的符号引用不同JVM机器上对应的直接引用可能不同,直接引用一般对应已加载到内存中的一个具体对象
2.3 初始化分析(Initialization)
此阶段为类加载的最后一个阶段,这个阶段我们让自定义类加载器参与进来,其余阶段完全由JVM主导,例如JVM负责对类进行初始化,主要对类变量进行初始化
在Java中,对类变量进行初始值测设置有两种方式
1) 声明类变量时指定初始值
2) 使用静态代码段为类变量执行初始值
说明:只有当对类的主动使用的时候才会导致类的初始化
Java程序对类的使用方式可以分为两种
主动使用: 会执行加载,连接,初始化静态域
被动使用: 只执行加载,连接,不初始化类的静态域
如何理解被动使用?
如通过子类引用父类的静态字段,为子类的被动使用,不会导致子类初始化 例如:
package com.home;
class ClassA{
public static int a=10;
static {
System.out.println("A.enclosing_method()");
}
}
class ClassB extends ClassA{
static {
System.out.println("B.enclosing_method()");
}
}
public class TestClassLoader03 {
public static void main(String[] args) {
//不会执行子类的静态代码块,会执行父类的静态代码块
//但是子类和父类都被加载了,只是子类没有执行静态代码块
System.out.println(ClassB.a);
}
}
3.1 类加载器概要分析
3.1.1 类加载器简介
类加载器是在类运行时负责将类读到内存的一个对象,其类型为ClassLoader类型,此类型为抽象类型,通常以父类形式出现
类加载器对象常用方法说明:
1) getParent() 返回类加载器的父类加载器
2) LoadClass(String name) 加载名称为name的类
3) findLoadedClass(String name) 查找名称为name的类
4) defineClass(String name,byte[] b,int off ,int len) 把字节属性b中的内容转换成java类
3.1.2 类加载器的层次架构
java中类加载器大致可分为两种,一类是系统提供,一类是自己定义,其层级结构如下图所示
类加载器加载类的过程分析
1) 首先会查看自己是否以加载过此类,有则返回,没有则将加载任务委托给父类(parent)加载器加载,依次递归
2) 父类加载器无法完成此加载任务时,自己去加载
3) 自己也无法完成加载时就会抛出异常
说明:类加载时首先委托父类加载的这种机制称之为双亲委派机制,基于这种机制实现了类加载时的优先级层次关系,同时也可以保证同一个类只被一个加载器加载(例如Object类只会被BootstrapClassLoader加载),这样更有利于java程序的稳定运行
代码演示: 获取类的加载器对像
package com.baidu.exercise;
import java.util.ArrayList;
public class Demo01 {
public static void deMethod01() {
ClassLoader loader = ClassLoader.getSystemClassLoader();
//AppClassLoader 应用类加载器
System.out.println(loader);
//ExtClassLoader 扩展类加载器
System.out.println(loader.getParent());
//null BoorstrapClassLoader 启动类加载器 因为是用c语言类写的,所以拿不到
System.out.println(loader.getParent().getParent());
}
public static void deMethod02() {
//获取当前线程的类加载器
ClassLoader loader = Thread.currentThread().getContextClassLoader();
//AppClassLoader 应用类加载器
System.out.println(loader);
//ExtClassLoader 扩展类加载器
System.out.println(loader.getParent());
//null BoorstrapClassLoader 启动类加载器
System.out.println(loader.getParent().getParent());
}
public static void deMethod03() {
//AppClassLoader 应用类加载器
System.out.println("ClassLoader of this class:"+Demo01.class.getClassLoader());
//null BoorstrapClassLoader 启动类加载器
System.out.println("ClassLoader if ArrayList:"+ArrayList.class.getClassLoader());
}
public static void main(String[] args) {
deMethod01();
deMethod02();
deMethod03();
}
}
自定义类加载器
JVM自带的类加载器只能加载默认classpath下的类,如果我们需要加载应用程序之外的类文件呢?比如网络上的某个类文件,这种情况一般就用自定义加载器了,自定义加载器可以自己制定类加载的路径,可以实现系统在线升级(热替换)等操作,在我们使用的Tomcat服务器,spring框架,mybatis框架等其内容都自己定义了类加载器
我们自己写类加载器一般需要直接或间接的继承ClassLoader类,然后重写相关方法,
3.2.1 准备工作: 在指定包中创建一个自己写的类,例如:
package pkg;
public class Search {
static {
System.out.println("Search static");
}
public Search() {
System.out.println("Search constructor");
}
}
将Search.class文件拷贝包F:\\WORKSPACE\\pkg
3.2.2 基于ClassLoader创建
package com.danei.jvm;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader01 extends ClassLoader{
/**基本路径*/
private String baseDir;
public MyClassLoader01(String baseDir) {
this.baseDir=baseDir;
}
/**从指定路径下找到指定的类*/
@Override
protected Class> findClass(String name)throws ClassNotFoundException{
byte[] classDate = loadClassBytes(name);
if(classDate == null) {
throw new ClassNotFoundException();
}else {
//将字节数组信息转换为Class类型的对象
return defineClass(name, classDate, 0, classDate.length);
}
}
/**从指定路径下读取字节码信息
* className 全路径(包名.类名)*/
private byte[] loadClassBytes(String className) {//pkg.Search
String fineName = baseDir+className.replace(".",File.separator)+".class";
System.out.println("fineName:"+fineName);
InputStream ins=null;
try {
ins = new FileInputStream(fineName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while((length = ins.read(buffer)) != -1) {
bos.write(buffer,0,length);
}
return bos.toByteArray();
}catch(Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}finally {
if(ins != null)try {ins.close();}catch(Exception e) {}
}
}
}
说明: 自己写类加载器一般不建议重写LoadClass方法,当然不是不可以重写
定义测试方法: 假如使用自定义类加载器加载我们指定的类,要求被加载的类应该与当前类不在同一命名空间,否则可能直接使用AppClassLoader进行类加载
public class TestMyClassLoader{
public static void main(String[] args)throws Exception{
String baseDir = "F:\\WORKSPACE\\";
MyClassLoader classLoader = new MyClassLoader(baseDir);
//此类不要和当前类放在相同目录结构中
String pkgCls = "pkg.Search";
Class> cls = classLoader.loadClass(pkgCls);
Object obj = cls.newInstance();
System.out.println(obj.getClass());
System.out.println(obj.getClass().getClassLoader());
}
}
修改准备类Search的类名为Searcher,看看区别
3.2.3基于URLClassLoader创建
URLClassLoader继承ClassLoader,可以从指定位置,jar包,网络中加载指定的类资源
package com.danei.jvm;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
/**
* 基于URLClassLoader创建
* @author Administrator
*
*/
public class TestMyClassLoader02 extends URLClassLoader{
public TestMyClassLoader02(URL[] urls) {
//指定夫加载器为null
super(urls,null);
}
public static void main(String[] args) throws Exception {
File file = new File("F:\\WORKSPACE\\");
URI uri = file.toURI();
URL[] urls ={uri.toURL()};
ClassLoader classLoader= new TestMyClassLoader02(urls);
Class> cls = classLoader.loadClass("pkg.Search");
System.out.println(cls.getClassLoader());
Object obj = cls.newInstance();
System.out.println(obj);
}
}
3.3 基于类加载器实现热替换
当我们的项目运行时假如需要实现在线升级(也就是常说的热替换),可以通过自定义类加载实现,例如
自定义类加载器
class MyClassLoader03 extends ClassLoader{
//需要该类加载器直接加载的类文件的基目录
private String baseDir;
//需要由该类加载器直接加载的类名
private HashSet loadClasses;
pulblic MyClassLoader03(String baseDir,String[] classes)throws Exception{
//指定父类加载器为null ,打破双亲委派原则
super(null);
this.baseDir = baseDir;
loadClasses = new HashSet();
customLoadClass(classes);
}
/**获取所有文件完整路径及类名,存入缓存*/
private void customLoadClass(String[] classes)throws Exception{
for(String classStr : classes){
loadDirectly(classStr);
loadClasses.add(classStr);
}
}
/**拼接文件路径及文件名*/
private void loadDirectly(String name)throws IOException{
StringBuilder sb = new StringBuilder(baseDir);
String classname = name.replace(".",File.separatorChar)+".class";
sb.append(File.separator).append(classname);
File classF = new File(sb.toString());
instantiateClass(name,new FileInputStream(classF),classF.length());
}
/**读取并加载类*/
private void instantiateClass(String name,InputStream fin,long len)throws Exception{
byte[] raw = new byte[(int)len];
fin.read(raw);
fin.close();
defineClass(name,raw,0,raw.length);
}
@OVerride
protected Class> loadClass(String name , boolean resolve)throws Exception{
//判断是否已加载(在命名空间中寻找指定的类是否已存在)
Class> cls = findLoadedClass(name);
if(!this.loadClasses.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if(cls == null)
throw new ClassNouFoundException(name);
if(resolve)
resolveClass(cls);
return cls;
}
}
编写测试类
public class TestMyClassLoader03{
public static void main(String[] args)throws Exception{
MyClassLoader03 loader = new MyClassLoader03("f:\\workspace\\"
,new String[] {"pkg.Search"});
Class> cls = loader.loadClass("pkg.Search");
System.out.println(cls.getClassLoader());
Object searach = cls.newInstance();
System.out.println(search);
Thread.sleep(20000);
//再次执行加载时需要用新的类替换目标目录中的类
loader = new MyClassLoader03("f:\\workspace\\"
,new String[] {"pkg.Search"});
cls = loader.loadClass("pkg.Search");
System.out.println(cls.getClassLoader());
searach = cls.newInstance();
System.out.println(search);
}
}