Java面试必备,JVM核心知识点总结

Java
267
0
0
2023-08-28

JVM 基础

Java面试必备,JVM核心知识点总结

Java面试必备,JVM核心知识点总结


程序计数器(Program Counter Register)CPU中的寄存器

作用: 记住下一条JVM指令

特点:

  • 线程 私有
  • 唯一一个不会出现内存溢出的区域

虚拟机 栈(Java virtual mechine Stack)

线程私有

Java面试必备,JVM核心知识点总结

每个线程运行时所需要的内存

每个栈由多个栈帧Frame组成,对应着每次方法调用时占的内存

每个线程只能有一个当前活动栈帧,对应着当前正在执行的方法

三个问题

一、垃圾回收是否涉及栈内存?

不会:垃圾回收的是堆内存的无用数据,栈帧调用完会自动弹出栈,不需要垃圾回收。

二、栈内存分配越大越好?

不是,物理内存一定,栈内存分配越大,线程数就会越少,并且较大的栈内存并不会提升运行速度,只不过像递归调用可以存储更多栈帧。可通过以下虚拟机参数调节栈内存大小

Java面试必备,JVM核心知识点总结

三、方法内的局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用访问,则线程安全

如果局部变量引用了对象,并逃离了方法的作用访问(如返回对象变量,如对象作为参数传入方法)需要考虑线程安全问题

栈内存溢出

两种情况,会抛出 Java .lang.StackOverflowError的异常

栈帧过多(一般出现在递归)

Java面试必备,JVM核心知识点总结

栈帧过大,栈内存分配较小(一般不容易出现)

线程运行诊断

CPU占用过高,定位哪个线程引起的

Linux系统下top命令找出对应的进程PID

ps H -eo pid,tid.%CPU | grep 进程ID (用ps命令进一步定位哪个线程占用CPU过高)

jstack 进程id 列出当前Java进程下的所有的线程,根据十六进制线程id进行定位线程

程序很长时间没有获得结果(常见死锁问题)

jstack 进程id 列出当前Java进程下的所有的线程,根据十六进制线程id进行定位线程

本地方法栈

特点:线程私有

作用:Java代码不能很好地与操作系统打交道,因此采用native关键字,API调用C/C++代码,实现对操作系统相关API调用,这些方法(如Object类的notyAll,wait方法)运行需要单独的栈内存空间,称之为本地方法栈。

Heap堆,通过使用new关键字创建对象都会使用堆内存

特点:

线程共享,需要考虑线程安全问题

有垃圾回收机制(堆中不在引用的对象)

堆内存溢出

会抛出异常OutOfMemoryError:Java heap space

调节堆空间内存大小 -Xmx内存大小

方法区(概念)

特点:线程共享

定义:存储包含类的结构相关一些信息(包含运行时常量池,类的成员变量,方法数据,成员方法构造器方法的代码部分)和类的加载器的信息,

方法区在虚拟机启动时被创建。

JDK1.6 与JDK 1.8 的对比

  • 1.8方法区的实现是元空间,占用的是操作系统的内存,并将常量池中的StringTable移到堆内存中。
  • 1.6方法区的实现是永久代,占用的是堆内存。

Java面试必备,JVM核心知识点总结

内存溢出

也会抛出异常OutOfMemoryError:Metaspace 1.8及以后,OutOfMemoryError:PermGen space 1.7及之前

调节方法区内存大小参数

-XX:MaxMetaspaceSize内存大小 JDK1.8及以后

-XX:MaxPermSize内存大小 JDK1.7及以前

出现场景

spring或mybatis都会用到cglib动态代理产生很多代理类,可能会出现OOM。

运行时常量池

常量池:就是一张表,虚拟机根据这张表找到需要的执行的类名,方法名,参数类型,字面量等信息

运行时常量池 :位于*.class文件中,当类被加载,常量池信息就会加载到运行时常量池中,并将里面的符号转换成内存地址。

Java面试必备,JVM核心知识点总结

StringTable串池:

位于堆内存中, JDK1.6及以前位于常量池中,因为大量运用,老年代回收效率低,后续放在队中。

