JVM内存结构学习

对应视频点这里😁黑马家的
JVM内存结构
JVM垃圾回收与调优学习
JVM字节码技术与Java语法糖字节码分析
JVM类加载过程和编译器优化

思维导图:

image-20220601142405135

先贴一张1.8jdk的JVM的架构图

image-20220531143854437

再来一张中文版本的

image-20220531144228815

所以JVM内存模型我们主要学习这五个方面:

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区

1. 程序计数器

在java中使用CPU寄存器作为程序计数器

作用:是记住下一条JVM指令的执行地址

特点:

  1. 是线程私有的,每个线程都有自己的程序计数器,用来记录程序运行到了那个位置
  2. 不会存在内存溢出(java中唯一不用考虑内存溢出的地方)

2. 虚拟机栈

2.1 定义

Java Virtual Machine Stacks(Java虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 活动栈帧就是栈顶的栈帧

常见面试题:

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

不涉及,因为栈内存会随着方法出栈释放掉,不需要GC管理。GC只回收堆内存的无用垃圾

栈内存的分配越大越好吗?

栈内存划分的越大,系统运行产生的线程数就越少,例如给线程栈划分10M内存,但是系统只有500M的资源,那么就只能产生10个线程,所以并不是划分的越大越好

我可以通过JVM指令来分配栈空间,如果不指定会默认分配。Java官方文档

image-20220531151856031

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

  • 如果方法内部局部变量没有逃离方法的作用访问,它是线程安全的
  • 如果是局部变量引用了对象,并逃离了方法,就不是线程安全的

不会,因为每一个线程都会产生一个单独的栈帧(局部变量是线程私有的),不会出现共享资源抢占的问题,所以不会有线程安全问题

但是如果是变量是static,是多个线程共享的会产生线程安全问题

2.2 栈内存溢出(java.lang.StackOverflowError)

以下情况可能会导致栈内存溢出:

  • 栈帧过多导致栈内存溢出(例如不合理的递归调用)
  • 栈帧过大导致栈帧溢出
  • 类的循环引用导致内存溢出

2.3 线程运行诊断

案例1: cpu 占用过多

定位

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

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

互斥、请求保持、相互等待、不可剥夺

3. 本地方法栈

image-20220531174919845

本地方法栈就是存放native方法的空间,线程私有

4. 堆

image-20220531180653959

4.1定义

Heap 堆

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

特点:

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

4.2 堆内存溢出(java.lang.OutOfMemoryError: Java heap space)

栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!

4.3 堆内存诊断

  1. jps工具
  2. jmap工具
    • 查看堆内存占用情况:jmap -heap 进程id
  3. jconsole 工具
    • 图形界面的,多功能的监测工具,可以连续监测
  4. jvisualvm

5. 方法区(Method Area)

image-20220531185124696

5.1 定义

方法区官方定义:

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

Java 虚拟机有一个方法区域,该区域在所有 Java 虚拟机线程之间共享。方法区域类似于常规语言或操作系统进程中的“文本”段的编译代码的存储区域。它存储每个类的结构,如运行时常量池字段方法数据,以及方法和构造函数的代码,包括用于类和实例初始化和接口初始化的特殊方法(2.9)。

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

方法区域在虚拟机启动时创建。虽然方法区域在逻辑上是堆的一部分,但简单实现可以选择不对其进行垃圾收集或压缩。本规范并不要求方法区域的位置或用于管理已编译代码的策略。所述方法区域可以是固定的大小,或者可以根据计算的要求扩大,如果不需要更大的方法区域,则可以缩小。方法区域的内存不需要是连续的。

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

Java 虚拟机实现可以为程序员或用户提供对方法区域的初始大小的控制,以及在变大小方法区域的情况下对最大和最小方法区域大小的控制。

The following exceptional condition is associated with the method area:

下列异常情况与方法区域相关联:

  • If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

    如果方法区域中的内存不能用于满足分配请求,则 Java 虚拟机抛出 OutOfMemoryError。

5.2 方法区组成

Hotspot JVM的结构,可以看到堆空间是发生了改变

image-20220531190648933

5.3 方法区内存溢出

1.8 以前会导致永久代(PermGen)内存溢出

 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
 -XX:MaxPermSize=8m

1.8 之后会导致元空间内存溢出

 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 -XX:MaxMetaspaceSize=8m

5.4 运行时常量池

二进制字节码注册(类基本信息、常量池、类方法定义、虚拟机指令)

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

5.5 StringTable

StringTable的特性:

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

5.6 StringTable位置

不同版本的JVMStringTable存放的位置不一样,jdk1.6以后的版本将StringTable的位置由PermGen改到了Heap中,主要是因为永久代的垃圾回收需要Full GC(重量级GC),而Heap只需要Monir GC就能回收垃圾,而常量池是经常用到所以进行了优化

image-20220601094510963

5.7 StringTable垃圾回收

我们先演示一段简单的代码:

/**
 * 演示StringTale垃圾回收
 * 堆空间大小      打印信息StringTable的信息       打印GC信息(GC次数、时间等)
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 *
 *
 * @since: 2022/6/1 10:06
 * @author: 梁峰源
 */
public class GCStringTable {
    public static void main(String[] args) {
        int i = 0;
        try {

        }catch (Throwable e){
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

我们需要添加一些虚拟机参数再执行:

堆空间大小      打印信息StringTable的信息       打印GC信息(GC次数、时间等)
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc	

执行结果为:

0
Heap
 PSYoungGen      total 2560K, used 1892K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 92% used [0x00000000ffd00000,0x00000000ffed9180,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
 Metaspace       used 3242K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 344K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13500 =    324000 bytes, avg  24.000
Number of literals      :     13500 =    602424 bytes, avg  44.624
Total footprint         :           =   1086512 bytes
Average bucket size     :     0.675
Variance of bucket size :     0.674
Std. dev. of bucket size:     0.821
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1698 =     40752 bytes, avg  24.000
Number of literals      :      1698 =    173864 bytes, avg 102.393
Total footprint         :           =    694720 bytes
Average bucket size     :     0.028
Variance of bucket size :     0.028
Std. dev. of bucket size:     0.169
Maximum bucket size     :         3

我们看第20行StringTable statistics,StringTable底层的实现是HashTable,我们知道HashTable的底层实现是数组+链表+红黑树

数组的个数我们称为bucket(桶),桶的个数我们称之为buckets,对应第27行,我们可以看到在本例中有60013个桶,第22行Number of entries表示键值对共有1698个,第23行literals表示字符串常量的个数,有1698

我们现在更改一下代码:

public class GCStringTable {
    public static void main(String[] args) {
        int i = 0;
        try {
            for (int j = 0; j < 100; j++) {
                String.valueOf(j).intern();//将字符串入串池
                i++;
            }
        }catch (Throwable e){
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

我们将100个字符串加入到字符串串池也就是StringTable中,再执行一下:

StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1795 =     43080 bytes, avg  24.000
Number of literals      :      1795 =    178496 bytes, avg  99.441
Total footprint         :           =    701680 bytes
Average bucket size     :     0.030
Variance of bucket size :     0.030
Std. dev. of bucket size:     0.173
Maximum bucket size     :         3

可以看到100个字符串并没有达到我们设置堆空间的阈值10M,现在还没有触发垃圾回收,现在我们将字符串的数量增大到10000,查看结果:

StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      7514 =    180336 bytes, avg  24.000
Number of literals      :      7514 =    453560 bytes, avg  60.362
Total footprint         :           =   1114000 bytes
Average bucket size     :     0.125
Variance of bucket size :     0.131
Std. dev. of bucket size:     0.362
Maximum bucket size     :         3

我们发现Number of literals的值并没有增加10000,我们可以看到最上面有一行代码:

[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->866K(9728K), 0.0017615 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

这行代码表示堆空间不足,触发了新生代的Monir GC,所以速度还是很快的

5.8 StringTable性能调优

我们知道StringTable的底层是hashTable,hashTable的性能和元素个数也就是桶的数量有关的。

如果hashTable中桶的个数比较多,那么它就越分散,hash碰撞的几率就越小

如果桶的数量较少,它就越几种,hash碰撞的几率就越大

那么如何调优呢?

  • 调整 -XX:StringTableSize=桶个数(最少设置为 1009 以上)
  • 考虑将字符串对象是否入池(用intern函数入池,如果池中有返回池中的对象)

6. 直接内存

6.1 定义

Direct Memory (直接内存)不属于JVM管理,属于操作系统内存

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

6.2 文件读取过程

我们在使用NIO进行数据读取时的流程为:

image-20220601111316318

可以看到其实读一个文件会产生两个缓存区,分别是系统缓存区Java缓存区,这样数据明显冗余了,所以传统NIO方式读取文件效率低下

我们来看BIO方式下读取:

image-20220601111500612

我们通过ByteBuffer.allocateDirect获取了一块直接内存,这块直接内存是操作系统和Java都可以共同访问的,所以读写性能是非常快的

6.3 直接内存释放原理

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

直接内存的释放是调用了unsafe类进行释放的

unsafe#allocateMemory(int size) //获得申请空间的地址

unsafe#setMemory(int size) // 申请空间

unsafe#freeMemory(long base) // 释放空间,需传入需要释放空间的地址

我们看对应源码

ByteBuffer#allocateDirect(int capacity)

image-20220601114424490

现在我们知道了当ByteBuffer对象被GC的时候,才会回收我们的直接内存,但是有的时候为了防止程序员频繁使用System.gc()(这是full GC),我们会用命令关闭手动full gc

-XX:+DisableExplicitGC   //关闭显示full GC

这个时候我们的ByteBuffer对象如果没有被回收就会导致直接内存一直不会被回收

我们的解决方法是调用

unsafe#freeMemory(long base) // 释放空间,需传入需要释放空间的地址

自己手动的释放内存

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值