教程:https://www.bilibili.com/video/BV1PJ411n7xZ
Java 17 文档:https://docs.oracle.com/javase/specs/jvms/se17/html

java 文件先编译为 class 文件,然后通过类加载器子系统进行加载,连接,初始化。

当所需的类加载进来放在内存(运行时数据区),结构如下,其中有:

执行引擎

String 类的 intern() 方法),还存放已被加载的类的信息、常量、静态变量、即时编译器编译后的代码缓存等数据。一个在 class 中的类 → \to → ClassLoader → \to → 元数据模板

java.lang.Class 对象,作为方法区这个类的各种数据的访问入口加载 class 文件的方式:
类加载时机:
Class.forName("com.mysql.cj.jdbc.Driver"))default 关键字修饰的接口方法时,当这个接口的实现类被初始化,该接口需要在此之前被加载static 修饰的变量初始化为 0,null,false。若是 static final 修饰,则编译的时候就已经分配值了Note:
我对准备阶段的理解:应该类似之前学习 C++ 的时候,new 的对象因为是内存中直接分配的,所以该对象的值可能是随机的,一般都初始化为 0,false 或者 NULL。
() ,该方法不需要定义,由 javac 编译器收集所有类变量的赋值动作和静态代码块的语句合并而来。
() 和类的构造方法不同,类的构造方法为 () () 之前先执行父类的 () () 方法在多线程下被同步加锁Note:
补充一个小基础知识,被 final 修饰的变量必须在定义的时候就赋值吗?
答案:不是,在构造方法中赋值也可以。
static { } 静态代码和 static 修饰的变量只会执行一次。
从 JVM 的角度来看,只有两种类加载器,一种是 Bootstrap 类加载器,一种就是其他类加载器。
java.lang.ClassLoader。// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// 获取上层, 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);
// 试图获取引导启动类加载器 null
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);
// 获取当前类的类加载器
ClassLoader classLoader = InitTest.class.getClassLoader();
System.out.println(classLoader);
// 加载字符串类型的类加载器, String 也是引导启动类加载器加载的
ClassLoader classLoader2 = String.class.getClassLoader();
System.out.println(classLoader2);

JAVA_HOME/jre/lib/rt.jar、resource.jar 或者 sun.boot.class.path 路径下的内容,用于提供 JVM 自身需要的类java、javax、sun 等开头的类URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url);
}

JAVA_HOMEjre/lib/ext 或者 java.ext.dirs 系统变量所指定的路径种的所有类库String exts = System.getProperty("java.ext.dirs");
for (String ext : exts.split(";")) {
System.out.println(ext);
}

classpath 或者系统属性 java.class.path 指定路径下的类库为什么需要自定义类加载器:
自定义类加载器实现步骤:
// 1. class
System.out.println(Class.forName("java.lang.String").getClassLoader());
System.out.println(InitTest.class.getClassLoader());
// 2.
System.out.println(Thread.currentThread().getContextClassLoader());
// 3.
System.out.println(ClassLoader.getSystemClassLoader());
假如我创建一个 String 相同的包和相同的类,然后执行 main() 方法会发生什么?
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("hello world");
}
}

换个类名行不行?
package java.lang;
public class CCC {
public static void main(String[] args) {
System.out.println("hello world");
}
}

因此可以解释,自定义的 String 类已经被上层加载器加载过了,因此该类中找不到 main 方法。第二个错误原因是该包下被 Bootstrap 加载器加载过了,所以下层加载器不能再对其加载,这也是 Java 沙箱安全机制中对于恶意代码所采取的防护措施,防止核心 API 被篡改。
Note:
JVM 两个 class 对象是一个类的必要条件:
- 完整类名相同
- 加载这个类的类加载器必须相同

红色为线程共享的,灰色是线程私有的。方法区又叫元空间(永久代)。

| 空间 | 异常 | 垃圾回收 |
|---|---|---|
| 程序计数器 | × | × |
| 本地方法栈 | √ | × |
| 虚拟机栈 | √ | × |
| 堆 | √ | √ |
| 方法区 | √ | √ |
一个 JVM 允许有多个线程并行执行。
Hotspot JVM 中,每个线程都与操作系统的本地线程直接映射。
用来存储下一条指令的地址,就是说记录当前线程运行到哪里。
如果执行的是本地方法,指定的地址为未指定值(undefined)。
唯一没有规定 OutOfMemoryError 且不需要 GC 的区域。

