JVM概述和内存结构

一、JVM概述

定义:

Java Virtual Machine - java程序的运行环境 (java二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界越界检查
  • 多态 (使用虚方法调用的方式实现多态)

比较:

jre jdk jvm

常见的JVM:

二、内存结构

2.1 程序计数器 (Program Counter Register PCR)

  1. 定义:物理上用寄存器
  2. 作用:记住下一条jvm指令的执行地址

  1. 特点:
    • 线程私有,每个线程维护一个程序计数器,随着线程创建而创建,销毁而销毁。
    • 不会存在内存溢出,是一块较小的内存空间

2.2 虚拟机栈

-Xss定义栈大小

定义:

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)(包含参数,局部变量,返回地址)组成,对应着每次方法调用所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题:

  1. 垃圾回收是否涉及栈内存?

​ 垃圾回收只涉及堆内存,不涉及栈内存

  1. 栈内存分配越大越好吗?

​ 并不是越大越好,内存空间是固定的,栈内存越大,可用线程数则越少

  1. 方法内的局部变量是否线程安全?

    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
    // 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
    // 如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全

    //线程安全
    public static void m1() {
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    sb.append(2);
    sb.append(3);
    System.out.println(sb.toString());
    }

    //线程不安全,可以将StringBuilder改为StringBuffer
    public static void m2(StringBuilder sb) {
    sb.append(1);
    sb.append(2);
    sb.append(3);
    System.out.println(sb.toString());
    }

    //线程不安全,因为它作为返回值逃离方法的作用范围。
    public static StringBuilder m2() {
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    sb.append(2);
    sb.append(3);
    return sb;
    }

2.2.1 栈内存溢出

  • 栈帧过多导致内存溢出(错误的递归调用)
  • 栈帧过大导致内存溢出(不常见)

2.2.2 线程运行诊断

案例1:cpu占用过多

定位

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

案例2:程序运行很长时间没有结果

2.3 本地方法栈

调用本地方法时,给本地方法的运行提供内存空间

native方法:不是由java代码编写的方法。因为java代码存在限制,有时候不能够直接和操作系统进行交互。

举例:clone(), hashCode(), notify(), notifyall(), wait()

2.4 堆

定义

Heap 堆

  • 通过new关键字,创建对象都会使用堆内存

特点

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

展开:对于局部变量,如果是基本类型,会把值直接存储在栈;如果是引用类型,比如String s = new String(“william”);会把其对象存储在堆,而把这个对象的引用(指针)存储在栈。

堆内存溢出

-Xmx定义堆内存大小

堆内存诊断

  1. jps

    • 查看当前系统有哪些java进程
  2. jmap工具

    • 查看堆内存占用情况 jmap -heap 进程id
  3. jconsole工具

    • 图形界面的,多功能的检测工具,可以连续检测
  4. jvirsualvm

2.5 方法区

方法区定义:

  1. 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是所有Java虚拟机线程共享的区域。当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
  2. 保存在着被加载过的每一个类的信息;这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中。
  3. 方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。jvm也可以允许用户和程序指定方法区的初始大小,最小和最大限制;
  4. 方法区在虚拟机开启时创建。

2.5.1 组成部分

2.5.2 方法区内存溢出

  • 1.8以前会导致永久代内存溢出
  • 1.8以后会导致元空间内存溢出

场景

  • Spring
  • Mybatis

2.5.3运行时常量池

  • 常量池:一张表,虚拟机指令根据这张常量表找到要执行的而类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是*.class文件中的,当该类被加载时,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址。

2.5.4 StringTable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// StringTable ["a", "b", "ab"] hashtable结构,不能扩容
// 常量池中的信息,都会被加载到运行时常量池中,这时a b ab 都是常量池中的符号,还没有变为java字符串对象
// ldc #2 会把a符号变为"a"字符串对象
// ldc #3 会把b符号变为"b"字符串对象
// ldc #4 会把ab符号变为"ab"字符串对象

String s1 = "a"; // 懒惰的,延迟加载
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2; // new StringBuilder.append("a").append("b").toString() -> new String("ab")
String s5 = "ab";
String s6 = s4.intern();

System.out.println(s3 == s4); // false (s3在StringTable中,s4在堆中)
System.out.println(s3 == s5); // true (javac在编译期间的优化,结果在编译器确定为ab)
System.out.println(s3 == s6); // true

String x2 = new String("c") + new String("d"); // new String("cd")
String x1 = "cd"; // "cd"
x2.intern();

//问:如果调换了【最后两行代码】的位置呢?如果是jdk1.6呢(复制一份放入常量池)?
System.out.println(x1 == x2); // false (常量池 堆) true(堆和常量池 堆和常量池) false(常量池 堆)

2.5.5 StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串常量拼接的原理是编译期间优化
  • 可以用intern方法,主动将串池中还没有的字符串对象放入串池,会把串池的对象返回
    • 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
    • 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池中,会把串池中的对象返回
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
//1.8
//["a", "b", "ab"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b"); // new String("ab")

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回

System.out.println(s2 == x); // true
System.out.println(s == x); // false

}

//1.8
//["a", "b", "ab"]
public static void main(String[] args) {

String s = new String("a") + new String("b"); // new String("ab")

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回

String x = "ab";
System.out.println(s2 == x); // true
System.out.println(s == x); // true

}

//1.8
//["ab","a", "b"]
public static void main(String[] args) {

String s = new String("a") + new String("b"); // new String("ab")

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回

String x = "ab";
System.out.println(s2 == x); // true
System.out.println(s == x); // true

}

//1.6
//["ab","a", "b"]
public static void main(String[] args) {

String s = new String("a") + new String("b"); // new String("ab")

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池中,会把串池中的对象返回

String x = "ab";
System.out.println(s2 == x); // true
System.out.println(s == x); // false

}

2.5.6 StringTable位置

JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。

JDK 1.7 为什么要将字符串常量池移动到堆中?

主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

2.5.7 StringTable 性能调优

  • -XX:StringTableSize=20000 调整桶个数 -XX:+PrintStringTableStatistics

​ StringTable 底层使用哈希表实现的,增加bucket的数量,就可以减少哈希碰撞,从而实现性能调优。

  • 考虑将字符串对象是否入池

2.6 直接内存

定义:

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制

2.6.1 分配和回收原理

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

传统io

使用直接内存

参考:

https://www.bilibili.com/video/BV1yE411Z7AP P1 - P47

https://javaguide.cn/java/jvm/memory-area.html

作者

lin ronghui

发布于

2022-12-20

更新于

2022-12-20

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.