StringTable串池是延迟加载的,起初为空,用到才会转换新的String对象,且数组中对象不会重复转换【唯一】。

Java面试必备,JVM核心知识点总结

s1、s2是变量,引用的值可能改变,因此必须运行时进行StringBuilder拼接,返回一个新的对象

  • s1+“b”,s2+“a”,s1+s2都与”ab”不相等,原因是不同对象

Java面试必备,JVM核心知识点总结

此时”a”+”b”值固定,已经在编译期间进行优化完成,不需要运行时解析变量拼接。

Java面试必备,JVM核心知识点总结


public static void main(String[] args) {
    //串池对象【”a”】
    //堆对象【new String(“a”)】
    System.out.println(new String(“a”)==”a”); //false 说明串池与堆中对象不是同一个
}
public static void main(String[] args) {
    //串池【”a”,”b”】一旦出现”a”就会创建串池对象
    //堆池【ab,new String(“a”),new String(“b”)】
    String ab =new String(“a”)+new String(“b”);
    String abi = ab.intern(); //尝试将ab变量String对象放入串池,并返回串池的对象名为abi
    //串池【”a”,”b”,”ab”】 因为串池中没有此”ab”值的常量对象,因此放入成功
    System.out.println(ab ==”ab”); //true ab变量对象已在串池中
    System.out.println(abi ==”ab”); //true abi变量引用返回的串池对象”ab”
}
public static void main(String[] args) {
    String x =”ab”; //将”ab”放入串池
    //串池【”a”,”b”,”ab”】一旦出现”a”就会创建串池对象
    //堆池【ab,new String(“a”,new String(“b”)】
    String ab =new String(“a”)+new String(“b”);
    String abi = ab.intern(); //尝试将ab变量String对象放入串池,并返回串池的对象”ab”
    //串池【”a”,”b”,”ab”】 因为串池中”ab”值的常量对象,因此放入失败
    System.out.println(ab ==”ab”); //false 放入失败,ab变量对象不是串池对象
    System.out.println(abi ==”ab”); //true abi变量引用指向返回的”ab”串池对象
}

上述代码最后一问补充:

//调换顺序后,结果为true,但如果是JDK1.6及以前都是false

x2.intern();

String x1 = “cd”;

垃圾回收

当堆内存满时,会对串池中的常量进行垃圾回收,没有引用的常量会被回收!

设置堆内存为10m,打印串池和垃圾回收详细信息

-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

性能调优

 -XX:StringTableSize=(桶个数)
 

对于常量字符串特别多又重复(读取很多单词的文件)的情况下,intern()方法将字符串入池,适当将-XX:StringTableSize调大,减少hash冲突,增加执行速度。

检测工具

jps ——-》 显示java进程id

jmap -heap 进程id ——-》显示堆内存的详细信息

jvisualvm ,jconsole ——-》图形化界面显示各线程详细信息

Java面试必备,JVM核心知识点总结

直接内存(Direct Memory)

  • 常见于NIO操作,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

直接内存在Java堆内存与系统内存中共享,减少了数据的一次复制。

Java面试必备,JVM核心知识点总结

内存溢出

抛出异常:Java.lang.OutOfMemoryError: Direct buffer memory

分配和回收管理

获取Unsafe对象需要通过反射

直接内存使用底层Unsafe类管理(分配和释放等等)的,释放需要主动调用freeMemory 方法。

ByteBuffer 的实现内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被回收,那么就会由RefernceHandler线程通过 Cleaner 的 clean 方法调用 freeMemory来释放直接内存。

JVM参数

-XX:+DisableExplicitGC JVM参数禁用显式调用 System.gc() 垃圾回收,因为 System.gc() 是full GC 影响性能。

垃圾回收

如何判断一个垃圾可以回收

一、引用计数法

只要一个对象被一个变量引用,就让对象的计数加一,如果对象被引用两次,计数就变成2,如果某一个变量不在引用

这个对象了,就让计数减一,当对象引用计数等于0时,意味着是垃圾对象,等待垃圾回收。

问题:对象循环引用导致计数不会等于0,造成内存泄漏。

