一、类加载过程
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
1、加载:在磁盘上查找编译后的.class文件,并通过IO写进内存。只有当某个类被使用时才会加载(懒加载),比如调用该类的main()方法或者new对象等。在加载阶段会生成一个该类的java.lang.Class对象,作为方法区各种数据(比如方法等类元信息)的访问入口
注:每一个类被加载时,其实JVM内部会生成两个Klass对象,InstanceKlass和MateInstanceKlass,前者是在堆区,主要包含一些静态属性;后者是在方法区,包含类元信息
2、验证:校验字节码文件的正确性
3、准备:给类的静态变量分配内存,并赋予默认值
4、解析:将符号引用替换为直接引用,该阶段会把一些静态方法(比如main())替换为指向数据所在内存的指针或句柄等(直接引用),这个过程称为静态链接(类加载期间完成)。而动态链接是在程序运行过程中将符号引用转换为直接引用。
符号引用可以简单理解为一个类中定义的属性和方法等的字面量,也就是方法名、属性名等这些,而直接引用就是方法或者属性对应的在内存中的地址。每个.class文件都有一个常量池,这些常量池里面存放的就是各种符号引用。常量池又分为静态常量池和运行时常量池,常量池在磁盘上存放着时就是静态常量池,将静态常量池加载进内存中(方法区)就变成了运行时常量池,程序运行的过程中可以将新的常量放入到运行时常量池中。
5、初始化:给类的静态变量初始化为指定的值,执行静态代码块。
类被加载到方法区之后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器引用、对应Class实例的引用等信息。
类加载器引用:该类指向类加载器实例的引用
对应Class实例引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中,作为访问方法区中类定义的入口和切入点,该Class实例与加载过程中提到的InstanceKlass对象是对应的。
注意:类在使用过程中是逐步加载的,Jar包或War包中的类并不是一次性全部加载,而是使用到时才加载。
二、类加载器
启动类加载器(Bootstrap Class Loader)
负责加载存放在JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等,该加载器实例是通过JVM底层的C++代码创建的,所有Java程序中如果想打印该加载器实例时,返回的是Null。
扩展类加载器(Extension Class Loader)
该类加载器是在sun.misc.Lauuncher$ExtClassLoader内部通过Java代码实现的。主要负责加载存在在JRE的lib/ext目录下的JAR包以及通过java.ext.dirs系统变量指定的路径中的JAR包。
应用程序类加载器(Application Class Loader)
该类加载器与扩展类加载器一样,是在sun.misc.Lauuncher$AppClassLoader内部通过Java代码实现,主要负责加载ClassPath路径下的所有JAR包。
ClassLoader中的loadClass方法源码如下:
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
}
//父类找不到就抛异常 findClass(name);直接抛异常ClassNotFoundException异常:
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;
}
}
还有一个方法我们需要注意的是defineClass(),它的作用是将字节数组转换成Class对象
三、双亲委派机制
双亲委派机制的好处:
沙箱安全机制:如果开发人员编写了一个同核心类库包和类名都相同的类,通过双亲委派机制,这样的类并不会加载(加载的是对应路径下的类),可以防止核心API库被随意篡改。
避免重复加载:父加载器已经加载过的类,无序子加载器再次加载,保证被加载类的唯一性。同一个类,被不同的类加载器加载,生成的Class实例是不同的。
除了双亲委派机制外,还有全盘负责委托机制,即:当一个ClassLoader装载一个类时,除非显示的使用另一个ClassLoader,否则该类所依赖和引用的类也都将由该ClassLoader进行装载。
机制:类加载的过程中子加载器会交给父加载器,逐级传递,父加载器找不到后才逐级向下传递创建
四、自定义类加载器
继承ClassLoader类
//自定义类加载器
public class MyClassLoader extends ClassLoader{
//路径
private String codePath;
public MyClassLoader(ClassLoader parent, String codePath) {
super(parent);
this.codePath = codePath;
}
public MyClassLoader(String codePath) {
this.codePath = codePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis=null;
ByteArrayOutputStream baos=null;
//完整的类名
String file = codePath+name+".class";
try {
//初始化输入流
bis = new BufferedInputStream(new FileInputStream(file));
//获取输出流
baos=new ByteArrayOutputStream();
int len;
byte[] data=new byte[1024];
while ((len=bis.read(data))!=-1){
baos.write(data,0,len);
}
//获取内存中的字节数组
byte[] bytes = baos.toByteArray();
//调用defineClass将字节数组转换成class实例
Class<?> clazz = defineClass(null, bytes, 0, bytes.length);
return clazz;
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
调用 E盘放一个实体类User.class
public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader("E:/");
try {
Class<?> clazz = myClassLoader.loadClass("User");
System.out.println("是"+clazz.getClassLoader().getClass().getName()+"加载");
Object o = clazz.newInstance();
System.out.println(o.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
双亲委派模型的实现核心方法是loadClass()、findClass()和defineClass(),其中loadClass()是核心逻辑,findClass()将文件加载到内存成为,defineClass()是将二进制文件转换为活的class文件,自定义类加载器也是从这三个核心方法入手的,我们通过重写findClass()方法来自定义了一个类加载器,成功加载了本地的一个class文件。