主题
JVM 面试题
一、jvm 的内存结构
分析 JVM 的内存结构,主要是分析 JVM 运行时数据存储区域。JVM 的运行时数据区主要包括:堆、栈、方法区、程序计数器等等。

1.程序计数器:
作用:当前线程的下一条JVM 字节码指令的执行地址,便于进行线程切换
特点:1.线程是私有的,保证了各线程不会相互影响
2.不会存在内存溢出
拓展题:
1.为什么要使用 PC 寄存器记录当前线程的执行地址?
答:因为 CPu 需要不停的切换各个线程,这个时候切换回来以后,就知道接着从哪里开始继续执行。
2.PC 寄存器为什么会被设定为线程私有?
答:多线程在一个特定的时间段只会执行其中某一个线程方法,CPU 会不停的做任务切换,这样必然会导致经常中断或者恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个 PC 寄存器,让每个线程都独立计算,不会相互影响。
2.java 虚拟机栈:
编译器可知的各种基本数据类型 (boolean、byte、char、short、int、float、long、double)、对象引用 (引用指针,并非对象本身)
栈是 java 方法执行的内存模型:
每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表 (包括参数)、操作栈、方法出口等信息。
每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的。
拓展题:
1.垃圾回收是否涉及栈内存?
答:不涉及。栈内存是 JVM 自动管理的,方法调用时入栈。方法结束后栈帧出栈,内存自动释放。
2.栈内存分配越大越好吗?
答:不是,因为物理内存的大小是一定的,所以栈内存越大,线程数就越小。
3.方法内的局部变量是否时线程安全的?
如果方法内的局部变量没有逃离方法的作用范围,他就是线程安全的。
如果局部变量引用了对象并逃离了方法的作用范围,它就有线程安全问题。
出现的问题:栈内存溢出:StackOverflowError
**方法过多导致栈内存溢出:**无递归边界的递归调用
栈帧过大导致栈内存溢出
案例:cpu 占用过高
步骤 1:top 命令查看进程 cpu 占用情况,锁定进程 id
步骤 2:用如下命令进一步定位占用出问题的线程 id:
ps H -eo pid,tid,%cpu | grep 进程 id
步骤 3:将 10 进制的线程 id 转成 16 进制,比如 32665 ==> 7F99
步骤 4:使用 jstack 命令查看进程信息,根据线程 id 7F99 查找线程,可看到线程的详细信息,进而定位出问题代码的行数 jstack 进程 id
3.本地方法栈
处理本地 native 方法(本地方法栈 是 JVM 给 本地方法 的调用提供内存空间的栈)
4.堆
通过 new 关键字创建的对象都会使用堆内存
堆是 线程共享 的,堆中对象需要 考虑线程安全问题
有 垃圾回收机制 ,堆中不再被引用的对象将被回收释放
出现的问题:堆内存溢出:java.lang.OutOfMemoryError:java heap space
例:
java
public static void main(String[] args) {
int i = 0;
ArrayList<String> list = new ArrayList<>();
String a = "BLU";
try {
while(true) {
list.add(a);
a = a+a;
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}java
java.lang.OutOfMemoryError: Java heap space
26
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at demo.Test.main(Test.java:385)5.方法区
方法区是线程共享的区,存储了类结构相关信息:类的成员变量、方法数据、方法和构造器代码,还有一个运行时常量池
方法区在虚拟机启动时被创建,方法区逻辑上是堆的组成部分
方法区也会内存溢出
二、常用垃圾回收算法
1.引用计数法
1.1 原理
假设有一个对象 A,任何对象对 A 进行引用,那么对象 A 的引用计数器 +1,当引用失效时,对象 A 的引用计数器-1,当对象 A 的引用计数器为 0 时,就说明对象 A 没用被引用,那么就可以进行回收。
1.2 优缺点
优点:
- 实时性高,不需要等到内存不足时,才开始回收,运行时判断对象计数器是否为 0,进行回收。
- 在垃圾回收过程中应用无需挂起,如果申请内存时,内存不足,则立即报 outofmember 错误。
- 区域性,更新对象计数器时,只会印象到该对象,不会扫描全部对象。缺点
- 每次对象都被引用,都需要去更新对象计数器,会增加时间开销。
- 浪费 CPU 资源,因为内存足够是,对象计数器任然在运行进行统计。
- 无法解决循环引用问题(最大缺点)循环引用如:对象 A 维护了一个成员属性,类型是对象 B,对象 B 中维护了一个成员属性,类型是对象 A,然后分别给这两个成员属性赋值,在将对象 A 赋值为 null,将对象 B 赋值为 null,这样对象 A 和对象 B 就都是 null,但是 a 和 b 存在引用关系,这样 a 和 b 永远不会被回收。
2.GC root - 可达性分析
https://www.pianshen.com/article/7854111205/
一、垃圾收集算法
1.1、标记-清除算法
首先标记所有需要回收的对象,在标记完成之后进行统计回收,标记过程即为可达性分析算法;然后清楚所有被标记的对象
缺点:效率低,标记和清楚效率都不高
空间问题,会产生大量不连续的内存碎片,导致大对象分配无法找到足够的空间

1.2、复制算法
将内存区域划分为两块,每次只使用一块,当这一块内存用完了,就将存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉
优点:解决了标记-清除算法的效率问题
缺点:浪费了部分内存;存活对象较多时效率低;如果不想浪费 50% 的空间还需要进行额外的空间担保

1.3、标记-整理算法
先标记,之后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

1.4、分代收集算法
根据对象存活周期的不同,将内存划分为新生代和年老代,根据各个年代的特点采取适当的算法,新生代中每次回收只有少量对象存活,采用复制算法;老年代存活率较高,没有额外空间对它进行分配担保,就必须使用标记-清除、标记-整理算法
三、双亲委派
4.1、java 类加载器
java
public static void main(String[] args){
//String类加载器null,引导类加载器是c++语言实现,所以打印出为null
System.out.println("String类加载器" + String.class.getClassLoader());
//DESKeyFactory加载器sun.misc.Launcher$ExtClassLoader@4617c264
System.out.println("jdk包下的DESKeyFactory的加载器" + DESKeyFactory.class.getClassLoader());
//类加载器sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("自定义类的加载器" + TestJDKClassLoader.class.getClassLoader());
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassLoader.getParent();
// the bootstrapLoader === null
System.out.println("the bootstrapLoader === " + bootstrapLoader);
//the extClassLoader === sun.misc.Launcher$ExtClassLoader@4617c264
System.out.println("the extClassLoader === " + extClassLoader);
//the appClassLoader === sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("the appClassLoader === " + bootstrapLoader);
System.out.println("bootstrapLoader加载以下文件");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i]);
}
System.out.println("extClassloader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}注意: appClassLoader 加载得包包含 extClassLoader
extClassLoader 加载得包包含 bootstrapClassLoader
bootstrapClassLoader 加载得包为 jre/lib 下得核心包
但是三者并非继承关系。
4.2、类加载器初始化过程
- sun.misc.Launcher 初始化使用了单例设计模式,保证一个 jvm 虚拟机内只有一个 sun.misc.Launcher 实例。
- 再 Launcher 构造器内部,创建了两个类加载器,分别是 ExtClassLoader 和 AppClassLoader
- JVM 默认使用 Launcher 得 getClassLoader() 方法返回的类加载器 AppClassLoader 的实例加载我们的应用程序
java
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
//launcher提前初始化好,类加载时及创建
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//创建扩展类加载器,再构造过程中讲父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//创建应用类加载器,注意var1变量为扩展类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
//双检索的单例模式创建扩展类加载器
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}4.3、双亲委派机制
4.3.1、双亲委派机制的流程

