教程:https://www.bilibili.com/video/BV1PJ411n7xZ
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
选项设置线程的最大栈空间,栈的大小直接决定了函数调用的最大深度。
/**
* 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
,默认 物理内存/64
Xmx
:堆区的最大内存(年轻代+老年代),等价于 -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 触发机制:
OOM
Full GC 触发机制:
System.gc()
,系统会建议执行 Full GC,不是一定执行不分代也可以,分代是为了优化 GC 的性能,如果没有分代,所有的对象都在一起,每次 GC 都要在全部对象中找已经不再使用的对象,效率太低。
由于堆内存是线程共享的,并发状态下从堆中划分内存空间是线程不安全的,为了避免多个线程操作同一个地址,需要使用加锁等机制,影响性能。
JVM 给每一个线程分配了一个私有缓冲区,包含在 Eden 空间中。
1%
-XX:TLABWasteTargetPercent
可以设置 TLAB 所占 Eden 空间的百分比jinfo -flag UseTLAB
可以查看进程是否使用 TLAB(默认开启)多个线程同时分配内存时,使用 TLAB 可以避免非线程安全的问题,同时提升内存分配的效率,因此我们将这种内存分配方式称为快速分配策略。
-XX:+PrintFlagsInitial
查看 JVM 所有参数的默认值-XX:+PrintFlagsFinal
查看 JVM 所有参数可能进行修改后的最终值jinfo -flag 参数
可以查看进程的某个参数的值-XX:PrintGCDetails
垃圾回收详细情况(Java 8)-Xlog:gc*
垃圾回收详细情况(Java 17)