JVM内存结构

image-20230910151117645

程序计数器

  • 用于记住下一条执行指令的地址, 线程之间切换,获取到了时间片,将获取程序计数器中的指令进行执行。
  • 并且线程私有
  • 不会有内存溢出问题

虚拟机栈(栈)

先进后出, 存储非native的Java方法(栈帧),执行新方法时会将方法放入栈中,并且执行完reutrn或者抛出异常,将会弹栈

设置大小 -Xss2024k

本地方法栈

与虚拟机栈最大区别是存入的是native方法

内存溢出

  1. 栈内存溢出: 递归调用方法,栈帧过大(栈帧容量大于栈内存)
  2. 堆内存溢出:

线程运行诊断

  1. 用top定位那个进程对cpu的占用过高或死锁
  2. ps H -eo pid,tid,%cpu | grep 进程id (来定位那个线程导致的)
  3. jstack 进程id (其中的线程id是16进制的,ps出来的是10进制)
  4. 最终可定位到代码行数

  • 通过new关键字,创建的对象
  • 线程共享,存在线程安全问题
  • 存在垃圾回收机制
  • 设置大小 -Xmx 1024k

堆内存溢出

大量新建对象存在堆中无法得到回收

1
2
3
4
5
6
List list = new ArrayList<>();
String str = "hello";
for(;;) {
str += "hello";
list.add(str);
}

堆内存诊断

  1. jps工具
  2. jmap工具 jmap -heap 进程id
  3. jconsole工具
  4. jvisualvm 工具(推荐)

方法区(元空间)

image-20230910151555041

方法区是规范,永久代和元空间是实现

主要存储类的信息

方法区内存溢出

类信息加载过多会出现方法区溢出,但是1.8JDK方法区使用的本地内存,需要设置元空间大小。就能出现元方法区溢出问题

-XX:MaxMetaspaceSize=N 1.8

-XX:MaxPermSize=N 1.8之前

运行时常量池

image-20230910154959937

Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table)

通过这个常量池表,指令就可以去和这个表对应匹配执行指令

字符串常量池(Stringtable)

  • JDK1.6中StringTable是放在永久代中
  • 1.8、1.7是放在堆中的
  • 并且 StringTable的大小会影响性能, -XX: StringTableSize = n(因为StringTable本质是一个HashTable)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
System.out.println(s3 == s4); // false s4, javap 反编译后会发现new 了一个StringBuilder通过append将"a",
// "b"都加入,最后toString在堆中生成一个对象, 所以为false JDK8
System.out.println(s3 == s5); // true 被解释器优化(s5 = 常量 + 常量 = 常量) 所以会去字符串常量池中找

// 2
String s1 = new String("a") + new String("b");
System.out.println(s1 == "ab"); // false // 与上方两变量相加类似最终会以StringBuilder.toString返回(new了一个新的String)
String intern = s1.intern();
System.out.println(intern == "ab"); // true 将"ab"加入字符串常量池并且返回引用(没有就加入)

// 3
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
System.out.println(s1 == "ab"); // true JDK1.6这里会是false,因为会去copy一份s1的值,导致两个并不是一个对象
System.out.println(s2 == "ab"); // true

// 4
String s1 = new String("a") + new String("b");
System.out.println(s1 == "ab"); // false
String s2 = s1.intern(); // 如果放中间下面也会是false, 可能是为了安全考虑保证前后一致
System.out.println(s1 == "ab"); // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
String s1 = new String("a") + new String("b");// 反编译

