关于JVM中的几大面试题
一、介绍
本文介绍JVM
中的几个面试题,十分有用
主要有几题
二、答疑
1)Java类的加载过程
简单来说,可以这样理解分类
- 类的加载
- 获取二进制文件,将
.class
文件加载至JVM
中
- 获取二进制文件,将
- 类的连接,验证,准备,解析合称连接
- 分配空间
- 静态属性赋值(赋初始值,而不是我们给予的值,如
int
是0,包装类为null
)
- 类的初始化,(是初始化,不是实例化)
- 静态属性赋值,这时候就是赋我们给予的值了
什么是符号引用,什么又是直接引用 可以这样进行理解,我们有一个
A
类和B
类,A
类中使用到了B
类 在字节码中,会用一个符号代表这是B
类,这就是符号引用 而在B
类进行类加载后,JVM
成功的加载了这个B
类,使得堆内存中有对应的B.class
的对象,同时方法区中有静态方法与属性。 这个时候,A
类就会将之前的符号引用,改为直接引用,设置为上面堆内存的B.class
对象,或者方法区中的静态方法与属性类加载的时机
- 实例化类对象
- 调用类的静态方法
- 使用类的静态属性
2)双亲委派机制是什么
在了解双亲委派机制之前,我们先设想一个问题,就是如果我们用户自己写一个String
这样一个的类,会出现什么样的情况?
这个问题说简单也简单,说复杂就比较复杂了,这个问题正好是由双亲委派机制来进行解决的。
在了解双亲委派机制之前,我们先得了解几个ClassLoader
类加载器
类加载器 | 说明 | 加载类的范围 |
---|---|---|
Bootstrap ClassLoader | 启动类加载器,最顶层的类加载器,这个加载器,Java中不能获取,返回的是一个null | <JAVA HOME>/lib |
Extension ClassLoader | 扩展类加载器 | <JAVA HOME>/lib/ext |
Application ClassLoader | 应用程序类加载器,也是我们最常用的类加载器 | classpath/java.class.path |
User ClassLoader | 用户自定义的类加载器 | 任意来源的类 |
好的,当了解完上面的四种类加载器之后,我们将进行验证,看下面代码
代码语言:javascript复制package com.banmoon.parentsappoint;
public class ParentsAppointTest {
public static void main(java.lang.String[] args) {
System.out.println("java.lang.String:" "abc".getClass().getClassLoader());
System.out.println("com.banmoon.parentsappoint.String:" String.class.getClassLoader());
}
}
为什么,他们的类加载器是不同的呢。有人说了,是因为类加载器本身就是有不同的加载类职责范围。
那么当我们进行类加载的时候,程序怎么知道这个类要用什么类加载器。然而就是这段不同的类,确定使用不同类加载器的过程,就是我们将的双亲委派机制。
我们先看这段代码,正是双亲委派机制的代码,在ClassLoader.java
中可以找到这段代码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,先检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// parent是父亲加载器,这里仅仅是逻辑层面上的,并不是指继承方面的父类
if (parent != null) {
// 如果父亲加载器不为空,则先交给父亲加载器进行类加载
c = parent.loadClass(name, false);
} else {
// 如果父亲加载器为空,那说明接下来是Bootstrap ClassLoader了,直接交给特殊的方法进行加载
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;
}
}
这就是双亲委派机制,下面可以画一个图
可以看到,类加载的时候,永远都是Bootstrap ClassLoader
(启动类加载器)尝试去加载
当然,如果类不合适,将会向下进行委派加载
上面的这种行为可以这样概括,向上检查,向下委托加载
3)JVM内存模型
下面这个就是JVM的内存模型,有些细节没有完全画出来,后续会补上
需要讲一下,其中的这些是什么意思
- 堆内存:这个先简单概括一下,基本对象的创建都会存储在该内存区域
- 方法区:方法区是一个概念,然而不同的虚拟机会有不同的实现。在这里,我们只需要关注方法区究竟是干什么的即可。在方法区中,主要存储以下三个内容,
- 类签名:记录权限名,访问权限,版本号
- 属性:记录类属性,访问权限
- 方法:方法字节码记录
- 常量池:在上面讲到过符号引用改为直接引用,那些这一块常量池就是存储这一块东西的。可以理解为,他是由字节码中一个指针指向另一个字节码。比如说定义了一个
String
的属性,那么在类加载的连接阶段,常量池中会存储这么一个指针常量。 - 运行时常量池:这是再上面模型图中没有体现的,需要单独讲解。它里面主要存储两个内容。可以看到的是,运行时常量池是包括了常量池的。所以这一块知识点,不需要额外去记忆。
- 运行时产生的:如字符串,如上面的符号引用改为直接引用
- 编译期间产生的:主要是字节码中定义的静态信息,各个类的
Class
对象。还有就是开发者编写的静态变量。
- 堆:对象主要存储在堆内存中,这里也是垃圾回收
GC
的主战场。下面篇幅会提到 - 程序计数器:用来存储字节码的指令地址,提供给执行引擎去读取执行。简单的来说就是执行到哪一步了
- 虚拟机栈:换个名字叫
Java
方法栈,这样好理解一下。Java
在调用方法时,会将字节码方法入栈,这个东西叫做栈帧。栈这种数据结构,就是先入后出。类似的,一个A
方法压入栈,这个方法调用一个B
方法,就会将B
方法压入栈。结构展示A
在最底下,B
在上。在结束的时候,是B
方法栈帧先结束,然后才是A
方法的栈帧。符合先入后出原则。在栈帧结构内部,我们可以如下进行划分,分别是- 局部变量表:
- 主要存储方法的参数、定义在方法内的局部变量,包括八大基本数据类型,对象的引用地址,返回值地址。
- 局部变量表中存储的基本单元为变量槽(Sot),32位(4字节)以内的数据类型占一个slot,64位(long,double)的占两个slot。
- 局部变量表是一个数字数组,byte、short、char都会被转化为int,boolean类型也会被转化为int,0代表alse、非0代表true。
- 局部变量表的大小是在编译期间决定下来的,所以在运行时它的大小是不会变的。
- 局部变量表中含有直接或者间接指向的引用类型变量时,不会被垃圾回收处理。
- 操作数栈:除了上面的局部变量表,还有一个操作数栈。这个操作数栈是在方法执行的过程中,根据字节码的指令,将上面的变量入栈,再执行指令。如执行复制、交换、求和等操作
- 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
- 方法出口:存放调用该方法的计数器的值;有两种情况,一种是方法正常返回,另一种是方法出先异常的返回。存储在一个异常处理表,方便再发生异常的时候找到处理异常的代码。
- 局部变量表:
- 本地方法栈:由于
Java
是由C 语言
编写的,里面肯定会调用到C
,故本地方法栈就是存储的是调用C
方法时的变量存储。
三、最后
我是半月,你我一同共勉!!!