Home
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • JavaSE
  • JVM
  • JUC
  • Netty
  • CPP
  • QT
  • UE
  • Go
  • Gin
  • Gorm
  • HTML
  • CSS
  • JavaScript
  • vue2
  • TypeScript
  • vue3
  • react
  • Spring
  • SpringMVC
  • Mybatis
  • SpringBoot
  • SpringSecurity
  • SpringCloud
  • Mysql
  • Redis
  • 消息中间件
  • RPC
  • 分布式锁
  • 分布式事务
  • 个人博客
  • 弹幕视频平台
  • API网关
  • 售票系统
  • 消息推送平台
  • SaaS短链接系统
  • Linux
  • Docker
  • Git
GitHub (opens new window)
Home
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • JavaSE
  • JVM
  • JUC
  • Netty
  • CPP
  • QT
  • UE
  • Go
  • Gin
  • Gorm
  • HTML
  • CSS
  • JavaScript
  • vue2
  • TypeScript
  • vue3
  • react
  • Spring
  • SpringMVC
  • Mybatis
  • SpringBoot
  • SpringSecurity
  • SpringCloud
  • Mysql
  • Redis
  • 消息中间件
  • RPC
  • 分布式锁
  • 分布式事务
  • 个人博客
  • 弹幕视频平台
  • API网关
  • 售票系统
  • 消息推送平台
  • SaaS短链接系统
  • Linux
  • Docker
  • Git
GitHub (opens new window)
  • JVM内存模型
    • 内存结构
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
    • 堆
    • 方法区
    • 运行时常量池
    • StringTable
    • 直接内存
    • 创建一个对象步骤
    • 对象访问定位?
  • 类加载
  • 垃圾回收
  • 调优与问题排查
  • JVM
Nreal
2023-07-02
目录

JVM内存模型

# 内存结构

  • 运行时数据区:
    1. 线程共享:堆(对象+字符串常量池)
    2. 线程私有:虚拟机栈+本地方法栈+程序计数器
  • 本地内存:
    • 元空间(运行时常量池)
    • 直接内存

jdk1.7 方法区的实现为永久代,且在运行时数据区中;

jdk1.8之后用本地内存实现元空间;

# 程序计数器

作用:记住下一条jvm指令的执行地址 特点:

  • 是线程私有的
  • 不存在内存溢出

# 虚拟机栈

  • 每个线程运行时所需要的内存,称为虚拟机栈

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,都会产生一个新的栈帧

    栈帧:参数,局部变量,返回地址

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

**q1:**栈内存越大越好?

栈内存越大,线程数越少,因为物理内存是一定的!

**q2:**方法内局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用范围是线程安全的,如果是局部变量引用了对象,并逃离方法的作用范围,则需要考虑线程安全!

public static StringBuilder m(){
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    return sb;//对象作为返回值逃离作用范围,不安全
}
1
2
3
4
5

栈内存溢出:

  • 栈帧过多导致栈内存溢出

  • 栈帧过大导致栈内存溢出

    案例:@JsonIgnore

public class Demo {
    public static void main(String[] args) throws JsonProcessingException {
        Dept d = new Dept();
        d.setName("Market");

        Emp e1 = new Emp();
        e1.setName("zhang");
        e1.setDept(d);

        d.setEmps(Arrays.asList(e1));
        // { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(d));
    }
}
class Emp {
    private String name;
    @JsonIgnore
    private Dept dept;
}
class Dept {
    private String name;
    private List<Emp> emps;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

CPU占用过多:

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id,可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

# 本地方法栈

本地方法栈为虚拟机使用到的 Native 方法服务

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。

# 堆

特点:

  • 线程共享,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

更改堆内存

VM options: -Xmx 8m

Terminal堆内存诊断:

  1. jps 工具 查看当前系统中有哪些 java 进程
  2. jmap 工具 查看堆内存占用情况 jmap - heap 进程id
  3. jconsole 工具 图形界面的,多功能的监测工具,可以连续监测

TLAB:

为什么要有TLAB?

  1. 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
  2. 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
  3. 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

什么是TLAB?

