JVM
Java就是Java虚拟机,它是Java实现跨平台的基石。
程序运行之前,需要先通过编译器将Java源代码文件编译成Java字节码文件。
程序运行时,JVM会对字节码文件进行逐行解释,翻译成机器码指令,并交给对应的操作系统去执行。

这样就实现了Java一次编译,处处运行的特性。
JVM的其他特性
1、JVM可以自动管理内存,通过垃圾回收器回收不再使用的对象并释放内存空间。
2、JVM包含一个即时编译器JIT,他可以在运行时将热点代码缓存到codeChche中,下次执行的时候不用再一行一行的解释,而是直接执行缓存后的机器码,执行效率大幅提高。
3、任何可以通过Java编译的语言,比如说Groovy,Kotlin,Scale等,都可以在JVM上运行。
JVM的组织架构
JVM可以划分三个部分:类加载器,运行时数据区和执行引擎。

1、类加载器:负责从文件系统、网络或其他来源加载Class文件,将Class文件中的二进制数据读入到内存当中。
2、运行时数据区:JVM在执行Java程序时,需要在内存中分配空间来处理各种数据,这些内存区域按照Java虚拟机规范可以划分为方法区,堆,虚拟机栈,程序计数器和本地方法栈。
3、执行引擎,也是JVM的心脏,负责执行字节码。它包括一个虚拟处理器,即时编译器JIT和垃圾回收器。
内存管理
JVM的内存区域
JVM的内存区域可以分为程序计数器,虚拟机栈,本地方法栈,堆和方法区。
其中方法区和堆事线程共享的,虚拟机栈、本地方法栈和程序计数器是线程私有的。
程序计数器
程序计数器也被称为PC寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码行号指示器。
Java虚拟机栈
Java虚拟机栈的生命周期和线程相同。
当线程执行一个方法时,会创建一个对应的栈帧,用于存储局部变量表,操作数栈,动态链接、方法出口等信息。然后栈帧会被压入虚拟机栈中。当方法执行完毕后,栈帧会从虚拟机栈中移除
一个什么都没有的空方法,空的参数都没有,那局部变量表里有没有变量
对于静态变量 局部变量表里不会有任何变量
但是对于非静态方法,局部变量表中也会有一个用于存储this引用的变量。this引用指向当前实例对象,在方法调用时被隐式传入。
本地方法栈
本地方法栈和虚拟机栈相似,区别在于虚拟机栈是为 JVM 执行 Java 编写的方法服务的,而本地方法栈是为 Java 调用本地 native 方法服务的,通常由 C/C++ 编写。
在本地方法栈中,主要存放了 native 方法的局部变量、动态链接和方法出口等信息。当一个 Java 程序调用一个 native 方法时,JVM 会切换到本地方法栈来执行这个方法。
native方法
native方法是在Java通过native关键字声明的,用于调用非Java语言,如C/C++编写的代码,Java可以通过JNI,也就是JavaNativeInterface与底层系统,硬件设备或者本地库进行交互。
Java堆
堆是JVM中最大的一块内存区域,被所有线程共享,在JVM启动时创建,主要用来存储new出来的对象。
Java 中"几乎"所有的对象都会在堆中分配,堆也是垃圾收集器管理的目标区域。
从内存回收的角度来看,由于垃圾收集器大部分都是基于分代收集理论设计的,所以堆又被细分为新生代
、老年代
、Eden空间
、From Survivor空间
、To Survivor空间
等。
堆和栈的区别
堆属于线程共享·的内存区域,几乎所有new出来的对象都会在堆上分配,生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不再被任何变量引用,最后被垃圾回收器回收。
栈属于线程私有的内存区域,主要存储局部变量、方法参数、对象引用等,通常随着方法调用的结束而自动释放,不需要垃圾收集器处理。
对象创建的过程
当我们使用new关键字创建一个对象时,JVM首先会检查new指令的参数是否能在常量池中定位到类的符号引用,然后检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,就先执行类加载。
如果已经加载,JVM 会为对象分配内存完成初始化,比如数值类型的成员变量初始值是 0,布尔类型是 false,对象类型是 null。
接下来会设置对象头,里面包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。
最后,JVM 会执行构造方法 <init>
完成赋值操作,将成员变量赋值为预期的值,比如 int age = 18
,这样一个对象就创建完成了。
对象的销毁
当对象不再被任何引用指向时,就会变成垃圾。垃圾收集器会通过可达性分析算法判断对象是否存活,如果对象不可达,就会被回收。
垃圾收集器通过标记清除、标记复制、标记整理等算法来回收内存,将对象占用的内存空间释放出来。
堆内存是如何分配的
当空闲的内存区较大,较完整时,我们会采用指针碰撞的方式,如年轻代;而空闲列表适用于内存碎片化严重或对象大小差异较大的场景如老年代。
什么是指针碰撞?
假设堆内存是一个连续的空间,分为两个部分,一部分是已经被使用的内存,另一部分是未被使用的内存,在分配内存时,Java虚拟机会维护一个指针,指向下一个可用的内存地址,每次分配内存时,只需要将指针向后移动一段juice,如果没有发送碰撞,就将这段内存分配给对象实例。
什么是空闲列表、
JVM维护一个列表,记录堆中所有没有占用的内存块,每个内存块都记录有大小和地址信息。
当有新的对象请求内存时,JVM会遍历空闲列表,寻找足够大的空间来存放新对象。
分配后,如果选中的内存块未被完全利于,剩余的部分会作为一个新的内存块加入到空闲列表当中。
new对象时,堆会发送抢占吗
会