Note:
使用javap -v class路径 可以进行反汇编

主要作用:
异常:
StackOverFlowError 异常OutOfMemoryError 异常为了跨平台的特性,Java 的指令都是根据栈来设计的。
因为不同平台的 CPU 架构不同,所以不能基于寄存器实现。
虚拟机栈好处:
虚拟机栈缺点:
使用 1+1 的操作例子:
iconst_1
iconst_1
iadd
istore_0
mov eax, 1
add eax, 1
Note:
参考:https://blog.csdn.net/shockang/article/details/116676873
栈解决程序的运行问题,程序如何执行,如何处理数据。
堆解决数据存储问题,数据怎么放,放在哪里。(堆主要负责的是对象存储,基本数据类型和引用类型的局部变量放在栈中)
Note:
参数设置参考:https://docs.oracle.com/en/java/javase/11/tools/java.html
Windows 默认是虚拟内存大小,Linux 默认是 1024 KB
使用 -Xss 选项设置线程的最大栈空间,栈的大小直接决定了函数调用的最大深度。
Idea 可以通过以下操作设置参数:

/**
* count 值:
* 默认情况下: 11232
* 设置 -Xss256K: 2314
*
* */
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
JVM 对栈的的操作就只有压栈和出栈,在活动线程中,一个时间点上,只有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的。若在当前方法中调用其他方法,则对应的新的栈帧会被创建放在栈顶,成为新的当前栈帧。
不同线程所包含的栈帧不允许相互引用。
Java 方法有两种返回函数的方式,会导致栈帧弹出:
return
每个栈帧包含:
returnAddress(一条指向字节码指令的地址)。局部变量表所需容量在编译期间已经确定下来,运行期间不会改变。局部变量表最基本的存储单位:Slot 局部变量槽
double 和 long 占两个 Slot,其余类型均占一个 Slotchar,byte,short,boolean 在存储之前都被转换为 int通过 javap -v 命令反汇编可以看到 double 和 long 的槽数为 2:
public static void main(String[] args) {
int a = 10;
int b = 20;
double c = 0D;
long d = 0L;
boolean e = false;
}

其中 Start 为字节码指令起始位置,Length 表示作用范围。
如果当前的栈帧由构造方法或者实例方法创建,那么对象引用 this 会存在索引为 0 的 Slot 处:

栈帧中的 Slot 是可以重复利用的,可以看到变量 c 使用的是 b 之前使用的 Slot:
public void xxx () {
int a = 10;
{
int b = 10;
b += a;
}
int c = 10;
}

Java 局部变量在使用之前必须进行初始化,不像 C 中可以不赋值:

又因操作数是存储在内存,每次操作都需要进行入栈出栈操作,必然会影响执行速度。
为了解决这个问题,HotSpot JVM 提出了栈顶缓存:
Java 文件编译成字节码文件时,所有的变量和方法引用都作为符号链接保存在 class 文件的常量池中。
静态链接:被调用的方法在编译时可知,且运行期间不变
动态链接:被调用的方法在编译期间无法被确定下来,运行期间才能确定
动态链接例子:多态,接口的实现
Note:
Java 中默认除了 invokestatic 和 invokespecial 指令调用的方法(除了final修饰的)都是虚方法。
Java 是静态类型的语言,因为定义变量的时候需要指定类型,而动态类型的语言(JS,Python)是只有变量值有类型信息
invokedynamic 指令使用 lambda 表达式可以直接生成
虚方法表用于快速寻找子类未实现的方法
存放该方法的 PC 寄存器(程序计数器) 的值。
若异常退出,返回地址需要通过异常表来确定。
Note:
静态代码块也算是一个没有返回值的类构造器
栈帧中允许携带与 JVM 实现相关的一些附加信息,例如对程序调试提供支持的信息。
栈溢出的情况?
-Xss 设置,当超出该容量会导致 StackOverFlowError,若整个内存空间不足会导致 OutOfMemoryError调整栈的大小,就能保证不出现溢出吗?
分配的栈内存越大越好吗?
垃圾回收是否会涉及到虚拟机栈?
方法中定义的局部变量是否线程安全?
本地方法(Native Method)是一个 Java 调用非 Java 代码的接口。
本地方法由 native 修饰。
为什么用本地方法?
虚拟机栈是用于管理 Java 方法的调用,本地方法栈负责本地方法的调用,一样具有两种异常。

