欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

JVM、(二)JVM内存结构

程序员文章站 2022-03-05 13:17:53
文章目录一、程序计数器(PC)定义: Program counter Register作用:记住下一条JVM指令的执行地址。(通过寄存器来实现的)特点:二、虚拟机栈(Virtual Stack)定义:面试问题:栈内存溢出线程运行诊断三、本地方法栈四、堆介绍堆内存溢出堆内存诊断五、方法区构成方法区的内存溢出常量池运行时常量池一、程序计数器(PC)定义: Program counter Register作用:记住下一条JVM指令的执行地址。(通过寄存器来实现的)二进制字节码通过解释器被解释为机...


一、程序计数器(PC)

定义: Program counter Register

作用:记住下一条JVM指令的执行地址。(通过寄存器来实现的)

JVM、(二)JVM内存结构
二进制字节码通过解释器被解释为机器码,然后机器码才能够交给CPU去执行。
当第1条JVM指令被解释翻译执行后 程序计数器会去记录下一条JVM指令的执行地址然后接着执行。

特点:

* **线程私有的**。程序运行时会开启多个线程,CPU会对这些线程进行调度,每个线程有着自己的时间片。当时间到但是线程1还未执行完,线程1的程序计数器便会记录此时执行到的指令的位置;然后切换到线程2去执行,同理线程2也有自己的程序计数器。

JVM、(二)JVM内存结构
* 不会存在内存溢出

二、虚拟机栈(Virtual Stack)

定义:

  1. 栈 - 线程运行需要的内存空间;由多个栈帧(Frame)组成。
  2. 栈帧 - 每个方法调用时需要的内存;
  3. 栈与栈帧的关系 - 每个线程只能有一个活动栈帧,对应着正在执行的那个方法(位于栈顶部的那个栈帧);
  4. 一个栈中可能存在多个栈帧。
    比如说某个方法执行时会需要一些参数、局部变脸、返回地址。这些都被记录在栈帧这个内存空间中。也可能在执行该方法时还调用了其余的方法,此时该栈中就会存储多个栈帧,当方法执行完后栈帧便会出栈,内存自动释放掉。
    JVM、(二)JVM内存结构

面试问题:

  1. 垃圾回收是否涉及栈内存?
    不涉及,因为栈内存是随着线程中方法执行的结束而弹出栈自动释放掉的。
  2. 栈内存分配越大越好吗?
    不是,因为物理内存的大小是一定的。如果栈内存划分大,那么能够执行的线程数就会减少,它只能够方便进行更多次的方法递归调用,不会增大方法运行的效率。
    JVM、(二)JVM内存结构
  3. 方法内的局部变量是否是线程安全的?
    如果方法内局部变量没有逃离方法的作用范围就是线程安全的;反之不安全,比如局部变量作为方法返回值返回,有可能被其它线程使用。
    举例:
    JVM、(二)JVM内存结构
    方法1中的 StringBuilder是线程私有的,因为它是作为局部变量出现的;
    方法2中的不是线程安全的,因为作为引用类型出现它可能还会被其它线程所引用,比如说在main 方法中开启了新线程调用 m2(StringBuilder sb)方法。
    方法3中的 StringBuilder不是线程安全的,因为它将 StringBuilder作为返回值返回,那么其它线程就有可能拿到这个值去修改 。

栈内存溢出

  • 栈帧过多(递归爆炸);
  • 栈帧过大(不太容易出现);
  • 实例:
    JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构

