JVM 内存结构详解

大家好 我是积极向上的湘锅锅💪💪💪

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

1. 程序计数器

在这里插入图片描述

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

从上面的介绍中我们知道了程序计数器主要有两个作用:

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
⚠️ 注意 :每一个线程都有自己的程序计数器
⚠️ 注意 :程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。


2. 虚拟机栈

在这里插入图片描述
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作
在这里插入图片描述

  • 局部变量表 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
  • 操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中
  • 动态链接 主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用

程序运行中栈可能会出现两种错误:

  • StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误,还有一种就是循环依赖也会导致该错误。
  • OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

问题辨析:

  1. 垃圾回收是否涉及栈内存
    不涉及,只涉及堆和方法区,也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束,执行完自动释放
  2. 栈内存越大越好吗
    可以通过-Xss来设置,windows的栈大小是根据虚拟内存来默认配置的,如果栈内存过大,那么可运行的线程数就会减少,所以并不是越大就越好
  3. 方法内的局部变量是否线程安全?
    • 如果一个变量x是方法的局部变量,还是基本数据类型,那么每一个线程的栈帧里面都是独立的的x,存放在自己局部变量表里面,线程安全
    • 如果这个变量是做为参数时候,那么既有可能别的线程也能访问到,比如说主线程传递给另一个线程同样的参数,那么在程序启动的时候,极有可能发生同时修改的情况,线程不安全
    • 如果这个变量是对象,还做为返回值,那么在高并发的情况下是有可能拿到这个返回对象的,线程不安全

拓展:

  1. 线程运行诊断(CPU占用高)

    • Linux中通过top命令,查看个进程CPU占用情况,找到占用过高的进程
    • 使用ps H -eo pid,tid,%cpu | grep 32655 32655是那个占用过高的进程pid
    • 这时就可以看到到底是哪个线程tid的cpu占用率过高
    • jstack 进程id,根据上个步骤得出的线程tid换算为十六进制,在列出的线程里面找到对应的nid
    • 此时已经找出对应的线程了,可以查看线程的相关信息找出错误

3. 本地方法栈

在这里插入图片描述
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。比如object类的wait方法

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

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


4. 堆

在这里插入图片描述
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存

Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存

堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

  • java.lang.OutOfMemoryError: GC Overhead Limit Exceeded : 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  • java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值

堆内存诊断

在程序运行的时候,在命令行使用jconsole可以调出可视化的堆内存管理界面,这个是jdk自带的,直接在命令行使用即可
在这里插入图片描述

问题辨析:

  1. 垃圾回收之后,内存占用仍然很高,改怎么排查?

这里需要用到另一个工具,jvisualvm,打开后是下面界面就成功了

在这里插入图片描述
比如我启动一个程序,叫做Day2,pid为14388,此时可以在这个图形化界面中找到

在这里插入图片描述
此时最重要的要关注右上角的堆Dump
在这里插入图片描述
进去之后点击查找
在这里插入图片描述
点击是就可以了
在这里插入图片描述
就可以看到所有占内存最大排名二十的类

在这里插入图片描述
点击进去查看,找到对应占最多的类,对应处理就可以了
这里主要是学方法


5. 方法区

在这里插入图片描述
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,方法区到底要如何实现那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的。

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

永久代 (PermGen) 与元空间 (MetaSpace)的关系
方法区是一块逻辑区域,所有需要对方法区的实现,所以永久代是 JDK 1.8 之前的方法区实现占用的是堆内存,JDK 1.8 及以后方法区的实现变成了元空间使用的是本地内存

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小

问题解析:

  1. 在JDK1.8之前,永久代是怎么溢出的?
    由于是使用的堆内存,所以只要类创建过多,堆内存是很有可能不够的
    可以设置以下参数
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
  1. 在JDK1.8之后,元空间是怎么溢出的?
    由于使用的是本地内存,但是相对来说比堆内存大很多,可以设置参数模拟一下
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
  1. 溢出场景:

    • spring的cglib动态代理
    • mybatis的cglib生成mapper接口

5.1 运行时常量池:

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误

5.2 字符串常量池

在这里插入图片描述
JDK1.8字符串常量池设立在了堆里,方便gc

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建

HotSpot 虚拟机中字符串常量池的实现是StringTable,本质上就是一个HashSet ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)

StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象

来看一道经典例题

String a = "a";
String b = "b";
String ab = "ab";
String c = a+b;
System.out.println(c==ab);

毫无疑问是false,那过程到底是什么样的呢?

首先在编译的时候,a,b,ab是存在在了StringTable之中,也就是字符串池,虽然也在堆里,但是也只是包含关系
而c是变量的拼接,底层是stringbuilder进行拼接的,最后是新建一个string对象,而这个对象肯定不是在串池中,属于在串池外的堆里面

所以俩者是互斥的关系,自然不相等

那再换一个

String a = "a";
String b = "b";
String ab = "ab";
String d = "a"+"b";
System.out.println(ab==d);

ab和d是不是相等的呢?
分析一下,d是属于常量的拼接,在javac的编译优化的过程中,常量的拼接结果是确定的,所以在编译的时候会自动变成ab
所以d是在编译的过程中,是去串池看有没有这样一个“ab”存在,如果存在,则直接返回ab的引用,不存在,则在串池中新建一个

答案是true;

趁热再来一道

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

来分析一下这个创建的过程

//字符串池中 a b
//堆中 new String("a") new String("b") new String("ab")  

可以发现池中并没有ab,只有常量的编译才会主动放在字符串池中,而s中的ab是属于变量的拼接,是不会主动放在字符串池里面的,总的说来就是new的在字符串池中,+的不会在字符串池中

如何主动放入池中呢?s.intern()方法
会将这个字符串对象尝试放入串池,如果有则不会放入,如果没有就放入串池,会把串池的对象返回

StringTable的性能调优:
由此在创建字符串的时候,尽量采取intern()将字符串入池,来减少不必要的重复字符串的创建,这对不管是堆内存还是gc都是有好处的


6. 直接内存

基本使用:
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,是被操作系统所管理,如果这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。

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

释放原理:
JVM是无权回收操作系统的内存的,也就是垃圾回收不会管理直接内存,如果DirectByteBuffer回收掉了,那留在直接内存的空间该怎么办呢

实际是使用了unsafe类做了内存释放的操作(感兴趣的可以看源码)

何为unsafe类?

Unsafe是位于 sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java运行效率、增强 Java语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe类使 Java语言拥有了类似 C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe类会使得程序出错的概率变大,使得 Java这种安全的语言变得不再“安全”,因此对 Unsafe的使用一定要慎重。

在这里插入图片描述
整个过程如下:

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法

  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程(守护线程)通过Cleaner的clean方法调用freeMemory 来释放直接内存

问题解析:

如何解决显式回收(主动gc)带来的性能影响?
在jvm调优的时候,一般是禁止显式回收的,只有等到真正的gc才会回收,但是直接内存空间得不到释放该怎么办呢,可以直接调用unsafe类的freeMemory


参考:JavaGuide

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

owensweat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值