PS F:\idea\code\jvm\target\classes\heapoverflow> javap -v .\Test.class
Classfile /F:/idea/code/jvm/target/classes/heapoverflow/Test.class
Last modified 2023-9-10; size 671 bytes
MD5 checksum 7b08566e21524c96238adc5e4e5c9627
Compiled from "Test.java"
public class heapoverflow.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#27 // java/lang/Object."<init>":()V
#2 = Class #28 // java/lang/StringBuilder
#3 = Methodref #2.#27 // java/lang/StringBuilder."<init>":()V
#4 = Class #29 // java/lang/String
#5 = String #30 // a
#6 = Methodref #4.#31 // java/lang/String."<init>":(Ljava/lang/String;)V
#7 = Methodref #2.#32 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = String #33 // b
#9 = Methodref #2.#34 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Class #35 // heapoverflow/Test
#11 = Class #36 // java/lang/Object
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lheapoverflow/Test;
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 s1
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 SourceFile
#26 = Utf8 Test.java
#27 = NameAndType #12:#13 // "<init>":()V
#28 = Utf8 java/lang/StringBuilder
#29 = Utf8 java/lang/String
#30 = Utf8 a
#31 = NameAndType #12:#37 // "<init>":(Ljava/lang/String;)V
#32 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#33 = Utf8 b
#34 = NameAndType #40:#41 // toString:()Ljava/lang/String;
#35 = Utf8 heapoverflow/Test
#36 = Utf8 java/lang/Object
#37 = Utf8 (Ljava/lang/String;)V
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 toString
#41 = Utf8 ()Ljava/lang/String;
{
public heapoverflow.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lheapoverflow/Test;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: new #4 // class java/lang/String
10: dup
11: ldc #5 // String a
13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4 // class java/lang/String
22: dup
23: ldc #8 // String b
25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; // 最终还是new String("ab")
34: astore_1
35: return
LineNumberTable:
line 10: 0
line 11: 35
LocalVariableTable:
Start Length Slot Name Signature
0 36 0 args [Ljava/lang/String;
35 1 1 s1 Ljava/lang/String;
}

直接内存

  • 不受JVM管理
  • 会有泄露问题
  • 多用于文件传输NIO
  • 需要手动去释放分配的内存(借用了虚引用)
  • 直接内存因为不需要通过系统内存,和Java堆内存而是另分配一段内存来提升速度

image-20230912090024767

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ByteBuffer allocate = ByteBuffer.allocateDirect(1024);

public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}

DirectByteBuffer(int cap) { // package-private

super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);

long base = 0;
try {
// Unsafe类分配内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 使用虚引用来手动free掉分配的内存
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

垃圾回收

如何判定对象是否能回收

  1. 引用计数法(A、B对象互相引用会导致无法回收掉;)

    对象每被引用一次计数就加1,否则减1

  2. 可达性分析法

    1. 分析对象之间的引用关系来确定哪些对象可以被程序访问到(即可达),哪些对象不再可达,从而判断哪些对象可以被回收
    2. GC Roots 是一组根对象,它们是程序中的起始点,通过它们可以追踪到所有的可达对象
    3. 从 GC Roots 开始,通过对象之间的引用链进行遍历,找到所有可以从 GC Roots 访问到的对象,这些对象被认为是可达的。如果一个对象不可达(即没有引用链可以连接到它),那么它被认为是不可达的,可以被回收。

五种引用

强引用

能通过GC Root引用链找到对象,就说明不会被垃圾回收

弱引用

没有强引用,并且不管内存还够不够,都会去回收,最终弱引用将加入引用队列等待被回收

软引用

强引用都没有,并且内存不够时才会去回收,最终软引用被加入引用队列等待回收

虚引用

须配合引用队列,将虚引用Cleaner放入引用队列, 然后Reference Handler线程会去看引用队列中是否有Cleaner,如果发现有,就会去调用Cleaner的clean方法将分配的直接内存通过Unsafe.freeMemory方法释放掉。最终将虚引用Cleaner也释放掉

终结器引用

须配合引用队列,终接器引用被方法引用队列,当它被释放时,会调用对象的finallize方法,当下一次GC时这个对象就会被GC回收掉

软引用-引用队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
// 软引用队列是强引用
List<SoftReference<String>> list = new ArrayList<>();
ReferenceQueue<String> queue = new ReferenceQueue<>();
// 软引用加入引用队列
SoftReference softReference = new SoftReference<String>(new String(""), queue);
list.add(softReference);

// 到引用队列中清除软引用
Reference<?> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
}