输入命令:jvisualvm 打开
安装插件 Visual GC 插件参考:https://blog.csdn.net/jushisi/article/details/109655175



现代 GC 大部分都是基于分代收集理论设计的,堆空间细分为:
Java 8 以后堆内存逻辑分为:
新生代进一步划分:
-XX:+PrintGCDetailes 可以查看堆空间的细节,Java17 使用 -Xlog:gc* 选项。
或者 jps 查看 java 进程 id,然后使用 jstat -gc 查看。
Xms:表示堆区的初始内存(年轻代+老年代),等价于 -XX:InitialHeapSize,默认 物理内存/64Xmx:堆区的最大内存(年轻代+老年代),等价于 -XX:MaxHeapSize,默认 物理内存/4查看内存:
Runtime runtime = Runtime.getRuntime();
longinit = runtime.totalMemory() / 1024 / 1024;
long max = runtime.maxMemory() / 1024 / 1024;
log.debug("Xms: {}", init);
log.debug("Xmx: {}", max);
通常会将初始内存和最大内存设置相同的值,目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
当堆区的内存超过最大内存所指定的值将会抛出 OutOfMemoryError:
public class InitTest {
public static void main(String[] args) throws InterruptedException {
List list = new ArrayList();
while (true) {
list.add(new byte[1024 * 1024]);
}
}
}

设置新生代和老年代在堆结构的占比:
-XX:NewRatio=4 表示新生代占 1,老年代占 4Eden 和两个 Survivor 空间默认比例为:8:1:1(实际上并不一定是该比例,是自适应的):
-XX:SurvivorRatio=8 手动设置 8:1:1几乎所有的对象都在 Eden 区域被 new 出来。
绝大部分的对象的销毁都在新生代中进行。
-Xmn 设置新生代最大内存,一般默认即可首先创建对象在 Eden 区域,当 Eden 区域满时,会进行垃圾回收 YGC/MinorGC(垃圾回收时会将S0,S1 一起回收)。Eden 中还在使用的放入 From Survivor 区,并将对象的 age++(初始为 1)。
一段时间后,若 Eden 区域又满,Eden 中还在使用的对象以及 From Survivor 中的对象一起放入 To Survivor 区,将对象的 age++。
重复此过程,当对象的 age 达到 15 (默认)时会放入老年代。
-XX:MaxTenuringThreshold= 进行设置放入老年代的 age 阈值Note:
jinfo -flags查看虚拟机设置参数
总结:
问题:

Minor GC 触发机制:
Major GC 触发机制:
OOMFull GC 触发机制:
System.gc(),系统会建议执行 Full GC,不是一定执行不分代也可以,分代是为了优化 GC 的性能,如果没有分代,所有的对象都在一起,每次 GC 都要在全部对象中找已经不再使用的对象,效率太低。
由于堆内存是线程共享的,并发状态下从堆中划分内存空间是线程不安全的,为了避免多个线程操作同一个地址,需要使用加锁等机制,影响性能。
JVM 给每一个线程分配了一个私有缓冲区,包含在 Eden 空间中。
1%-XX:TLABWasteTargetPercent 可以设置 TLAB 所占 Eden 空间的百分比jinfo -flag UseTLAB 可以查看进程是否使用 TLAB(默认开启)多个线程同时分配内存时,使用 TLAB 可以避免非线程安全的问题,同时提升内存分配的效率,因此我们将这种内存分配方式称为快速分配策略。
对象分配过程:TLAB