Java面试必备,JVM核心知识点总结

✳二、可达性分析 算法 【Java虚拟机采用】

判定对象是否存活,扫描堆中对象先确定一系列的根对象GC Roots,判断每个对象是不是直接或间接地被根对象引

用,如果没有任何引用,证明对象是可回收的。

Java面试必备,JVM核心知识点总结

固定根对象 GC Roots

  • 虚拟机栈中引用的对象(参数,局部变量,临时变量等)
  • 静态属性引用的对象
  • 常量引用的对象(串池)
  • 本地方法栈的引用对象
  • 虚拟机内部的引用(基本数据类型对应的Class对象,异常对象)
  • 同步所持有的对象(synchronized括号里的对象)

四种引用

Java面试必备,JVM核心知识点总结

强引用

只有强引用关系不存在时,即所有GC Root 都不通过强引用该对象,该对象才被垃圾回收。

Object obj = new Object(); //声明强引用

1

软引用

描述一些还有用,但非必须的对象,仅有软引用引用的对象,垃圾回收后,内存仍不足的情况下会回收。

可配合引用队列来释放软引用自身

SoftReference<Object> sf = new SoftReference<>(new Object());//创建软引用

1

弱引用

描述非必须的对象,仅被弱引用引用的对象,垃圾回收时,无论内存是否充足,都会回收该对象。

可配合引用队列来释放软引用自身

WeakReference<Object> wrf = new WeakReference<>(new Object());//创建弱引用
System.out.println(wrf.get()); // java.lang.Object@4b67cf4d
System.gc(); //执行gc
System.out.println(wrf.get()); //null

虚引用

必须配合引用队列释放自身,在创建时必须提供一个引用队列作为参数。主要配合ByteBuffer使用,被引用对象回收后,会将虚引用入队,由ReferenceHandler线程调用虚引用相关方法释放自身。

它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知

应用程序对象的回收情况。

ReferenceQueue<Object> phantomQueue = new ReferenceQueue<>(); //指定引用队列
PhantomReference<Object> prf = new PhantomReference<>(new Object(),phantomQueue);//创建弱引用
System.out.println(prf.get()); //null

终结引用

它用于实现对象的finalize()方法,也可以称为终结器引用。

无需手动编码,其内部配合引用队列使用。

在GC时, 终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。

垃圾回收算法

一、标记清除算法

特点:速度快

缺点:造成内存碎片

Java面试必备,JVM核心知识点总结

二、标记整理算法

特点:涉及内存地址的移动,速度慢

优点:不会造成内存碎布

Java面试必备,JVM核心知识点总结

三、复制算法

特点:没有内存碎片,速度最快

缺点:造成双倍内存空间的占用

Java面试必备,JVM核心知识点总结

分段理论

不同对象生命周期不一样,把Java堆分成新生代和老年代,根据各个年代的特点,使用不同的三种垃圾回收算法的集合。

Stop the world

每一次垃圾回收会触发Stop the World 暂停一切用户线程,因为对象的迁移涉及内存地址的转换,避免出现混乱,垃圾回收完成后,用户线程恢复!

新生代

每次垃圾收集都会有大批对象死去,每次回收后存活的少量对象,会通过寿命+1来晋升,触发Stop the world。

垃圾回收:Minor GC 【标记+复制算法】

老年代

寿命=15的对象会迁移至老年代。寿命在对象头以4bit表示,最大值为15 ,触发Stop the world。

垃圾回收:Major GC 【标记+整理算法】

整体回收

Full GC 新生代和老年代一起进行垃圾回收,之后触发Stop the world。触发Stop the world

Java面试必备,JVM核心知识点总结

具体步骤

① 对象首先分配在伊甸园区,新生代内存不足时,会触发一次 Minor GC ,伴随着 Stop the World

② 伊甸园和From幸存区中存活的对象会Copy到To区中,并且交换From和To区域,其存活对象的寿命会加1.

③ 当寿命超过阈值时,会晋升到老年代区域。阈值最大是15,有些情况(新生代内存严重不足)会提前。