new对象时,指针会向右移动一个对象大小的距离,加入一个线程A正在给字符串对象 s 分配内存,另外一个线程 B 同时为 ArrayList 对象 l 分配内存,两个线程就发生了抢占。
JVM怎么解决内存分配的竞争问题?
为了解决堆内存分配的竞争问题,JVM为每个线程保留了一小块内存空间,被称为TLAB,也就是线程本地分配缓冲区,用于存放该线程分配的对象。

当线程需要分配对象时,直接从TLAB中分配,只有当TLAB用尽或对象太大需要直接在堆中分配时,才会使用全局分配指针。
对象的内存布局
对象的内存布局是Java虚拟机规范定义的,但具体的实现细节各有不同。
就拿我们常用的 HotSpot 来说吧。
对象在内存中包括三部分,对象有,实例数据和对齐填充。

对象头的作用
对象头是对象存储在内存中的元信息,包含了MarkWord,类型指针等
MarkWord存储了对象的运行时状态信息,包括锁,哈希值,GC标记等。在64位操作系统中占8个字节。
类型指针指向对象所属类的元数据,也就是Class对象,用来支持多态,方法调用等功能。
除此之外,如果对象时数组类型,还会有一个额外的数组长度字段,占4个字节。
类型指针会被压缩吗
类型指针可能会被压缩,以节省内存空间。比如说 在开启压缩指针的情况下占 4 个字节,否则占 8 个字节。在 JDK 8 中,压缩指针默认是开启的。
实例数据
实例数据是对象实际的字段值,也就是成员变量的值,按照字段再类中声明的顺序存储。
对齐填充
由于JVM的内存模型要求对象的起始地址是8字节对齐,因此对象的总大小必须是8字节的倍数。
如果对象头和实例数据的总长度不是8的倍数,JVM会通过填充额外的字节来对齐。
为什么非要8字节对齐呢
因为CPU进行内存访问时,一次寻址的指针大小是8字节,正好是L1缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓存行加载,CPU的访问效率就会越低。
new Objet 对象的内存大小
一般来说,目前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默认开启的,因此在 64 位的 JVM 上,new Object()的大小是 16 字节(12 字节的对象头 + 4 字节的对齐填充)。
对象头的大小是固定的,在 32 位 JVM 上是 8 字节,在 64 位 JVM 上是 16 字节;如果开启了压缩指针,就是 12 字节。
对象的引用大小
在64位的JVM上,未开启指针压缩,对象引用占8个字节,开启压缩指针时,对象引用会被压缩到4字节。
JVM怎么访问对象?
主流的方式:
句柄和直接指针。