-XX:+PrintFlagsInitial 查看 JVM 所有参数的默认值-XX:+PrintFlagsFinal 查看 JVM 所有参数可能进行修改后的最终值jinfo -flag 参数 可以查看进程的某个参数的值-XX:PrintGCDetails 垃圾回收详细情况(Java 8)-Xlog:gc* 垃圾回收详细情况(Java 17)-XX:SurvivorRatio= 设置 Eden 区和 Survivor 区比值-XX:NewRatio= 设置老年代和新生代的比值若经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
这样就不用在堆上分配内存了,也不需要垃圾回收了。
Java 6u23 之后默认已经开启了逃逸分析。
没有发生逃逸的对象,可以分配到栈上,随着方法执行的结束,栈帧也随之被移除:
public void fun() {
List<Object> list = new ArrayList<>();
// ...
list = null;
}
发生逃逸的对象,方法中 new 的对象作为返回值,会放在堆中:
public List fun() {
List<Object> list = new ArrayList<>();
// ...
return list;
}
Note:
简单讲就是如果 new 的对象只在方法内使用,那么就是没有逃逸,即时编译器会进行优化,在栈上分配空间!
因为栈上的局部变量表存储单位是 Slot,是没办法存对象的,其本质就是标量替换,然后在进行存储!
通过下面的例子可以看到 JVM 这么做的好处:
@Slf4j(topic = "c.InitTest")
public class InitTest {
static void invoke() {
User user = new User("张三", 150);
}
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000_0000; i++) {
invoke();
}
log.debug("用时: {} ms", System.currentTimeMillis() - start);
TimeUnit.SECONDS.sleep(1000);
}
}
class User {
String name;
Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,执行结果:

打开逃逸分析(不使用任何参数,默认就是打开状态),执行结果:

可以看到速度提升显著!
若一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
Note:
简单讲就是如果一个对象只能被一个线程访问,就没必要上锁了,即使你上了锁,即时编译阶段会进行优化,把锁去掉。
这里的标量是指无法再分解成更小的数据的数据,比如基本数据类型。
如果一个对象不会被在方法外使用,那么即时编译阶段进行优化后会把整个对象拆成若干个成员变量来使用。
默认开启,可以设置 -XX:-EliminateAllocations 关闭。
Note:
标量替换就是栈上分配的本质!
Note:
方法区只是逻辑上的叫法,元数据区和永久代是落地的实现。

从线程共享与否的角度:

存储的角度:


OutOfMemoryError
-XX:MetaspaceSize= 设置元空间初始大小(默认 21M)-XX:MaxMetaspaceSize= 设置最大元空间大小(默认没有限制)
Note:
若初始元空间过小,加载的类信息容量超过该值,会触发 Full GC。为了避免频繁发生 Full GC,建议将-XX:MetaspaceSize=设置为一个相对较高的值。
出现 OOM 怎么办?
Note:
被处理为static final的类变量在编译的时候就会被分配值
类字节码中的常量池中存放编译期间生成的各种字面量以及符号引用。
当类的字节码被加载后放入方法区,它的常量池信息就会集中放入到一块内存,这块内存就称为运行时常量池,并且把里面的 符号地址 变为 真实地址。
Note:
为什么用常量池?
- 常量池是为了避免频繁的创建和销毁对象而影响系统的性能,其实现了对象的共享。动态链接的时候会用到运行时常量池。

Note:
为什么要移除永久代?
官方:为了融合 HotSpot 和 JRockit ,JRockit 中没有永久代。
- 永久代空间设置无法确定
- 永久代调优困难(一个类是否不再使用的判断比较麻烦,因此移除永久代,放在直接内存)
字符串常量池(StringTable)为什么为什么放在了堆?
- 永久代垃圾回收效率低,只有在 Full GC 的时候才触发,Full GC 在老年代空间不足,永久代空间不足的时候才触发,因此 StringTable 的回收效率不高,但是在开发中会有大量的字符串被创建,回收效率很低,导致永久代内存不足。放到堆中,能及时回收内存。
方法区的垃圾回收主要针对两部分内容:
Note:
在大量使用反射、动态代理、CGLib 等字节码框架,通常需要 JVM 具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
内存示意图:

new,最常见的方式,若构造私有化,一般使用静态方法获取实例(单例模式),XxxBuilder/XxxFactory 的静态方法获取实例(工厂模式)newInstance()getDeclaredConstructor().newInstance()(可以调空参、带参的构造器,权限没有要求)clone(),不调用任何构造器,当前类需要实现 Cloneable 接口并实现 clone()Objenesis0,false,null)

JVM 如何通过栈帧中的对象引用,找到其内部的对象实例呢?


元空间就在直接内存!!!
直接内存是直接向系统申请的内存区间。
基于 NIO,直接申请物理内存空间:
ByteBuffer.allocateDirect(BUFFER_SIZE);
需要大量 IO 操作的业务中可以使用直接内存,性能更高
OOM-XX:MaxDirectMemorySize= 设置,默认和 -Xmx 一样缺点: