Java类的加载过程

Posted lingengy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java类的加载过程相关的知识,希望对你有一定的参考价值。

文章目录


Java从编码到执行大概流程如图:

类加载总体流程:

一、加载

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。

2、类加载器

虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,实现这个动作的代码模块称为”类加载器”,JVM 提供了 3 种类加载器:

①、启动类加载器(Bootstrap ClassLoader)

负责加载 JAVA_HOME\\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。

②、扩展类加载器(Extension ClassLoader)

负责加载 JAVA_HOME\\lib*.jar 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。

③、应用程序类加载器(Application ClassLoader)

负责加载用户路径(classpath)上的类库。
JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。

④、自定义类加载器(Custom ClassLoader)

应用程序根据自身需要自定义的ClassLoader,如Tomcat、Jboss都会根据j2ee规范实现ClassLoader。
注意:这里容易误解,实际上不同类加载器本身不存在继承关系。

子加载器持有父加载器对象,会把类先传给父加载器加载,但是两者本身不存在继承关系。另外如果想打破双亲委派,可通过重写loadClass方法实现。


ClassLoader的findClass直接抛出异常,所以实现自定义类加载器,需要

  • 继承ClassLoader
  • 重写模板方法findClass -> 调用defineClass

如:

package com.mashibing.jvm.c2_classloader;

import com.mashibing.jvm.Hello;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class T006_MSBClassLoader extends ClassLoader 

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException 
        File f = new File("c:/test/", name.replace(".", "/").concat(".class"));
        try 
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) 
                baos.write(b);
            

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
         catch (Exception e) 
            e.printStackTrace();
        
        return super.findClass(name); //throws ClassNotFoundException
    

    public static void main(String[] args) throws Exception 
        ClassLoader l = new T006_MSBClassLoader();
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Class clazz1 = l.loadClass("com.mashibing.jvm.Hello");

        System.out.println(clazz == clazz1);

        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());

        System.out.println(getSystemClassLoader());
        System.out.println(clazz.getClassLoader());
        System.out.println(getSystemClassLoader());
    


自定义类加载器加载自加密的class,可以防止反编译,防止篡改

package com.mashibing.jvm.c2_classloader;

import com.mashibing.jvm.Hello;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class T007_MSBClassLoaderWithEncription extends ClassLoader 

    public static int seed = 0B10110110;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException 
        File f = new File("I:\\\\马士兵\\\\课程10 JVM调优第一版\\\\(剪) JVM调优第一版\\\\out\\\\production\\\\JVM\\\\", name.replace('.', '/').concat(".msbclass"));

        System.out.println("----------------");
        try 
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) 
                baos.write(b ^ seed);
            

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
         catch (Exception e) 
            e.printStackTrace();
        
        return super.findClass(name); //throws ClassNotFoundException
    

    public static void main(String[] args) throws Exception 

        encFile("com.mashibing.jvm.hello");

        ClassLoader l = new T007_MSBClassLoaderWithEncription();
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());
    

    private static void encFile(String name) throws Exception 
        File f = new File("I:\\\\马士兵\\\\课程10 JVM调优第一版\\\\(剪) JVM调优第一版\\\\out\\\\production\\\\JVM\\\\", name.replace('.', '/').concat(".class"));
        FileInputStream fis = new FileInputStream(f);
        FileOutputStream fos = new FileOutputStream(new File("I:\\\\马士兵\\\\课程10 JVM调优第一版\\\\(剪) JVM调优第一版\\\\out\\\\production\\\\JVM\\\\", name.replaceAll(".", "/").concat(".msbclass")));
        int b = 0;

        while((b = fis.read()) != -1) 
            fos.write(b ^ seed);
        

        fis.close();
        fos.close();
    


3、双亲委派模型

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

二、连接

1、验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
例如校验是否以cafe babe开头,每个位置代表什么含义也是规定好的

2、准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

public static int v = 8080;

实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器方法之中。
但是注意如果声明为

public static final int v = 8080;

在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v赋值为 8080。

3、解析/识别

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:

  1. CONSTANT_Class_info
  2. CONSTANT_Field_info
  3. CONSTANT_Method_info
    等类型的常量。

4、符号引用

符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。

5、直接引用

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

三、初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

JVM规范并没有规定何时加载类,但是严格规定了什么时候必须初始化:

  1. new getstatic putstatic invokestatic指令,访问final变量除外
  2. java.lang.reflect对类进行反射调用时
  3. 初始化子类的时候,父类首先初始化
  4. 虚拟机启动时,被执行的主类必须初始化
  5. 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化

java 类的加载过程

ClassLoader的主要职责就是负责各种class文件到jvm中,ClassLoader是一个抽象的class,给定一个class文件的二进制名,ClassLoader会尝试加载并且在jvm中生成构建这个类的各个数据结构,然后使其分布在对应的内存区域中。

  1. 1类的加载过程简介

类的记载过程一般分为三个比较大的阶段,分别是加载阶段,连接阶段和初始化阶段,如下图所示

 技术图片

加载阶段:主要负责查找并且加载类的二进制数据文件,其实就是class文件。

连接阶段:连接阶段所做的工作比较多,细分的话还可以分为如下三个阶段。

  • 验证:主要是确保类文件的正确性,比如class文件的版本,class文件的魔术因子是否正确。
  • 准备:为类的静态变量分配内存,并且为其初始化默认值。
  • 解析:把类中的符号引用转换为直接引用。

初始化阶段:为类的静态变量赋予正确的初始值(代码编写阶段给定的值)