线程运行诊断

  1. CPU占用过多:
  • 使用 top 定位哪个进程对CPU的占用过高;
  • ps H -eo pid,tid,%cpu | grep 进程id (使用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id 获取指定进程的线程运行情况;可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号。
    JVM、(二)JVM内存结构
    但是 jstack 打印出的线程编号是 16进制的,需要首先将 十进制的存在问题的线程编号换算为16进制然后去 jstack 打印结果中寻找。
  1. 程序运行很长时间没有结果
  • 比如说死锁问题
    JVM、(二)JVM内存结构

三、本地方法栈

  • 概念:Java代码调用本地方法时所占用的内存。给本地方法的运行提供内存空间。
    本地方法举例:
    JVM、(二)JVM内存结构

四、堆

介绍

  • 概念: 通过 new 关键字创建的对象都会使用到堆内存;
  • 特点:
    • 堆是线程共享的,堆中的对象都需要考虑线程安全问题;
    • 有垃圾回收机制。

堆内存溢出

  • 举例:
    JVM、(二)JVM内存结构
    报错:java.lang.OutOfMemoryError:java heap space
    可以通过 -Xmx堆内存大小 来设置程序中堆的内存
    JVM、(二)JVM内存结构

堆内存诊断

  1. jps 工具: 查看当前系统中有哪些 java进程;
  2. jmap 工具:查看堆内存占用情况;
  3. jconsole 工具:图形界面的,多功能的监测工具,可以连续监测;
    注意使用时需要保证 项目jdk版本和 idea版本一致。
    详细参考jhsdb jmap --heap --pid 进程号** **报错
    IntelliJ IDEA设置JDK版本

应用实例1:
JVM、(二)JVM内存结构JVM、(二)JVM内存结构
jps : 查看系统中的java进程。 如果报错需要切换到 jdk安装目录的bin目录下进行。
JVM、(二)JVM内存结构
jmap -heap 进程id:查看对应进程的堆内存占用情况

JVM、(二)JVM内存结构
堆分配前:
JVM、(二)JVM内存结构
堆分配后:
JVM、(二)JVM内存结构
gc回收后:
JVM、(二)JVM内存结构

应用案例2:
JVM、(二)JVM内存结构
使用 jvisualvm 来监控进程的堆内存分配:
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构

五、方法区

构成

  1. 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

  2. 方法区与堆一样都是各个线程共享的内存区域;

  3. 方法区在JVM启动的时候被创建,并且它的实际物理内存空间中和Java堆区一样都可以是不连续的;

  4. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGen space 或者 java.lang.OutOfMemoryError:Metaspace
    JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构

    JVM、(二)JVM内存结构

  • jvm1.6 用了一个永久代(PermGen)作为方法区的实现,永久代中包含了运行时常量池(StringTable)、类加载器(ClassLoader)、类的相关信息(Class);
  • jvm1.8 后用元空间代替了永久代作为方法区的实现。元空间和方法区类似,都是对JVM中方法区的实现,不过元空间与永久代的最大区别在于:元空间不在虚拟机设置的内存中、而是使用本地内存。不过1.8将 StringTable移动到了堆中。这是因为永久代的回收效率很低(需要 FULLGC时才能触发垃圾回收 )这样会占用大量的内存,而StringTable在堆中只需要 Minor GC时便能够触发垃圾回收,能够大大减轻字符串对内存的占用。
  • 运行时常量池 VS 常量池:
    方法区中包含了运行时常量池;字节码文件中包含了常量池;一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包含各种字面量和对类型、域和方法的符号引用。
  • 为什么需要常量池?
    一个Java 源文件中的类、接口 编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存储在字节码中,换另一种方式,可以存到常量池中,这个字节码包含了指向常量池的引用。在动态连接的时候回到运行时常量池。
  • 常量池中有什么?
    数量值、字符串值、类引用、字段引用、方法引用。
  • 运行时常量池:
    • 运行时常量池是在jvm虚拟机完成类装载操作之后,将class文件中的常量池载入到内存中,并保存在方法区中。
    • 运行时常量池中包含多种不同的常量,包括编译器期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
    • 运行时常量池相对于Class文件中的常量池的另一重要特征是:具备动态性。

方法区的内存溢出

JVM、(二)JVM内存结构
通过循环20000次来往方法区中加载多个类的信息,使得方法区内存溢出。

JVM、(二)JVM内存结构
但是只有在 JVM1.6之前才有可能产生方法区内存溢出,因为它使用的是虚拟机的内存,而JVM1.8以后使用的是本地内存很大不会溢出。但是在修改了虚拟机参数后可以做到内存溢出。 -XX:MaxMetaspaceSize=8m
JVM、(二)JVM内存结构

常量池

  • 常量池就是一张表,字节码文件中的虚拟机指令根据这张表找到需要执行的类名、方法名、参数类型、字面量等信息。
  • 字节码文件中包含了 类的基本信息(版本号、字段),常量池,类方法定义,虚拟机指令。 查看字节码信息: javap -v xxx.class

JVM、(二)JVM内存结构

JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构

运行时常量池

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

StringTable 面试题:

JVM、(二)JVM内存结构

StringTable 实例:

  1. 基础的 String str = "xxx"
    JVM、(二)JVM内存结构
    常量池中的信息,都会被加载到运行时常量池中。开始时 a、b、ab 都是作为常量池中的符号,还没有变为 java字符串对象。等到程序运行到这里时,字节码中的虚拟机指令(ldc)会将该符号变为字符串对象,并且放入到串池(StringTable,是一个 hashtable结构,不能扩容)中。每次都会先到串池中检查是否有对应的字符串,如果不存在再创建然后加入。

  2. 字符串拼接 String s4 = s1 + s2
    JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构
    因此得到结果为 false

  3. 字符串拼接 String s4 = "a" + "b"
    JVM、(二)JVM内存结构
    底层做法:javac在编译期间的优化,结果已经在编译期间确定为 “ab”;而 String s4 = s1 + s2 中s1与 s2均为变量可能在之后发生变化,因此不能提前确定,必须在运行期间使用 StringBuilder 获取。

  4. 使用 intern 方法主动将字符串放入串池
    JDK 1.8下:
    JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构

JVM、(二)JVM内存结构
JDK 1.6下:
JVM、(二)JVM内存结构
s.intern()

  • 会尝试将字符串s 放入到串池中,如果串池中有则不会放入,如果没有则放入。最终会返回串池中的对象。(1.7、1.8中);
  • 会将字符串s 拷贝一份并将该拷贝的那份放入串池中,而原本的字符串s 如果存在于堆中,此时仍然在堆中。最终返回串池中的对象(1.6中)

StringTable特性:

  1. 常量池中的字符串仅是符号,第一次用到才变为对象;
  2. 利用串池的机制,来避免重复创建字符串对象;
  3. 字符串变量拼接的原理是 StringBuilder(1.8中);
  4. 字符串常量拼接的原理是编译期优化;
  5. 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池。

StringTable位置:

1.8以前 StringTable 位于永久代中,需要满足 FULLGC时才能触发垃圾回收,会造成大量内存空间无法被回收;
1.8以后 StringTable 位于堆中,需要满足 MinorGC 时才能触发垃圾回收,因此能够大大减少字符串占用的内存空间,及时回收。

  • 证明如下:
    JVM、(二)JVM内存结构
    jvm1.6 下循环26000次往 StringTable中放入字符串,同时设置永久代的最大内存空间:-XX:MaxPermSize=10m
    此时会报错 OutOfMemoryError :PermGen space,说明1.6中 StringTable确实位于永久代中。

JVM、(二)JVM内存结构
jvm1.8下会报错 GC overhead limit exceeded,这是由于垃圾回收的一个限制导致的UseGCOverheadLimit(如果98%的时间花费到垃圾回收上,但是只有2%的堆空间被回收,就会触发该错误)。
JVM、(二)JVM内存结构
此时需要添加相应的虚拟机参数:-Xmx10m -XX:-UseGCOverheadLimit
JVM、(二)JVM内存结构
如图所示,此时报错 Java heap space 堆空间不足,证明1.8中 StringTable位于堆中。

StringTable垃圾回收机制:

虚拟机配置: -Xmx10m -XX:+PrintStringTableStatisttics -XX:+PrintGCDetails -verbose:gc
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构

StringTable性能调优:

  1. 调整StringTable 中桶的个数:
  • 命令: -XX:StringTableSize=桶个数(最小1009)
    JVM、(二)JVM内存结构JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构
  1. 考虑将字符串对象是否入池
    JVM、(二)JVM内存结构
    JVM、(二)JVM内存结构
    打开 jvisualvm可以发现String、char数组占用了80%的内存;

JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
可以发现字符串入池后 占用的内存只有30%左右

六、直接内存

介绍

  • Direct Memory,直接内存是属于操作系统的内存;
  • 常见于 NIO操作时,用于数据缓冲区;
  • 分配回收成本较高,但是读写性能高;
  • 不受 JVM 内存回收管理;

案例对比

JVM、(二)JVM内存结构
JVM、(二)JVM内存结构
JVM、(二)JVM内存结构

图示

JVM、(二)JVM内存结构
首先Java代码本身不具有读写文件的能力,它需要调用操作系统的方法来进行文件读写。CPU 方面会由用户态切换为内核态;内存方面会在系统内存划分一块系统缓存区,将磁盘文件读取到系统缓冲区然后再将系统缓冲区中的文件数据读取到Java 缓冲区(堆内存分配的)。
但是这样依赖就需要开辟两份缓冲区,这样会造成不必要的数据复制,效率不高。


JVM、(二)JVM内存结构
使用 ByteBuffer 后则会开辟一块Java和系统都能够共享的区域 direct memory(直接内存),这样一来就不用重复复制效率便提高了。

内存释放原理

JVM、(二)JVM内存结构
JVM、(二)JVM内存结构

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

JVM、(二)JVM内存结构

禁用显示回收对直接内存的影响

一般会禁用 System.gc() 来避免显式的进行垃圾回收。但是显式gc被禁用 ByteBuffer就不能被回收,相应的直接内存就得不到释放。因此一般推荐使用 UnSafe 对象直接 freeMemory 进行释放。

public class test2 {
    static int _1GB = 1024*1024*1024;

    //反射获取UnSafe 实例对象
    public static Unsafe getUnsafe(){
        Unsafe unsafe = null;
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return unsafe;
    }

    //分配直接内存并且回收
    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        //分配内存
        long base = unsafe.allocateMemory(_1GB);
        unsafe.setMemory(base,_1GB,(byte) 0);
        System.in.read();

        //释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }
}

参考文章:
JVM 完整深入解析
JVM系列(二) - JVM内存区域详解
JVM --方法区(超详细)
Java常量池详解

本文地址:https://blog.csdn.net/qq_43684985/article/details/114257696

相关标签: JVM