JVM(四)—一道面试题搞懂JVM类加载机制

2022-08-31 20:15:41 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

有这样一道面试题:

代码语言:javascript复制
class Singleton{
	private static Singleton singleton = new Singleton();
	public static int value1;
	public static int value2 = 0;
	
	private Singleton(){
		value1  ;
		value2  ;
	}
	
	public static Singleton getInstance(){
		return singleton;
	}
	
}

class Singleton2{
	public static int value1;
	public static int value2 = 0;
	private static Singleton2 singleton2 = new Singleton2();
	
	private Singleton2(){
		value1  ;
		value2  ;
	}
	
	public static Singleton2 getInstance2(){
		return singleton2;
	}
	
}

public static void main(String[] args) {
		Singleton singleton = Singleton.getInstance();
		System.out.println("Singleton1 value1:"   singleton.value1);
		System.out.println("Singleton1 value2:"   singleton.value2);
		
		Singleton2 singleton2 = Singleton2.getInstance2();
		System.out.println("Singleton2 value1:"   singleton2.value1);
		System.out.println("Singleton2 value2:"   singleton2.value2);
	}

说出运行的结果: Singleton1 value1 : 1 Singleton1 value2 : 0 Singleton2 value1 : 1 Singleton2 value2 : 1

稍后会带来分析。

一 类加载机制

JVM类加载分为5个过程:加载,验证,准备,解析,初始化,使用,卸载,如下图所示:

下面来看看加载,验证,准备,解析,初始化这5个过程的具体动作。

1.1 加载

加载主要是将.class文件(并不一定是.class。可以是ZIP包,网络中获取)中的二进制字节流读入到JVM中。 在加载阶段,JVM需要完成3件事: 1)通过类的全限定名获取该类的二进制字节流; 2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构; 3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

1.2 连接

1.2.1 验证

验证是连接阶段的第一步,主要确保加载进来的字节流符合JVM规范。 验证阶段会完成以下4个阶段的检验动作: 1)文件格式验证 2)元数据验证(是否符合Java语言规范) 3)字节码验证(确定程序语义合法,符合逻辑) 4)符号引用验证(确保下一步的解析能正常执行)

1.2.2 准备

准备是连接阶段的第二步,主要为静态变量在方法区分配内存,并设置默认初始值。

1.2.3 解析

解析是连接阶段的第三步,是虚拟机将常量池内的符号引用替换为直接引用的过程。

1.3 初始化

初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。 注: 1)当有父类且父类为初始化的时候,先去初始化父类; 2)再进行子类初始化语句。

什么时候需要对类进行初始化? 1)使用new该类实例化对象的时候; 2)读取或设置类静态字段的时候(但被final修饰的字段,在编译器时就被放入常量池的静态字段除外static final); 3)调用类静态方法的时候; 4)使用反射Class.forName(“xxxx”)对类进行反射调用的时候,该类需要初始化; 5) 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化); 6) 被标明为启动类的类(即包含main()方法的类)要初始化; 7)当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上情况称为对一个类进行主动引用,且有且只要以上几种情况需要对类进行初始化。

再回过头来分析一开始的面试题: Singleton输出结果:1 0 原因:

1 首先执行main中的Singleton singleton = Singleton.getInstance(); 2 类的加载:加载类Singleton 3 类的验证 4 类的准备:为静态变量分配内存,设置默认值。这里为singleton(引用类型)设置为null,value1,value2(基本数据类型)设置默认值0 5 类的初始化(按照赋值语句进行修改): 执行private static Singleton singleton = new Singleton(); 执行Singleton的构造器:value1 ;value2 ; 此时value1,value2均等于1 执行 public static int value1; public static int value2 = 0; 此时value1=1,value2=0

Singleton2输出结果:1 1 原因:

1 首先执行main中的Singleton2 singleton2 = Singleton2.getInstance2(); 2 类的加载:加载类Singleton2 3 类的验证 4 类的准备:为静态变量分配内存,设置默认值。这里为value1,value2(基本数据类型)设置默认值0,singleton2(引用类型)设置为null, 5 类的初始化(按照赋值语句进行修改): 执行 public static int value2 = 0; 此时value2=0(value1不变,依然是0); 执行 private static Singleton singleton = new Singleton(); 执行Singleton2的构造器:value1 ;value2 ; 此时value1,value2均等于1,即为最后结果