④ 当老年代内存空间不足时,先尝试触发Minor GC ,如果内存还不足,触发 Full GC,Stop the world 时间会更长。

垃圾回收JVM参数整理

Java面试必备,JVM核心知识点总结

大对象直接晋升老年代

新生代内存一定不够,老年代内存空间可以容纳的情况下

OOM

新生代和老年代的空间都不足以放入当前大对象,抛出OOM异常,不同线程抛出OOM不会影响其他线程的执行!

垃圾回收器

一、串行

Serial /SerialOld收集器

新生代采用 Serial 利用复制算法,效率高,老年代采用 SerialOld 标记整理算法。新生代+老年代回收两次STW。

用户线程在达到安全点时,其中一个线程抢占执行垃圾回收任务(还有stop the world),其他线程阻塞,完成之后恢复。

  • 单线程
  • 堆内存较小,适合个人电脑

Java面试必备,JVM核心知识点总结

二、吞吐量优先

Parallel Scavenge/ Parallel Old 收集器

新生代采用 Parallel Scavenge 利用复制算法,效率高,老年代使用 Parallel Old 多线程和 “标记-整理” 算法。新生代+老年代回收两次STW。

用户线程在达到安全点时,所有线程并行执行垃圾回收任务(还有stop the world),完成之后恢复

多线程

堆内存较大,多核CPU

目的单位时间内,尽可能减少Stop the world 的时间

区别 1小时的单位时间

吞吐量优先:0.2+0.2(共执行2次)

响应时间优先:0.1+0.1+0.1+0.1+0.1(共执行5次)

Java面试必备,JVM核心知识点总结

垃圾回收线程个数与CPU核心数相同,执行垃圾回收线程时,CPU占用率会大幅提高。

三、响应时间优先

CMS收集器

基于标记清除算法,对于造成的内存碎片,采用基于标记整理算法的Serial Old回收器执行Full GC作为补偿。产生浮动

垃圾【并发清理其他用户线程产生的垃圾对象需下次清理】

适用场景

  • 多线程
  • 堆内存较大,多核CPU
  • 目的 :尽可能让每次Stop the wordl 的时间减少

Java面试必备,JVM核心知识点总结

G1收集器

G1适合作为服务端垃圾收集器,应用在多处理器和大内存的条件下,可以实现高吞吐量的同时,尽可能满足垃圾收集

较短可控的暂停时间。

整体基于标记清除算法,局部采用复制算法。

垃圾收集器采用分区(不连续的Region),除了Full GC 外,还包含年轻代和部分老年代的Mixed GC,分为4阶段。

Java面试必备,JVM核心知识点总结

  • 初始标记 【标记根对象】伊甸园区域空间不足时,年龄较大的对象放入老年区,G1采用复制算法,触发Minor GC和STW,时间很短。【单一线程运行,其余阻塞】
  • 并发标记 【随着根对象的引用标记其他对象 SATB算法】
  • 老年代占用堆空间达到阈值时,进行并发标记,不会STW,【JVM参数可调阈值】。

Java面试必备,JVM核心知识点总结

  • 最终标记 【并发标记中漏掉的对象】规定时间内,标记并发标记中新变化的回收价值高的区域
  • 筛选回收 【复制标记存活的对象进入新的老年代区域,清除旧内存】
  • 并发失败后,即老年代区域仍不够,就会进入多线程收集器执行Full GC!

应用

Java面试必备,JVM核心知识点总结

一般用于自定义类加载器(框架)会采用!

Java面试必备,JVM核心知识点总结

Java面试必备,JVM核心知识点总结

为避免Full GC,可控制老年代垃圾回收的阈值,JVM参数

-XX:initiatingHeapOccupancyPercent=45% 设置 【默认】

JDK 9 中设置后可进行自动的动态调整。

算法细节

卡表减少GC Root的搜寻时间

新生代回收需要寻找根对象,根对象有一部分是来自老年代的,老年代对象多,便历直接搜对象效率太低,因此采用卡

表的数据结构将老年代细分更小的单位(卡),如果老年代中卡中存有根对象引用了新生代,标记为脏卡,通过写屏 障更新脏卡,避免找整个老年代。

