类加载

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规范的这样的

  1. 加载,验证,准备和初始化发生的顺序是确定的,而解析阶段则不一定
  2. 加载,验证.准备和初始化则四个阶段按顺序开始不一定按顺序完成

2.1.1 加载 基本步骤分析

  1. 通过一个类的全限定名(类全名)来获取其定义的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(把类中的字节码信息读到方法区,在方法区里存储类的结构信息)
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问 入口

2.1.2 加载路径分析

JVM从何处加载我们要使用的类呢?主要从如下是三个地方

  1. JDK类库中的类(lib\jar,lib\ext)
  2. 第三方类库中的类
  3. 应用程序类库中的类

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<String> loadClasses;
    pulblic MyClassLoader03(String baseDir,String[] classes)throws Exception{
        //指定父类加载器为null ,打破双亲委派原则
        super(null);
        this.baseDir = baseDir;
        loadClasses = new HashSet<String>();
        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);
    }
}

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值