两者的区别在于,句柄是通过一个中间的句柄表来定位对象的,而直接指针则是通过引用直接指向对象的内存地址。
优点是:对象被移动,只需要修改句柄表中的指针,不需要修改对象引用本身。
在直接指针中,引用直接存储对象的内存地址;对象的实例数据和类型信息都存储在堆中固定的内存区域。
优点:访问速度更快,因为少了一次句柄的寻址操作。缺点是如果对象在内存中移动。引用需要更新为新地址。HotSpot 虚拟机主要使用直接指针来进行对象访问。
说对象有哪几种引用
四种:强引用,弱引用,软引用,虚引用
强引用是 Java 中最常见的引用类型。使用 new 关键字赋值的引用就是强引用,只要强引用关联着对象,垃圾收集器就不会回收这部分对象,即使内存不足。
软引用于描述一些非必须对象,通过 SoftReference 类实现。软引用的对象在内存不足时会被回收。
弱引用用于描述一些短生命周期的非必须对象,如 ThreadLocal 中的 Entry,就是通过 WeakReference 类实现的。弱引用的对象会在下一次垃圾回收时会被回收,不论内存是否充足。
虚引用主要用来跟踪对象被垃圾回收的过程,通过 PhantomReference 类实现。虚引用的对象在任何时候都可能被回收。
Java堆的内存分区了解吗
Java堆被划分为新生代和老年代两个区域
新生代又被划分为Eden空间和两个Survivor空间(Form,To)
新创建的对象会被分配到Eden区,当Eden区填满时,会触发一次MinorGC,清除不在使用的对象。存活下来的对象会从Eden区移动到Survivor区。
对象在新生代中经历了多次GC后,如果仍然存活,会被移动到老年代。当老年代内存不足时,会触发MajorGC,对整个堆进行垃圾回收。
新生代的区域划分
新生代的垃圾收集主要采用标记-复制算法,因为新生代的存活对象比较少,每次复制少量的存活对象效率比较高
基于这种算法,虚拟机将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。默认 Eden 和 Survivor 的大小比例是 8∶1。
对象什么时候会进入老年代
对象通常会在年轻代中分配,随着时间的推移和垃圾收集的进程,某些满足条件的对象会进入到老年代中,如长期存活的对象

长期存活的对象如何判断?
JVM会为对象维护一个"年龄"计数器,记录对象在新生代中经历MinorGC的次数,每次GC未被回收的对象,其年龄会加1。
当超过一个特定的阈值,默认是15,就会被认为是老对象了,需要重点关照。
大对象如何判断
大对象是指占用内存较大的对象,如大数组、长字符串等。
其大小由 JVM 参数 -XX:PretenureSizeThreshold
控制,但在 JDK 8 中,默认值为 0,也就是说默认情况下,对象仅根据 GC 存活的次数来判断是否进入老年代。
G1 垃圾收集器中,大对象会直接分配到 HUMONGOUS 区域。当对象大小超过一个 Region 容量的 50% 时,会被认为是大对象。
动态年龄判断
如果 Survivor 区中所有对象的总大小超过了一定比例,通常是 Survivor 区的一半,那么年龄较小的对象也可能会被提前晋升到老年代。
这是因为如果年龄较小的对象在 Survivor 区中占用了较大的空间,会导致 Survivor 区中的对象复制次数增多,影响垃圾回收的效率。
STW
JVM 进行垃圾回收的过程中,会涉及到对象的移动,为了保证对象引用在移动过程中不被修改,必须暂停所有的用户线程,像这样的停顿,我们称之为Stop The World
。简称 STW。
如何暂停线程
JVM 会使用一个名为安全点(Safe Point)的机制来确保线程能够被安全地暂停,其过程包括四个步骤:
- JVM 发出暂停信号;
- 线程执行到安全点后,挂起自身并等待垃圾收集完成;
- 垃圾回收器完成 GC 操作;
- 线程恢复执行。
什么是安全点
安全点是 JVM 的一种机制,常用于垃圾回收的 STW 操作,用于让线程在执行到某些特定位置时,可以被安全地暂停。
对象一定在堆中吗
不一定。
默认情况下,Java 对象是在堆中分配的,但 JVM 会进行逃逸分析,来判断对象的生命周期是否只在方法内部,如果是的话,这个对象可以在栈上分配。
逃逸分析
逃逸分析是一种 JVM 优化技术,用来分析对象的作用域和生命周期,判断对象是否逃逸出方法或线程。
可以通过分析对象的引用流向,判断对象是否被方法返回、赋值到全局变量、传递到其他线程等,来确定对象是否逃逸。
如果对象没有逃逸,就可以进行栈上分配、同步消除、标量替换等优化,以提高程序的性能。
逃逸具体指的是什么
根据对象逃逸的范围,可以分为方法逃逸和线程逃逸。
当对象被方法外部的代码引用,生命周期超出了方法的范围,那么对象就必须分配在堆中,由垃圾收集器管理。
public Person createPerson() {
return new Person(); // 对象逃逸出方法
}
比如说 new Person()
创建的对象被返回,那么这个对象就逃逸出当前方法了。

