java面试问题总结


类加载机制


虚拟机把class文件加载到内存,并对数据进行校验,转换解析和初始化,形成虚拟机可以直接使用的Java类型,即java.lang.Class

类加载的流程


类从被加载到虚拟机内存开始到类使用完毕卸载出内存这一过程为类的生命周期,整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析为连接阶段。如图所示:

1、加载Loading

加载类的字节码文件以及二进制文件,通过完整的类路径查找此类字节码文件(class文件即二进制文件)。将二进制文件的静态存储结构转化为方法区运行时数据结构,并利用二进制流文件创建一个Class对象存储在Java堆中。

2、链接Linking

链接包含3部分:验证,准备,解析

  1. 验证(Verification):确保被加载类的正确性。为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

  1. 文件格式验证:格式验证就是对文件是否是0xCAFEBABE开头、class文件版本等信息进行验证,确保其符合JVM虚拟机规范。

  1. 元数据验证:元数据验证是对源码语义分析的过程,验证的是子类继承的父类是否是final类;如果这个类的父类是抽象类,是否实现了其父类或接口中要求实现的所有方法;子父类中的字段、方法是否产生冲突等,这个过程把类、字段和方法看做组成类的一个个元数据,然后根据JVM规范,对这些元数据之间的关系进行验证。所以,元数据验证阶段并未深入到方法体内。

  1. 字节码验证:字节码主要是对方法体内部的代码的前后逻辑、关系的校验,例如:字节码是否执行到了方法体以外、类型转换是否合理等。

  1. 符号引用验证:符号引用的验证其实是发生在符号引用向直接引用转化的过程中,而这一过程发生在解析阶段。

  1. 准备(Preparation):准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。(如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成;如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成)

  1. 解析(Resolution):把类中的符号引用转化为直接引用(比如说方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置)。解析包含:类或接口的解析、字段解析、类方法解析、接口方法解析。

3、初始化

在链接的准备阶段,类的静态变量已赋过一次初始值(默认值),而在初始化阶段,则是为静态变量赋指定值。例子:在准备阶段num1和num2的初始值为0,在初始化阶段,会将num1设置为10,num2设置为20。

pubilcclassTest{

privatestaticintnum1=10;

privatestaticintnum2=20;

}

  • 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>初始化方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用;

  • 编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;

  • 如果超类还没有被初始化,那么优先对超类初始化,但在<clinit>方法内部不会显示调用超类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的超类<clinit>方法已经被执行。

  • JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用静态内部类实现线程安全的单例模式)

  • 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类<clinit>方法;

何时出发初始化?

  • 为一个类型创建一个新的对象实例时(比如new、反射、序列化)

  • 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)

  • ··············调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式

  • 调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)

  • 初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)

  • JVM启动包含main方法的启动类时。

类加载器


双亲委派

类加载时首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达BootstrapClassLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。如图所示:

类加载时源代码

protectedClass<?>loadClass(Stringname, booleanresolve)
        throwsClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?>c=findLoadedClass(name);
            if (c==null) {
                longt0=System.nanoTime();
                try {
                    if (parent!=null) {
                        c=parent.loadClass(name, false);
                    } else {
                        c=findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundExceptione) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
​
                if (c==null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    longt1=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);
            }
            returnc;
        }
    }

打破双亲委派

为什么要打破双亲委派:

mysql的MYSQL CONNECROR,所有这就有个问题,DriverManger要加载各个Driver接口实现类,然后进行管理,但是DriverManager是由启动类加载器进行加载的,而这个启动类加载器默认值加载JAVA_HOME下面的lib,所以在不打破双亲委派的情况下启动类加载器会尝试加载Driver接口实现类,但我们真正要加载的是各个实现类,需要有系统类加载器进行加载,这个时候就需要启动类加载器委托系统类加载器去加载Driver实现类,从而破坏了双亲委派。

1、重写类加载逻辑

自定义类加载,重写loadClass方法,该方法可以指定类通过什么类加载器进行加载。

2、SPI机制

全称:Service Provider Interface,是一种服务发现机制。

Java SPI全称Service Provider Interface(服务提供接口),是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制.例如jdbc就是spi打破双亲委派的例子

3、OSGI热部署 热更新

是OSGi联盟指定的一个基于Java语言的动态模块化规范,这个规范最初是由Sun、IBM、爱立信等公司联合发起,目的是使服务提供商通过住宅网管为各种家用智能设备提供各种服务,后来这个规范在Java的其他技术领域也有不错的发展,现在已经成为Java世界中的“事实上”的模块化标准,并且已经有了Equinox、Felix等成熟的实现。OSGi在Java程序员中最著名的应用案例就是Eclipse IDE

相关面试题

一个自于网易面试官的一个问题,一个类的静态块是否可能被执行两次。

答案:如果一个类,被两个 osgi的bundle加载, 然后又有实例被初始化,其静态块会被执行两次

什么是运行时数据区


java内存布局介绍

方法区:线程共享,创建进程时就创建方法区,方法去是逻辑堆上的一部分,所以也称为非堆

运行时常量池:是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

堆区:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值