弱引用

1
2
3
4
5
6
7
8
9
10
List<WeakReference<String>> list = new ArrayList<>();
ReferenceQueue<String> queue = new ReferenceQueue<>();
WeakReference WeakReference = new WeakReference<String>(new String(""), queue);
list.add(WeakReference);

Reference<?> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}

垃圾回收算法

标记-清除

通过GC Root引用链找到需要清除的对象进行标记,然后再清除

速度快,但是清除的空间不连续,会产生内存碎片, 如果再放入一个数组就放不下了

image-20230912093223894

标记-整理

不会产生内存碎片,但是整理需要时间,故效率较低(当整理存在的对象的地址时,会花更多的时间去处理对象)

image-20230912093631966

复制

不会产生内存碎片,但是需要双倍的内存空间来复制存活的对象

将FROM存活的对象复制到TO上,然后TO再和FROM交换

image-20230912094021748

分代算法

  1. 刚开始对象会放在新生代中的伊甸园区
  2. 如果新生代容量满了,会触发minor GC,GC的同时会将所有的非GC线程停止住, 此时会对伊甸园区中进行标记,并使用复制算法将存活的对象复制到幸存区To中,且将年龄加1,此时会将幸存区To与幸存区From进行交换
  3. 如果幸存区和伊甸园都满了,并且对象的年龄到达阈值(15)。会将对象加入到老年代中。
  4. 如果新生代和老年代都满了,会触发Full GC。如果GC后还是装不下新对象就会触发OOM了
  5. 如果特别大的对象,会直接到老年代还是装不下就OOM,其中也会触发GC

image-20230912100241063

相关VM参数

image-20230912103227442

垃圾回收器

串行

单线程

堆内存较小

GC线程开始回收, 会导致其他用户线程阻塞,直到GC线程回收完成后才能继续运行

image-20230913215626271

吞吐量优先

多线程,多核cpu

区间内STW时间最短, 发生GC次数少,堆内存较大、

发生垃圾回收的时候,会创建多个GC线程同时回收垃圾,并且回收的同时CPU会迅速跑到100%

image-20230913215932755

响应时间优先 CMS

多线程,多核cpu

单位时间STW最短, 频繁发生GC,堆内存较大

垃圾回收的时候,GC线程会先进行标记,此时其他用户线程也会进行阻塞,然后其他用户线程就可以与GC线程并行执行,GC线程继续标记,重新标记的时候又会使用户线程进行阻塞,最后用户线程又可以并行执行,GC线程开始回收垃圾。GC线程回收垃圾的同时,其他用户线程也会生产垃圾。回收失败也会造成CPU急速增加到100%

image-20230913220932867

G1

Garbage First

image-20230913230229171

回收阶段

先是新生代收集,然后是新生代收集 + 并发标记,然后混合收集。随后循环执行

image-20230913230543396

Young Collection

G1垃圾回收器会将堆内存划分为多个区域,每个区域可以是伊甸园、幸存区和老年代

image-20230913231121222

当伊甸园被占满了,会触发新生代的垃圾回收,并且引起STW。将伊甸园中的幸存的对象复制到幸存区。

image-20230913231441220

当幸存区内存紧张的时候,会将幸存区中的年龄大于一定数的幸存对象放入到老年代中。

image-20230913231657621

Young Collection + CM

当老年代内存占用达到阈值时,会进行并发标记,不会STW

image-20230913231918296

Mixed Collection

对伊甸园、幸存区、老年代进行垃圾回收,并且在混合收集中会优先收集那些垃圾最多的区域。故优先收集老年代

最终标记,会引起STW

拷贝存活,会引起STW

image-20230913232454378

Young Collection 跨代引用

新生代回收的跨代引用(老年代引用新生代)问题