再比如说,对象被另外一个线程引用,生命周期超出了当前线程,那么对象就必须分配在堆中,并且线程之间需要同步。
public void threadEscapeExample() {
Person p = new Person(); // 对象逃逸到另一个线程
new Thread(() -> {
System.out.println(p);
}).start();
}
逃逸分析的好处
主要有三个。
第一,如果确定一个对象不会逃逸,那么就可以考虑栈上分配,对象占用的内存随着栈帧出栈后销毁,这样一来,垃圾收集的压力就降低很多。
第二,线程同步需要加锁,加锁就要占用系统资源,如果逃逸分析能够确定一个对象不会逃逸出线程,那么这个对象就不用加锁,从而减少线程同步的开销。
第三,如果对象的字段在方法中独立使用,JVM 可以将对象分解为标量变量,避免对象分配。
内存溢出和内存泄漏
内存溢出,俗称 OOM,是指当程序请求分配内存时,由于没有足够的内存空间,从而抛出 OutOfMemoryError。
内存泄漏是指程序在使用完内存后,未能及时释放,导致占用的内存无法再被使用。随着时间的推移,内存泄漏会导致可用内存逐渐减少,最终导致内存溢出。
内存泄漏由哪些原因导致
①、静态的集合中添加的对象越来越多,但却没有及时清理;静态变量的生命周期与应用程序相同,如果静态变量持有对象的引用,这些对象将无法被 GC 回收。
java
class OOM {
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object();
list.add(obj);
}
}
②、单例模式下对象持有的外部引用无法及时释放;单例对象在整个应用程序的生命周期中存活,如果单例对象持有其他对象的引用,这些对象将无法被回收。
java
class Singleton {
private static final Singleton INSTANCE = new Singleton();
private List<Object> objects = new ArrayList<>();
public static Singleton getInstance() {
return INSTANCE;
}
}
③、数据库、IO、Socket 等连接资源没有及时关闭;
java
try {
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url", "", "");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("....");
} catch (Exception e) {
}finally {
//不关闭连接
}
④、 ThreadLocal 的引用未被清理,线程退出后仍然持有对象引用;在线程执行完后,要调用 ThreadLocal 的 remove 方法进行清理。
java
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set(new Object()); // 未清理
什么情况发生栈溢出
栈溢出发生在程序调用栈的深度超过 JVM 允许的最大深度时。
栈溢出的本质是因为线程的栈空间不足,导致无法再为新的栈帧分配内存。
当一个方法被调用时,JVM 会在栈中分配一个栈帧,用于存储该方法的执行信息。如果方法调用嵌套太深,栈帧不断压入栈中,最终会导致栈空间耗尽,抛出 StackOverflowError。
最常见的栈溢出场景就是递归调用,尤其是没有正确的终止条件下,会导致递归无限进行。
垃圾收集
JVM垃圾回收机制
垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除或回收。
JVM 在做 GC 之前,会先搞清楚什么是垃圾,什么不是垃圾,通常会通过可达性分析算法来判断对象是否存活。
在确定了哪些垃圾可以被回收后,垃圾收集器(如 CMS、G1、ZGC)要做的事情就是进行垃圾回收,可以采用标记清除算法、复制算法、标记整理算法、分代收集算法等。
如何判断对象仍然存活
Java通过可达性分析算法来判断一个对象是否还存活。
通过一组名为 "GC Roots" 的根对象,进行递归扫描,无法从根对象到达的对象就是"垃圾",可以被回收。

什么是引用计数法
每个对象有一个引用计数器,记录引用它的次数。当计数器为零时,对象可以被回收
引用计数器无法解决循环引用的问题。
可达性分析的时候,应该由哪些前置性的操作
在进行垃圾回收之前,JVM会暂停所有正在执行的应用线程。
这是因为可达性分析过程必须确保在执行分析时,内存中的对象关系不会被应用线程修改。如果不暂停应用线程,可能会出现对象引用的改变,导致垃圾回收过程中判断对象是否可达的结果不一致,从而引发严重的内存错误或数据丢失。