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

JVM基础

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

作用:记住下一条JVM指令

特点:

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

虚拟机栈(Java virtual mechine Stack)

线程私有

  • 每个线程运行时所需要的内存
  • 每个栈由多个栈帧Frame组成,对应着每次方法调用时占的内存
  • 每个线程只能有一个当前活动栈帧,对应着当前正在执行的方法
三个问题

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

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

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

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

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

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

栈内存溢出

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

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

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

线程运行诊断

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方法区的实现是永久代,占用的是堆内存。

内存溢出

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

调节方法区内存大小参数

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

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

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

运行时常量池

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

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

StringTable串池:

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

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

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

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

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

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=20000(桶个数)

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

检测工具

jps -------》 显示java进程id

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

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

直接内存(Direct Memory)

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

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

内存溢出

抛出异常: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虚拟机采用】

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

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

固定根对象 GC Roots

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

强引用

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

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

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

  • 可配合引用队列来释放软引用自身
SoftReference<Object> sf = new SoftReference<>(new Object());//创建软引用
弱引用

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

  • 可配合引用队列来释放软引用自身
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堆分成新生代老年代,根据各个年代的特点,使用不同的三种垃圾回收算法的集合。

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

具体步骤

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

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

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

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

垃圾回收JVM参数整理

大对象直接晋升老年代

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

OOM

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

垃圾回收器

一、串行

Serial /SerialOld收集器

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

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

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

二、吞吐量优先

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次)

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

三、响应时间优先

CMS收集器

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

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

适用场景

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

G1收集器

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

较短可控的暂停时间。

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

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

  • 初始标记 【标记根对象】

    伊甸园区域空间不足时,年龄较大的对象放入老年区,G1采用复制算法,触发Minor GC和STW,时间很短。【单一线程运行,其余阻塞】

  • 并发标记 【随着根对象的引用标记其他对象 SATB算法】

    老年代占用堆空间达到阈值时,进行并发标记,不会STW,【JVM参数可调阈值】。

  • 最终标记【并发标记中漏掉的对象】

    规定时间内,标记并发标记中新变化的回收价值高的区域

  • 筛选回收【复制标记存活的对象进入新的老年代区域,清除旧内存】

并发失败后,即老年代区域仍不够,就会进入多线程收集器执行Full GC!

应用

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

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

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

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

算法细节

卡表减少GC Root的搜寻时间

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

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

障更新脏卡,避免找整个老年代。

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

当对象引用发生改变时,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 容器
步骤

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法
    注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 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 容器
步骤

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法
    注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法

打破双亲委派机制----SPI

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

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

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

  • JDBC 的 驱动类
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Steve_hanhaiLong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值