二 类加载器

类加载器实现的功能是即为加载阶段获取二进制字节流的时候。

JVM提供了以下3种系统的类加载器:

  • 启动类加载器(Bootstrap ClassLoader): 最顶层的类加载器,负责加载 JAVA_HOMElib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
  • 扩展类加载器(Extension ClassLoader): 负责加载 JAVA_HOMElibext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
  • 应用程序类加载器(Application ClassLoader): 也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。

类加载器之间的层次关系如下:

照片来源:http://www.importnew.com/25295.html

类加载器之间的这种层次关系叫做双亲委派模型。 双亲委派模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不是以继承关系实现的,而是用组合实现的。

双亲委派模型的工作过程

如果一个类接受到类加载请求,他自己不会去加载这个请求,而是将这个类加载请求委派给父类加载器,这样一层一层传送,直到到达启动类加载器(Bootstrap ClassLoader)。 只有当父类加载器无法加载这个请求时,子加载器才会尝试自己去加载。

双亲委派模型的代码实现

双亲委派模型的代码实现集中在java.lang.ClassLoader的loadClass()方法当中。 1)首先检查类是否被加载,没有则调用父类加载器的loadClass()方法; 2)若父类加载器为空,则默认使用启动类加载器作为父加载器; 3)若父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass() 方法。

loadClass源代码如下:

代码语言:javascript复制
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先检查类是否被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 没有则调用父类加载器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父类加载失败,抛出ClassNotFoundException 异常后
            c = findClass(name);
        }
    }
    if (resolve) {
	    //5 再调用自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}

破坏双亲委派模型

双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。 若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了。 下面介绍两个例子来讲解破坏双亲委派模型的过程。

  1. JNDI破坏双亲委派模型 JNDI是Java标准服务,它的代码由启动类加载器去加载。但是JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。 为了解决这个问题,引入了一个线程上下文类加载器。 可通过Thread.setContextClassLoader()设置。 利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。
  2. Spring破坏双亲委派模型 Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。 那么Spring是如何访问WEB-INF下的用户程序呢? 使用线程上下文类加载器。 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。 利用这个来加载用户程序。即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。

三 附上Tomcat类加载架构:

(图片来源:http://lib.csdn.net/article/java/60356) Tomcat目录下有4组目录:

  • /common目录下:类库可以被Tomcat和Web应用程序共同使用;由 Common ClassLoader类加载器加载目录下的类库;
  • /server目录:类库只能被Tomcat可见;由 Catalina ClassLoader类加载器加载目录下的类库;
  • /shared目录:类库对所有Web应用程序可见,但对Tomcat不可见;由 Shared ClassLoader类加载器加载目录下的类库;
  • /WebApp/WEB-INF目录:仅仅对当前web应用程序可见。由 WebApp ClassLoader类加载器加载目录下的类库;
  • 每一个JSP文件对应一个JSP类加载器。

参考:《深入理解Java虚拟机》 MyBlog:https://nomico271.github.io/2017/07/07/JVM类加载机制/ 2017/07/05 In NJ

— PS 链接加上一个面试题,巩固下类的加载过程。

代码语言:javascript复制
public class B{
    public static B t1 = new B();
    public static B t2 = new B();
    {
        System.out.println("构造块");
    }
    static
    {
        System.out.println("静态块");
    }
    public static void main(String[] args)
    {
        B t = new B();
    }
}

以上代码输出结果是? 根据上文分析,类的加载过程是: (1)加载B类; (2)验证、准备(给静态属性t1、t2赋默认值null)、解析; (3)初始化:执行public static B t1 = new B();public static B t2 = new B();、static {}; 注意,我们之前说的类的加载顺序是 有父类先加载父类静态域,这里的静态域不仅包括static{} 代码块,还有 静态属性; 然后再执行构造块{}和构造方法; 所以在执行public static B t1 = new B();时,不会加载static{}代码块,否则就会重复执行,只会执行B的构造块和构造方法(默认的无参构造器),输出”构造块”; 执行public static B t2 = new B();时,也同样执行构造块和构造方法,输出”构造块”; 第三步再执行static{} 代码块,输出”静态快“; 最后执行main方法中的B t = new B();,输出”构造块”;

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/142316.html原文链接:https://javaforall.cn

0 人点赞