Java虚拟机 ,分享一波阿里、字节、腾讯、美团等精选大厂面试题

  1. 类或接口解析: 假设当前的类A通过符号X引用了类B,虚拟机会把代表类B的全限定名传递给A的类加载器去加载BB经过加载、验证、准备过程,在解析过程又可能会触发B引用的其他的类的加载过程,相当于一个类引用链的递归加载过程,整个过程只要不出现异常,B的就是一个加载成功的类或接口了,也就是可以获取到代表Bjava.lang.Class对象。在验证了A具备对B的访问权限后,就将符号引用X替换为B的直接引用。

  2. 字段解析: 解析未被解析过的字段,要先解析字段所属的类或接口的符号引用。如果类本身就包含了简单的名称和字段描述与目标字段相匹配,就直接返回这个字段引用;如果实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段;如果是继承自其他类的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

  3. 类方法解析:类方法解析和字段解析的方式类似,也是依据继承和实现关系从小到上搜索,只不过是先搜索类,后搜索接口。如果有简单名称和字段描述符都与目标相匹配的字段,就返回字段引用。

  4. 接口的方法解析: 与类方法解析类似,从小到上搜索接口(接口没有父类,只可能有父接口)。如果存在简单名称和字段描述符都与目标相匹配的字段,就返回字段引用。

初始化

类的初始化类加载过程的最后一步,在前面的过中,除了在加载阶段开发者可以自定义加载器之外,其余的动作都是完全有虚拟机主导和控制完成。到了初始化阶段,才真正开始执行类中定义的Java代码。

在准备阶段,类变量已经设置了系统要求的零值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中所有的类变量(static变量)和静态代码块(static{}块)中的语句合并生成的。编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态代码块中只能访问到定义在静态代码块之前的变量,定义在它之后的变量,在前面的静态代码块可以赋值,但是不能访问。


public class Test {

    static {

        number = 111;               // 可以赋值

        System.out.println(number); // 不能读取,编辑器或报错Illegal forward reference

    }

    static int number;

}

<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类的<clinit>()方法,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。所以,父类定义的静态代码块要先与子类的赋值操作。


class Parent {

    public static int A = 1;

    static {

        A = 2;

    }

}



class Sub extends Parent {

    public static int B = A;

    public static void main(String[] args) {

        System.out.println(Sub.B);

    }

}

<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。

类加载器

在之前的加载过程中,提到了类加载器通过一个类的全限定名来获取描述此类的二进制字节流,这个过程可以让开发中自定义类加载器来决定如何获取需要的字节流。那么,什么是类加载器呢?

对于任意一个Java类,都必须通过类加载器加载到方法区,并生成java.lang.Class对象才能使用类的各个功能,所以我们可以把类加载器理解为一个将class类文件转换为java.lang.Class对象的工具。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。也就是说,如果两个类“相等”,那么这两个类必须是被同一个虚拟机中的同一个类加载器加载,并且来自同一个class文件。

在Java当中,已经有3个预制的类加载器,分别是BootStrapClassLoaderExtClassLoader、AppClassLoader

  • BootStrapClassLoader: 启动类加载器,它是由C++来实现的,在Java程序中不能显氏的获取到。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下的类。

  • ExtClassLoader: 扩展类加载器,它是由sun.misc.Launcher$ExtClassLoader实现,负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库。开发者可以直接使用它。

  • AppClassLoader: 应用程序类加载器,由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。一般来说,开发者自定义的类就是由应用程序类加载器加载的。

ExtClassLoader作为类加载器,但它也是一个Java类,是由BootStrapClassLoader来加载的,所以,ExtClassLoader的parent是BootStrapClassLoader。但是由于BootStrapClassLoaderc++实现的,我们通过ExtClassLoader.getParent获取到的是null。同样地,AppClassLoader是由ExtClassLoader加载,AppClassLoader的parent是ExtClassLoader


public class Test {

    public static void main(String[] args) {

        ClassLoader cl = Test.class.getClassLoader();

        while (cl != null) {

            System.out.println(cl);

            cl = cl.getParent();

        }

    }

}

打印结果:


sun.misc.Launcher$AppClassLoader@232204a1

sun.misc.Launcher$ExtClassLoader@74a14482

同时我们可以定义自己的类加载器CustomClassLoader,那么它的parent肯定就是AppClassLoader了。类加载器的这种层次关系称为双亲委派模型。

类加载器

类加载器

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系不是以继承的关系来实现,而是都使用递归的方式来调用父加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

ClassLoader的源码:


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

            }



            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;

    }

}

先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,依次向上递归。若父类加载器为空则说明递归到启动类加载器了。如果从父类加载器到启动类加载器的上层次的所有加载器都加载失败,则调用自己的findClass()方法进行加载。

使用双亲委派模型能使Java类随着加载器一起具备一种优先级的层次关系,保证同一个类只加载一次,避免了重复加载,同时也能阻止有人恶意替换加载系统类。

自定义类加载器

一般地,在ClassLoader方法的loadClass方法中已经给开发者实现了双亲委派模型,在自定义类加载器的时候,只需要复写findClass方法即可。


public class CustomClassLoader extends ClassLoader {



    private String root;



    public CustomClassLoader(String root) {

        this.root = root;

    }



    @Override

    protected Class<?> findClass(String name) throws ClassNotFoundException {

        byte[] classData = loadClassData(name);

        if (classData == null) {

            throw new ClassNotFoundException();

        } else {

            return defineClass(name, classData, 0, classData.length);

        }

    }



    private byte[] loadClassData(String name) {

        String fileName = root + File.separatorChar

                + name.replace('.', File.separatorChar)

                + ".class";

        try {

            InputStream ins = new FileInputStream(fileName);

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            int bufferSize = 1024;

            byte[] buffer = new byte[bufferSize];

            int length;

            while ((length = ins.read(buffer)) != -1) {

                baos.write(buffer, 0, length);

            }

            return baos.toByteArray();

        } catch (IOException e) {

            e.printStackTrace();

        }

        return null;

    }

}

新建一个类com.xiao.U,编译成class文件,放到桌面,来测试一下:


public class Test {

    public static void main(String[] args) {

        CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\PC\\Desktop");

        try {

            Class clazz = customClassLoader.loadClass("com.xiao.U");

            Object o = clazz.newInstance();

            System.out.println(o.getClass().getClassLoader());

        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {

            e.printStackTrace();

        }

    }

}

打印结果:

\PC\Desktop");

    try {

        Class clazz = customClassLoader.loadClass("com.xiao.U");

        Object o = clazz.newInstance();

        System.out.println(o.getClass().getClassLoader());

    } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {

        e.printStackTrace();

    }

}

}




打印结果:





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值