JVM面试题这一篇就够了
什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。
一旦Java代码被编译为Java字节码,便可以在不同平台上的Java虚拟机上运行。
不同平台用不同的JVM,因此JDK和JRE也不同
Java代码是怎么运行的?
Java代码被编译为Java字节码,在Java虚拟机上运行。
Java虚拟机将运行时内存区域划分为五个部分,分别为方法区、堆、PC寄存器、Java方法栈 和 本地方法栈。Java程序编译而成的 class文件,需要先加载至方法区中,方能在Java虚拟机中运行。
为了提高运行效率,标准JDK中的HotSpot虚拟机采用的是一种混合执行的策略。
解释执行 :在执行时才翻译成机器指令,无需保存不占内存。
即时编译 (just-in-time compilation,JIT):将其中反复执行的热点代码,以方法为单位进行即时编译,翻译成机器码后直接运行在底层硬件之上。HotSpot 装载了多个不同的即时编译器。
- Client Complier(C1):更高的编译速度
- Server Complier(C2):更好的编译质量
但即使编译类似预编译,编译之后的指令需要保存在内存中,这种方式吃内存,按照二八原则这种混合模式最恰当。
类加载
类的加载,由类加载器完成:
- 通过类的全限定名来获得二进制字节流
- 将这个字节流所代表的静态存储结构转换成方法区运行时的数据结构
- 并在堆内存区创建一个Java.lang.Class 对象,作为对方法区中这个数据结构的访问入口
类加载器分类:
- Bootstrap ClassLoader : C++实现,加载java核心API, jre/lib/rt.jar
- ExtClassLoader: 加载扩展API,jre/lib/ext 目录中 ,如 javax.*
- APPClassLoader或SystemClassLoader:加载ClassPath下定义的class
- CustomClassLoader:应用程序根据自身需要自定义的ClassLoader
类加载过程采用父类委托机制,更好的保证了java平台的安全性(更优先加载jre的class文件),类的加载首先请求父类的加载器加载,父类加载器无法加载该类时才尝试从自己的类路径中加载该类。(父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码)
类的生命周期
当Java程序需要使用某个类时,JVM要保证这个类被加载、连接(校验、准备、解析)和初始化。
- 类的加载:
- 通过类的全限定名来获得二进制字节流
- 将这个字节流所代表的静态存储结构转换成方法区运行时的数据结构
- 并在堆内存区创建一个Java.lang.Class 对象,作为对方法区中这个数据结构的访问入口
- 连接校验:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。(文件格式验证,元数据验证,字节码验证,符号引用验证)
- 准备:为静态成员变量分配内存并设置默认的初始值(不是等于之后的真实值),这些变量所使用的内存都将在方法区中进行分配。
假设定义类变量:public static int value=3; 在准备阶段后value为0,将value=3的指令是存放于构造器方法之中的,在初始化阶段才会执行。如果同时被 final 修饰,则在准备阶段就会value赋值为3
- 解析:将常量池内的符号引用替换为直接引用(也可能发生在初始化之后)
- 初始化:先初始化父类,对 static修饰的静态变量和静态代码进行初始化,并初始化程序员设置的变量值。
- 生命周期结束:
- 执行了System.exit() 方法。
- 程序正常执行结束。
- 程序在执行过程中遇到了异常或错误而异常终止。
- 由于操作系统出现错误而导致java虚拟机进程终止。
Java 对象的创建过程
当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的 实例变量(非静态变量) 及其 从父类继承过来的实例变量 (即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。
在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。
在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。
在继承中代码的执行顺序为:
- 父类静态对象,父类静态代码块
- 子类静态对象,子类静态代码块
- 父类非静态对象,父类非静态代码块
- 父类构造函数
- 子类非静态对象,子类非静态代码块
- 子类构造函数
1-2为类的初始化完成,3-6为类的实例化,即一个对象的初始化时完成。
获取对象的方式
A a = (A)Class.forName(“pacage.A”).newInstance();
A a = new A();
是一样的效果。
newInstance( )
是一个方法,而new
是一个关键字。创建对象的方式不一样,前者是使用类加载机制,后者是创建一个新类。Class下的newInstance()
的使用有局限,因为它生成对象只能调用无参的构造函数,而使用 new
关键字生成对象没有这个限制。newInstance()
实际上是把new
这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。 这样可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。
Class.forName(className)
方法,内部实际调用的方法是 Class.forName(className,true,classloader);
第2个boolean
参数表示类是否需要初始化, Class.forName(className)
默认是需要初始化。一旦初始化,就会触发目标对象的 static
块代码执行。
ClassLoader.loadClass(className)
方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);
第2个 boolean
参数,表示目标对象是否进行链接,false
表示不进行链接,可以不进行链接意味着不进行包括初始化等一系列步骤,那么静态块和静态对象就不会得到执行。
为什么在加载数据库驱动包的时候用的是Class.forName( ),却没有调用newInstance( )?newInstance()
方法,会保证这个类加载、连接和(初始化)。
JDBC Driver源码如下,因此使用Class.forName(classname)
才能在反射回去类的时候执行static
块。
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
所以在静态初始化器的中已经进行了注册,所以我们在使用JDBC时只需Class.forName(XXX.XXX);
就可以了。