Skip to content

JVM 面试题

一、jvm 的内存结构

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

1617330932455-77a586d2-0da4-407b-9ee9-bfceca8a05c5.png

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、标记-清除算法

首先标记所有需要回收的对象,在标记完成之后进行统计回收,标记过程即为可达性分析算法;然后清楚所有被标记的对象

缺点:效率低,标记和清楚效率都不高

空间问题,会产生大量不连续的内存碎片,导致大对象分配无法找到足够的空间

1601089444571-619435ba-c6e4-4a3d-945b-cbc93e8fdf2b.png

1.2、复制算法

将内存区域划分为两块,每次只使用一块,当这一块内存用完了,就将存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉

优点:解决了标记-清除算法的效率问题

缺点:浪费了部分内存;存活对象较多时效率低;如果不想浪费 50% 的空间还需要进行额外的空间担保

1601089532521-126d647a-019a-4d46-88ea-ba8905a7bdd2.png

1.3、标记-整理算法

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

1601089598338-a3ac9a4d-a76f-47e4-9fd1-e4cfc30e8d18.png

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、双亲委派机制的流程

1600240065388-a21493fa-5c1a-4d59-a853-ea35caeaf63f.png

  • 应用类加载器(在没有自定义类加载器时)根据类的全路径名检查此类是否已经被加载,如果加载过了就不需要在加载,直接返回
  • 如果未加载过,则委托给父加载器扩展类加载器,父加载器检查此类是否加载过,如果加载过直接返回。
  • 如果未加载则委托给引导类加载器,引导类加载器判断是否加载过,加载过,直接返回
  • 如果未加载过,则查找 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 对象的有以下几种:

  1. 虚拟机栈中引用的对象
  2. 方法区类静态属性引用对象
  3. 方法区常量池引用的对象
  4. 本地方法栈 JNI 引用的对象

虽然这些算法可以判定一个对象是否能被回收,但当满足上述条件时,一个对象不一定会被回收。当一个对象不可达 GC Root 时,这个对象不会立刻被回收,而是会出现一个死缓阶段,若要真正的回收则需要经历两次标记。如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize() 方法,当对象没有覆盖 finalize() 方法或者已被虚拟机调用过,那么就认为是没有必要的。如果该对象有必要执行 finalize() 方法,那么这改对象将会被放在一个称为 F-Queue 的等待队列中,虚拟机会触发一个 Finalize() 线程去执行,此线程是低优先级的,并且虚拟机不会承诺它一直到运行完,这是因为如果 finalize() 执行缓慢或者发生死锁,那么就会造成 F-Queue 队列一直等待,造成内存回收系统崩溃。GC 处于 F-Queue 中的对象进项第二次被标记,这时,该对象将被移除 " 即将回收集合 ",等待回收。

最近更新