- 应用类加载器(在没有自定义类加载器时)根据类的全路径名检查此类是否已经被加载,如果加载过了就不需要在加载,直接返回
- 如果未加载过,则委托给父加载器扩展类加载器,父加载器检查此类是否加载过,如果加载过直接返回。
- 如果未加载则委托给引导类加载器,引导类加载器判断是否加载过,加载过,直接返回
- 如果未加载过,则查找 lib 下核心包,有么有指定指定类,有则加载,没有则委托给扩展类加载器
- 扩展类寻找指定类,有则加载,么有则委托给应用类加载器
- 应用类加载器寻找指定类进行加载
java
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 {
//如果未加载,则委托给父类,extClassLoader父未null,appClassLoader父类为extClassLoader
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();
//都对调用URLClassLoader的findClass方法在加载器的类路径里查找并加载类
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;
}
}4.3.2、为什么设计双亲委派机制
沙箱安全机制:防止 java 核心 api 被随意的篡改
避免重复加载:当父类已经加载,就没必要子类在加载一次,保证类被唯一加载
4.3.3、为什么不可以从父类先加载,之后再子类加载
如自定义类 User
双亲委派加载 User
第一次:appClassLoader -> extClassLoader -> bootstrapClassLoader ->extClassLoader ->appClassLoader
第二次:appClassLoader 直接返回
若从父类加载
第一次:bootstrapClassLoader ->extClassLoader ->appClassLoader
第二次:bootstrapClassLoader ->extClassLoader ->appClassLoader
第三次.......
要加载的自定义类比 java 核心类要多。
四、如何判断一个对象是否存活?(或者 GC 对象的判定)
判断一个对象是否存活的方法有两种:
引用计数法
所谓的引用计数法就是给每个对象设置一个引用计数器,每当有个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收。但是引用计数器有一个缺陷就是无法解决循环引用问题,例如:当对象 A 引用对象 B,对象 B 又引用对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成了无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
可达性算法 (引用链法)
可达性算法:该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。
在 java 中可以作为 GC Roots 对象的有以下几种:
- 虚拟机栈中引用的对象
- 方法区类静态属性引用对象
- 方法区常量池引用的对象
- 本地方法栈 JNI 引用的对象
虽然这些算法可以判定一个对象是否能被回收,但当满足上述条件时,一个对象不一定会被回收。当一个对象不可达 GC Root 时,这个对象不会立刻被回收,而是会出现一个死缓阶段,若要真正的回收则需要经历两次标记。如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize() 方法,当对象没有覆盖 finalize() 方法或者已被虚拟机调用过,那么就认为是没有必要的。如果该对象有必要执行 finalize() 方法,那么这改对象将会被放在一个称为 F-Queue 的等待队列中,虚拟机会触发一个 Finalize() 线程去执行,此线程是低优先级的,并且虚拟机不会承诺它一直到运行完,这是因为如果 finalize() 执行缓慢或者发生死锁,那么就会造成 F-Queue 队列一直等待,造成内存回收系统崩溃。GC 处于 F-Queue 中的对象进项第二次被标记,这时,该对象将被移除 " 即将回收集合 ",等待回收。