  1. 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
  2. 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

1、每个线程都有一个TLAB空间

2、当一个线程的TLAB存满时,可以使用公共区域(蓝色)的

# 方法区

方法区是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

永久代&元空间:

方法区内存溢出:

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

spring和mybatis中cglib动态代理在运行期间生成大量类,可能导致永久代内存溢出,1.8之后使用元空间是系统内存,相对充裕很多,垃圾回收机制也是元空间自行管理,不会像永久代垃圾回收效率那么低

# 运行时常量池

out目录下的类反编译:javap -v xx.class

Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
...
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

# StringTable

例题:

// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
String s1 = "a"; // 懒惰的,执行到时才创建
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; //false
// new StringBuilder().append("a").append("b").toString()  new String("ab"),在堆中
String s5 = "a" + "b"; //true  
// javac 在编译期间的优化,结果已经在编译期确定为ab,在串池中找到了
1
2
3
4
5
6
7
8
9
10
//  ["ab", "a", "b"]
public static void main(String[] args) {
    String x = "ab";
    String s = new String("a") + new String("b");
    // 堆  new String("a") new String("b") new String("ab")
    String s2 = s.intern(); 
    // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    System.out.println( s2 == x);//t
    System.out.println( s == x );//f
}
1
2
3
4
5
6
7
8
9
10
//  ["ab", "a", "b"]
public static void main(String[] args) {
    String s = new String("a") + new String("b");
    // 堆  new String("a") new String("b") new String("ab")
    String s2 = s.intern();
    // 1.6将堆中对象s拷贝一份放入串池(副本入池),1.8直接放入串池 
    // 返回串池中的对象
    String x = "ab";
    System.out.println( s2 == x);//1.6 t 1.8 t
    System.out.println( s == x );//1.6 f 1.8 t
}
1
2
3
4
5
6
7
8
9
10
11

特性:

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回

性能调优:

  • 调整 -XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池

# 直接内存

示例代码:

static final String FROM = "E:\\..";
static final String TO = "E:\\..";
static final int _1Mb = 1024 * 1024;

public static void main(String[] args) {
    io(); 
    directBuffer(); 
}

private static void directBuffer() {
    long start = System.nanoTime();
    try (FileChannel from = new FileInputStream(FROM).getChannel();
         FileChannel to = new FileOutputStream(TO).getChannel();
    ) {
        ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
        while (true) {
            int len = from.read(bb);
            if (len == -1) {
                break;
            }
            bb.flip();
            to.write(bb);
            bb.clear();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    long end = System.nanoTime();
    System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}

private static void io() {
    long start = System.nanoTime();
    try (FileInputStream from = new FileInputStream(FROM);
         FileOutputStream to = new FileOutputStream(TO);
    ) {
        byte[] buf = new byte[_1Mb];
        while (true) {
            int len = from.read(buf);
            if (len == -1) {
                break;
            }
            to.write(buf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    long end = System.nanoTime();
    System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
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

直接内存为Java堆内存和系统内存共享的一块区域,少了一次缓冲区复制操作

分配与回收:

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

禁用显示回收对直接内存的释放:

-XX:+DisableExplicitGC 显式的
System.gc(); // 显式的垃圾回收,Full GC
1
2

# 创建一个对象步骤

  1. 判断对象对应的类是否加载、链接、初始化;

    1. 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。
    2. 如果该类没有加载,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的.class文件,如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class对象。
  2. 为对象分配内存;

    1. 首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小;

    2. 如果内存规整:采用指针碰撞分配内存;

      如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。

    3. 如果内存不规整:空闲列表来为对象分配内存;

      意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为了 “空闲列表(Free List)”

      标记清除算法清理过后的堆内存,就会存在很多内存碎片。

  3. 初始化分配到的空间;

    所有属性设置默认值;

  4. 设置对象的对象头;

    将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中;

  5. 执行init方法进行初始化;

    初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量;

# 对象访问定位?

  1. 句柄

    句柄池中存储的是稳定的句柄地址,对象被移动时只会改变数据指针;

  2. 直接指针;

    速度快,节省一次指针定位开销;

类加载

类加载→

Theme by Vdoing | Copyright © 2021-2024
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式