jvm 类加载面试题目
1、Class.forName 和 ClassLoader.loadClass 都能加载类,这两者在加载类时的区别?
- Class.forName有重载方法可以指定是否需要初始化,而默认的方法初始化设置为true这会初始化类执行链接和初始化操作
- ClasaLoader是有类加载器的loadClass方法加载,传入的是false,只会执行连接操作,不会初始化操作
Class.forName()方法实际上也是调用的CLassLoader来实现的。
Class.forName(String className);这个方法的源码是:
@CallerSensitive public static Class> forName(String className) throws ClassNotFoundException { Class> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
最后调用的方法是forName0这个方法,在这个forName0方法中的第二个参数被默认设置为了true,这个参数代表是否对加载的类进行初始化,设置为true时会类进行初始化,代表会执行类中的静态代码块,以及对静态变量的赋值等操作。Class.forName加载类是将类进了初始化,而ClassLoader的loadClass并没有对类进行初始化,只是把类加载到了虚拟机中
2、什么叫类加载器
虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解析以及类初始化,最终形成可以被虚拟机直接使用的java类型(java.lang.Class对象)。
3、类的生命周期
- 加载过程:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中(方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
- 找到类文件(通过类的全限定名来获取定义此类的二进制字节流)
-
- 放入方法区(将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构)
- 开个入口(生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口)
- 验证过程:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,文件格式验证、元数据验证、字节码验证、符号引用验证
- 准备过程:正式为类属性分配内存并设置类属性初始值的阶段,这些内存都将在方法区中进行分配
- 解析阶段:虚拟机将常量池内的符号引用替换为直接引用的过程
- 初始化阶段:类初始化阶段是类加载过程的最后一步。初始化阶段就是执行类构造器()方法的过程
- 使用阶段:
- 卸载阶段:
4、类加载器
类加载器负责加载所有的类,同一个类(一个类用其全限定类名(包名加类名)标志)只会被加载一次。
- Bootstrap ClassLoader:根类加载器,负责加载java的核心类,它不是java.lang.ClassLoader的子类,而是由JVM自身实现
- Extension ClassLoader:扩展类加载器,扩展类加载器的加载路径是JDK目录下jre/lib/ext,扩展类的getParent()方法返回null,实际上扩展类加载器的父类加载器是根加载器,只是根加载器并不是Java实现的
- Application ClassLoader:应用程序类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径。程序可以通过getSystemClassLoader()来获取系统类加载器。系统加载器的加载路径是程序运行的当前路径。
双亲委派模型的工作过程
- 先查找当前ClassLoader是否加载过此类,有就返回;
- 如果没有,查询父ClassLoader是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
- 如果整个类加载器体系上的ClassLoader都没有加载过,才由当前ClassLoader加载(调用findClass),整个过程类似循环链表一样。
双亲委托机制的作用
- 共享功能:可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加载,一些framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
- 隔离功能:java核心类库的纯净和安全,防止恶意加载。
如何打破双亲委派模型?
- 双亲委派模型的逻辑都在loadClass()中,重写loaderClass()、findClass()
- 系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载
打破双亲委派
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。
比如JDBC就打破了双亲委派机制。它通过Thread.currentThread().getContextClassLoader()得到线程上下文加载器来加载Driver实现类,从而打破了双亲委派机制。
自定义ClassLoader
- loadClass(String name,boolean resolve):根据指定的二进制名称加载类
- findClass(String name): 根据二进制名称来查找类
- 直接使用或继承已有的ClassLoader实现:java.net.URLClassLoader、java.security.SecureClassLoader、 java.rmi.server.RMIClassLoader
- 在调用loadClass(),会先根据委派模型在父加载器中加载,如果加载失败,则会调用自己的findClass方法来完成加载
5、引起类加载操作的五个行为
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令
- 反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
- 子类初始化的时候,如果其父类还没初始化,则需先触发其父类的初始化
- 虚拟机执行主类的时候(有 main(string[] args))
- JDK1.7 动态语言支持
6、Java对象创建时机
- 使用new关键字创建对象
- 使用Class类的newInstance方法(反射机制)
- 使用Constructor类的newInstance方法(反射机制)
- 使用Clone方法创建对象
- 使用(反)序列化机制创建对象
深入原理
类的生命周期
其实理解之后,基本上就不会再忘了。
加载
加载主要做三件事:
- 找到类文件(通过类的全限定名来获取定义此类的二进制字节流)
- 放入方法区(将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构)
- 开个入口(生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口)
总的来讲,这一步就是通过类加载器把类读入内存。需要注意的是,第三步虽然生成了对象,但并不在堆里,而是在方法区里。
连接
连接分为三步,一般面试都比较喜欢问准备这一步。
校验
顾名思义,检查Class文件的字节流中包含的信息是否符合当前虚拟机的要求。
准备
这一步中将为静态变量和静态常量分配内存,并赋值。
需要注意的是,静态变量只会给默认值。比如下面这个:
public static int value = 123;
此时赋给value的值是0,不是123。
静态常量(static final修饰的)则会直接赋值。比如下面这个:
public static final int value = 123;
此时赋给value的值是123。
解析
解析阶段就是jvm将常量池的符号引用替换为直接引用。
恩......啥是常量池?啥是符号引用?啥是直接引用?
常量池我们放在jvm内存结构里说。先来说下什么是符号引用和直接引用。
符号引用和直接引用
假设有一个Worker类,包含了一个Car类的run()方法,像下面这样:
class Worker{ ...... public void gotoWork(){ car.run(); //这段代码在Worker类中的二进制表示为符号引用 } ...... }
在解析阶段之前,Worker类并不知道car.run()这个方法内存的什么地方,于是只能用一个字符串来表示这个方法。该字符串包含了足够的信息,比如类的信息,方法名,方法参数等,以供实际使用时可以找到相应的位置。
这个字符串就被称为符号引用。
在解析阶段,jvm根据字符串的内容找到内存区域中相应的地址,然后把符号引用替换成直接指向目标的指针、句柄、偏移量等,这之后就可以直接使用了。
这些直接指向目标的指针、句柄、偏移量就被成为直接引用。
初始化
类的初始化的主要工作是为静态变量赋程序设定的初值。
还记得上面的静态变量吗:
public static int value = 123;
经过这一步,value的值终于是123了。
类初始化的条件
Java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:
- 使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。
- 通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。
- 当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。
- 当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。
- 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
除了以上这五种情况,其他任何情况都不会触发类的初始化。
比如下面这几种情况就不会触发类初始化:
- 通过子类调用父类的静态字段。此时父类符合情况一,而子类不符合任何情况。所以只有父类被初始化。
- 通过数组来引用类,不会触发类的初始化。因为new的是数组,而不是类。
- 调用类的静态常量不会触发类的初始化,因为静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类。
现在,我们可以回答文章开头提出的问题了。尽量在理解的基础上回答,不需要死记硬背。
- 什么是类的加载?
JVM把通过类名获得类的二进制流之后,把类放入方法区,并创建入口对象的过程被称为类的加载。经过加载,类就被放到内存里了。
- 讲一下JVM加载一个类的过程
问题1。不过这里也可以问下面试官是不是想问类的生命周期。如果是问类的生命周期,可以回答有”加载、连接、初始化、使用、卸载“五个阶段,连接又可以分为”校验、准备、解析“三个阶段。
- 什么时候会为变量分配内存?
在准备阶段为静态变量分配内存。
- JVM的类加载机制是什么?
双亲委派机制,类加载器会先让自己的父类来加载,父类无法加载的话,才会自己来加载。
- 双亲委派机制可以打破吗?为什么
可以打破,比如JDBC使用线程上下文加载器打破了双亲委派机制。原因是JDBC只提供了接口,并没有提供实现。这个问题可以再看下引用文献的内容。



