从一道面试题来认识Java类加载过程 - Go语言中文社区

从一道面试题来认识Java类加载过程


下边是一道笔试题:

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
    private SingleTon() {
        count1++;
        count2++;
    }
    public static SingleTon getInstance() {
        return singleTon;
    }
}
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

我想大部分人的第一答案都是:

count1=1
count2=1

其实它的正确答案是:

count1=1
count2=0

为什么会这样呢?认真看完下边的讲解,就会明了。

一、类加载时机

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
在这里插入图片描述
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

二、什么时候开始初始化
  1. 创建类的实例的时候;
  2. 访问类的静态变量(除常量【被final修饰的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译;
  3. 访问类的静态变量;
  4. 反射,如(Class.forName(“my.xyz.Test”));
  5. 当初始化一个类时,发现其父类还未初始化,则先触发父类的初始化;
  6. 虚拟机启动时,定义了main()方法的那个类先初始化
三、类加载过程
  1. 加载, 在加载阶段虚拟机需要完成三件事
    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 将字节流所代表的的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  2. 验证, 验证就是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全(java文件本身是相对安全的,但是改为Class文件后可能危害到虚拟机或程序的安全)
    • 文件格式验证:验证字节流是否符合Class文件格式的规范
    • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求
    • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
    • 符号引用验证:确保解析动作能正确执行。
  3. 准备, 准备阶段是正式为类静态变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都在方法区中进行分配
    • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中
    • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值
public static int value = 123 //在准备阶段 value的值是 0 并不是123

如果属性有Constant Value 属性( 被static和final修饰),那么在准备阶段变量就会被初始化为所指定的值

public static final int value = 123 // 准备阶段value 的值为123
  1. 解析, 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
  2. 初始化, 为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
    在Java中对类变量进行初始值设定有两种方式:
    ①声明类变量是指定初始值
    ②使用静态代码块为类变量指定初始值

JVM初始化步骤:
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句

看到这里,我们再回头分析上边的例子:
1、SingleTon singleTon = SingleTon.getInstance();调用了类的SingleTon调用了类的静态方法,触发类的初始化
2、类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0
3、类初始化,为类的静态变量赋值和执行静态代码快。singleton赋值为new SingleTon()调用类的构造方法
4、调用类的构造方法后count=1;count2=1
5、继续为count1与count2赋值,此时count1没有赋值操作,所有count1为1,但是count2执行赋值操作就变为0

四、类加载器

把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。

  • 启动类加载器: 负责加载%JAVA_HOME%bin目录下的所有jar包,或者是-Xbootclasspath参数指定的路径;
  • 扩展类加载器: 负责加载%JAVA_HOME%binext目录下的所有jar包,或者是java.ext.dirs参数指定的路径;
  • 应用程序类加载器: 负责加载用户类路径上所指定的类库,如果应用程序中没有自定义加载器,那么此加载器就为默认加载器。
    在这里插入图片描述
    那么问题来了,如果同时存在两个或多个全限定名完全一样的情况,该如何选择类呢,这就是双亲委派机制要做的工作。

双亲委派机制的原理:
1-类加载器收到类加载的请求;
2-把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
3-启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。
4- 只有当 父类加载器 反馈 自己无法完成该加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己去加载;

参考:https://www.cnblogs.com/javaee6/p/3714716.html

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/zx1293406/article/details/104633295
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-04-19 13:18:46
  • 阅读 ( 1188 )
  • 分类:面试题

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