新生代回收时会去遍历GC ROOT,但是老年代中也会有引用了新生代的对象,此时通过脏卡来标记新生代对象。以便回收时不用全遍历老年代,提高效率

image-20230914181649073

image-20230914184527266

Remark

黑色处理完,存活

灰色正在处理,被黑色引用,存活

白色未处理,被灰色引用,存活

对于多线程操作,会对引用添加一个写屏障,并且将这个对象加入到队列中,等到并发标记结束后,将队列中的对象取出来进行重新标记

image-20230914185521672

JDK 8u20 字符串去重

优点:节省大量内存

缺点:会造成轻微的cpu运算,新生代回收时间轻微增加

字符串底层使用的是char数组,G1会使新建的字符串如果内存相同就会指向相同的char数组引用

String的intern方法注重的是String对象(一个String对象)

而G1则是char数组对象(还是两个String对象)

1
2
String s1 = new String("abc")
String s2 = new String("abc")

JDK 8u40 并发标记卸载

所有对象都经过并发标记后,就能知道那些类不再使用,当一个类加载器的所有类都不在使用,则卸载它所有加载的类

JDK 8u60 回收巨型对象

一个对象大于region的一半时,称为巨型对象

G1 不会对巨型对象进行拷贝

回收时被优先考虑

G1会跟踪老年代所有incoming引用,当老年代的incomeing引用为0的巨型对象就可以在新生代回收的时候被处理掉

垃圾回收调优

查看当前配置信息

java -XX:+PrintFlagsFinal -version | grep GC

java -XX:+PrintFlagsFinal -version | findstr GC

相关工具

jmap, jconsole….

确定目标

低延迟还是高吞吐量

CMS, G1, ZGC 低延迟

parallelGC 高吞吐量

最快的GC是不发生GC

查看FullGC前后内存的占用,然后考虑一下问题

数据太多

数据表示太臃肿

​ 对象图

​ 对象大小

是否内存泄露

新生代调优

TLAB thread-local allocation buffer

死亡对的回收代价是零

大部分用过就死

推荐修改新生代堆内存大小(25%-50% 堆内存)

并发数 * (请求数据 + 相应数据)

晋升阈值配置得当,让长时间存活的对象尽快到老年代

老年代调优

观察再查看是否是老年代造成的;如果是再调大老年代内存

JVM规范类文件结构

image-20230915110916560

多态原理步骤

当执行 invokevirtual指令时

  1. 通过栈找到具体对象地址
  2. 分析对象头,找到对象Class
  3. 根据Class中的vtable(虚方法表, 加载类链接阶段就处理好了), 找到具体方法的地址
  4. 执行方法

类加载

1、加载

将类的字节码载入到方法区中,内部采用C++的instanceKlass描述java类,它的重要field有:

  1. _java_mirror即java的类镜像,例如对String来说,就是String.class,作用是把klass暴露给java使用
  2. _super即父类
  3. _fileds 即成员变量
  4. _methods 即方法
  5. _constants 即常量池
  6. _class_loader 即类加载器
  7. _vtable 虚方法表
  8. _itable 接口方法表

如果这个类还有父类没有加载,就先加载父类

加载和链接可能是交替运行的

image-20230918224640904

2、链接

1、验证

对类型文件进行验证,是否是Java文件,例如魔数验证

2、准备

  1. static变量在JDK7之前存储在instanceKlass后, JDK7开始存储与class对象后
  2. static变量分配空间和赋值是两个步骤,分配空间在准备阶段,赋值在初始化阶段
  3. 如果static变量是final的基本类型,以及字符串常量,那么在编译阶段就确定了,赋值在准备阶段完成
  4. 如果static变量是final的,但属于引用类型,那么赋值会在初始化阶段完成

3、解析

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

3、初始化

发生时机

image-20230919094842837

类加载器

image-20230615164339790

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 双亲委派源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 父类加载器
c = parent.loadClass(name, false);
} else {
// boot 类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 需重写findClass, 自定义加载器
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}