jvm对类的初始化是一个延迟的机制,即:使用的是lazy的方式,当一个类在首次使用的时候才会被初始化,在同一个运行时包下,一个class只会被初始化一次

(运行时包和类的包时有区别的,下次再说),那么什么是类的主动使用和被动使用呢?

  1. 2 类的主动使用和被动使用

jvm虚拟机规范规定了,每个类或者接口被java程序首次主动使用时才会对其进行初始化,当然随着JIT技术越来越成熟,JVM运行期间的编译也越来越只能,

不排除JVM在运行期间提前预判并且初始化某个类。

JVM同时规范了以下6种主动使用类的场景,具体如下

  • 通过new关键字会导致类的初始化:这种是大家经常采用的初始化一个类的方式,它肯定会导致类的加载并且最终初始化。
  • 访问类的静态变量,包括读取和更新会导致类的初始化,这种情况的示例代码如下:
    public class SimpleOne 
    	static
    		System.out.println("我会被初始化");
    	
    	public static int x = 10;
    
      

这段代码中x是一个简单的静态变量,其他类即使不对SimpleOne进行new的创建,直接访问x也会导致类的初始化。

  • 访问类的静态方法,会导致类的初始化,这种情况的示例代码如下:
public class SimpleTwo 
	static
		System.out.println("我会被初始化");
	
	// 静态方法
	public static void test()
	

  同样,在其他类中直接调用test静态方法也会导致类的初始化。

  • 对某个类进行反射操作,会导致类的初始化,这种情况的示例代码如下:
public class InvokeClass 

	public static void main(String[] args) 
		try
			Class.forName("com.lanlei.classLoader.SimpleOne");
		catch(ClassNotFoundException e)
			
		
	


  运行上面的代码,同样会看到静态代码块中的输出语句执行。

  • 初始化子类会导致父类的初始化,这种情况的示例代码如下:
public class Parent 
	static
		System.out.println("父类初始化了");
	
	
	public static int y = 100;

  

public class Child extends Parent
	static
		System.out.println("子类会被初始化");
	
	public static int x = 10;

  

public class ActiveLoadTest 

	public static void main(String[] args) 
		System.out.println(Child.x);
	


 

在ActiveLoadTest中,我们调用了Child的静态变量,根据前面的知识可以得出Chid类被初始化了,Child类又是Parent类的子类,子类的初始化会进一步导致

父类的初始化,当然这里需要注意的一点是,通过子类使用父类的静态变量只会导致父类的初始化,子类则不会被初始化,示例代码如下:

public class ActiveLoadTest 

	public static void main(String[] args) 
		System.out.println(Child.y);
	


 

改写后的ActiveLoadTest,直接用Child访问子类的静态变量y,并不会导致Child的初始化,仅仅会导致Parent的初始化。

  • 启动类:也就是执行main函数所在的类会导致该类的初始化,比如使用java命令运行上下文中的ActiveLoadTest类。

除了上述6种情况,其余的都被称为被动引用,不会导致类的加载和初始化。  

  1. 3 类的加载过程详解

在正式讲解类的各个阶段的内容之前,请大家思考下面这段程序的输出结果,如果你不能准确计算出结果或者感觉有点模棱两可,那么请认真看完本小节。

public class Singleton 

	
	//  ①
	private static int x =0;
	private static int y;
	
	private static Singleton instance = new Singleton(); // ②
	
	private Singleton()
		x++;
		y++;
	
	
	public static Singleton getInstance()
		return instance;
	
	public static void main(String[] args) 
		Singleton singleton = Singleton.getInstance();
		System.out.println(singleton.x);
		System.out.println(singleton.y);
	


  运行上面的程序代码输出将是多少?如果将注释②的代码移到注释①的位置,输出结果又是什么呢?两种输出会产生不一样的结果,为何会发生这样的

现象,下面就看下本小节寻找答案。

  1. 3.1 类的加载阶段

简单来说,类的加载就是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存总

生成一个该类的java.lang.Class对象,作为访问方法区数据结构的入口,如下图所示。

技术图片

类加载的最终产物就是堆内存中的class对象,对同一个ClassLoader来讲,不管某个类被加载了多少次,对应到堆内存中的class对象始终是同一个。虚拟机

规范中指出了类的加载是通过一个全限定名(包名+类名)来获取二进制数据流,但是并没有限定必须通过某种方式获得,比如我们常见的二进制文件的形式,

但是除此之外还会有如下的几种形式。

  • 运行时动态生成,比如通过开源的ASM包可以生成一些class,或者通过动态代理java.lang.Proxy也可以生成代理类的二进制字节流。
  • 通过网络获取,比如很早之前的Applet小程序,以及RMI动态发布等。
  • 通过读取zip文件获得类的二进制字节流,比如jar、war(其实,jar和war使用的是和zip同样的压缩算法)。
  • 将类的二进制数据存储在数据库的BLOB字段类型中。
  • 运行时生成class文件,并且动态加载,比如使用Thrift,AVRO等都是可以在运行时将某个Schema文件生成对应的若干个class文件,然后进行加载。
  1. 3.2类的连接阶段

类的连接阶段可以细分为三个小的过程,分别为验证,准备,解析。

  • 验证

 先写到这,改天补充,有点事

以上是关于Java类的加载过程的主要内容,如果未能解决你的问题,请参考以下文章

JVM探究之 —— 类加载过程

JVM笔记:Java虚拟机的类加载机制

[jvm解析系列][九]类的加载过程和类的初始化。你的类该怎么执行?为什么需要ClassLoader?

JVM 类加载机制

JVM类加载

[jvm解析系列][九]类的加载过程和类的初始化。你的类该怎么执行?为什么需要ClassLoader?