Java面试必备,JVM核心知识点总结

多线程下增加写屏障保证重新标记阶段安全

当对象引用发生改变时,JVM会加入写屏障,只要对象引用发生改变,写屏障代码会执行,会将对象置为待处理的状

态,并放入队列,并发标记结束后再判断是否需要回收。

Java面试必备,JVM核心知识点总结

类加载

加载阶段

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 右: _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用

_super 即父类

_fields 即成员变量

_methods 即方法

_constants 即常量池

_class_loader 即类加载器

_vtable 虚方法表

_itable 接口方法表

  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的
注意
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中可以通过前
面介绍的 HSDB 工具查看

链接

验证

验证类是否符合 JVM规范,安全性检查(例如用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行异常)

准备

javap -v -p class文件路径 //反编译

为 static 变量分配空间,设置默认值

static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror (类对象)末尾

static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

解析

将常量池中的符号解析为直接引用。

初始化

()V 方法 【类的构造方法】
初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

概括地说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 人类初始化,如果父类还没初始化,会引发
  • 人类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化

类加载器

双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则 ,防止恶意篡改类,保证了类的唯一。

//loadclass实现双亲委派模式的算法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上级的话,委派上级 loadClass
                    c = parent.loadClass(name, false);
                }else {
                    // 3. 如果没有上级了(ExtClassLoader),则委派
                    BootstrapClassLoader c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {}
            if (c == null) {
                long t1 = System.nanoTime();
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                c = findClass(name);
                // 5. 记录耗时
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 – t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

启动类加载器

根据虚拟机参数 -Xbootclasspath 表示设置 bootclasspath

其中 /a:. 表示将当前目录追加至 bootclasspath

java -Xbootclasspath:

java -Xbootclasspath/a:<追加路径>

java -Xbootclasspath/p:<追加路径>

扩展类加载器

将 Jar 包拷贝到 JAVA_HOME/jre/lib/ext 目录下,启动 Jar 包,其包含的类就采用扩展类加载器加载。

应用程序类加载器

用户指定的路径下的类采用此类加载器加载。

自定义类加载器

什么时候需要自定义类加载器?

1)想加载非 classpath 随意路径中的类文件

2)都是通过接口来使用实现,希望解耦时,常用在框架设计

3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

继承 ClassLoader 父类

要遵从双亲委派机制,重写 findClass 方法

注意不是重写 loadClass 方法,否则不会走双亲委派的机制

读取类文件的字节码

调用父类的 defineClass 方法来加载类

使用者调用该类加载器的 loadClass 方法

打破双亲委派机制—-SPI

JDK提供了一种帮第三方实现者加载服务(如数据库驱动,日志库)的便捷方式,只要遵循约定,在Jar包中把类名写

在/META-INF文件夹里,在调用forName加载,当前类的ClassLoader是没办法加载的,那么就把他加载到当前线程的

线程上下文加载器【属于应用程序加载器】。

JDBC 的 驱动类

classpath/a:<追加路径>**

java -Xbootclasspath/p:<追加路径>

扩展类加载器

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext 目录下,启动 Jar 包,其包含的类就采用扩展类加载器加载。

应用程序类加载器

用户指定的路径下的类采用此类加载器加载。

自定义类加载器

什么时候需要自定义类加载器?

1)想加载非 classpath 随意路径中的类文件

2)都是通过接口来使用实现,希望解耦时,常用在框架设计

3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

继承 ClassLoader 父类

要遵从双亲委派机制,重写 findClass 方法

注意不是重写 loadClass 方法,否则不会走双亲委派的机制

读取类文件的字节码

调用父类的 defineClass 方法来加载类

使用者调用该类加载器的 loadClass 方法

打破双亲委派机制—-SPI

JDK提供了一种帮第三方实现者加载服务(如数据库驱动,日志库)的便捷方式,只要遵循约定,在Jar包中把类名写

在/META-INF文件夹里,在调用forName加载,当前类的ClassLoader是没办法加载的,那么就把他加载到当前线程的

线程上下文加载器【属于应用程序加载器】。

JDBC 的 驱动类