JVM_01 内存结构篇

1|0JVM(Java Virtual Machine)

1|1一、前言

1|01、什么是 JVM ?

1、定义:

  • Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。

2、好处:

  • 一次编译,处处执行
  • 自动的内存管理,垃圾回收机制
  • 数组下标越界检查

3、比较:

JVM、JRE、JDK 的关系如下图所示

在这里插入图片描述

1|02、学习 JVM 有什么用?

  • 面试必备
  • 中高级程序员必备
  • 想走的长远,就需要懂原理,比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事等待,JVM 是必须掌握的。

1|03、常见的 JVM

这里需要重点了解:JVM是一套规范,而我们也可以遵守这套规范实现自己的JVM(有能力的前提下!)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YKnVVBwl-1638002082673)(JVM笔记(黑马).assets/image-20211125102833946.png)]

1|04、JVM整体预览

先做一个整体预览,然后逐个击破!

1|2二、内存结构

1|01、程序计数器

1.1、概述:

JVM 中的程序计数器(Program Counter Register)有的时候也被称作PC寄存器,为了避免混淆这里解释一下,这里,并非是广义上所指的

物理寄存器,或许将其翻译为 PC 计数器(或指令计数器)更加贴切(也称为程序钩子),并且也不容易引起误会。

JVM 中 PC 寄存器是堆物理 PC 寄存器的一种抽象模拟。

1.2、特点:

  • 是线程私有的
  • 不会存在内存溢出(OOM)

1.3、作用:

PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

、

1|02、虚拟机栈

栈帧:每个方法执行的时候需要的内存空间,其中占用空间的有(参数、局部变量、返回地址等)

当我们调用一个方法的时候,会在虚拟机栈当中给他开辟一个栈帧大小的空间,然后让栈帧入栈,方法执行完毕栈帧出栈!

定义:

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

相关问题:

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

答: 不会,因为我们的栈中的栈帧空间是用完就释放的!

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

答:不是,由于我们的物理内存是有限的,一个栈的内存越大,能开线程越少,并发降低,其作用仅仅是增加了方法的递归调用

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

答:是的,局部变量是在线程私有的,不会与其他线程共享,不会产生线程安全问题(前提是局部变量没有逃离方法的作用范围!)

1|02.1、栈内存溢出

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

1|02.2、线程运行诊断

案例一:cpu 占用过多

解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

  • top 命令,查看是哪个进程占用 CPU 过高
  • ps H -eo pid (进程id), tid(线程id),%cpu | grep : 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
  • jstack 进程 id : 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

案例二:长时间运行未出现结果(可能出现死锁)

解决方法:使用jstack工具 + 进程号,就会定位到死锁问题!

1|03、本地方法栈

一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。

1|04、堆

Heap 堆

  • 通过new关键字创建的对象都会被放在堆内存

特点 :

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

1|04.1、堆内存溢出

发生堆内存溢出会抛出 java.lang.OutOfMemoryError

可以使用 -Xmx8m 来指定堆内存大小。将堆内存调小就可以便于我们排查问题!

1|04.2、堆内存诊断

1、jps工具:查看当前系统中有哪些java进程 : jps

2、jmap工具 :查看堆内存的占用情况 : jmap - heap + 进程id

3、jconsole工具 : 图形化界面多功能的检测工具,可以连续监测

4、jvisualvm工具 : 相对jconsole更加强大的可视化工具

案例 : 垃圾回收后,内存占用仍然很高!

使用jvisualvm,启动可视化工具检测我们的虚拟机内存,找到HeapDump,对堆内存进行一个快照(内存转储),然后获取并且分析此时详细数据,定位原因!

1|05、方法区

首先看一个定义:我们的JVM规范中对于方法区(Method Area)的定义:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RqXUuLbd-1638002082675)(JVM笔记(黑马).assets/image-20211125125226539.png)]

看一下内存结构

1|05.1、方法区内存溢出

  • 1.8 之前会导致永久代内存溢出

    使用 -XX:MaxPermSize=8m 指定永久代内存大小
  • 1.8 之后会导致元空间内存溢出

    使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

1|05.2、运行时常量池

首先先理解什么是常量池

1、我们可以通过堆一下代码得字节码文件进行反编译,拿到我们的反编译信息

//想要运行就需要被编译为二进制字节码(类基本信息,常量池,类方法定义,包含虚拟机指令) public class ContentPoolTest { public static void main(String[] args) { System.out.println("Hello World"); } }

2、找到字节码文件执行

javap -v ContentPoolTest.class

3、可以得出我们得字节码文件,反编译后的结果

然后我们对字节码文件进行说明

、

、

由此可以总结得出:常量池就是一张表,虚拟机指令根据这张常量表,去找到要执行的类名、方法名、参数类型、字面量等信息

我们一个类的字节码文件包含一个常量池,多个类一起运行的情况下,会将每个类的常量池表汇聚在一起,放在我们的运行时常量池

其中也不会是#1 #2 #3 这种地址,而是真实的内存地址!

1|05.3、串池 StringTable

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象【懒加载,只有当JVM指令用到的时候才会创建对象】
  • 利用串池的机制,来避免重复创建字符串对象 【主要是因为串池是HashTable实现的,底层是Hash表不可扩容】
  • 字符串变量的拼接原理:StringBuilder(1.8)
  • 字符串常量的拼接原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池子!

案例一

//StringTable["a","b","ab"] String s1 = "a" ; //当执行到此出处的时候会将a转为"a",然后放入StringTable中 String s2 = "b" ; String s3 = "ab" ; String s4 = s1 + s2 ; //new StringBuilder().append("a").append("b").toString ; new String("ab")保存在堆中! String s5 = "a" + "b" ; //在编译期间已经优化,在编译期确定为ab ; System.out.println(s3 == s4); // false ; System.out.println(s3 == s5); // true

案例二

intern方法1.7以后

String s = new String("a") + new String("b") ; //StringBuilder动态字符串拼接,保存在堆中【此时串池没有"ab"】 System.out.println(s == "ab"); //false ; String str = s.intern();// 如果串池没有s,intern方法会将s的值传入串池当中,并且返回串池子中的对象 // 如果串池中有s,s就不会入池但是,仍然会返回串池中的对象 【无论怎样s都会入池】 System.out.println(str == "ab"); //返回true System.out.println(s == "ab"); //true ;

intern方法1.6以前

String s = new String("a") + new String("b") ; //StringBuilder动态字符串拼接,保存在堆中【此时串池没有"ab"】 System.out.println(s == "ab"); //false ; String str = s.intern();// intern方法会将s的值拷贝一份传入串池当中,并且返回串池中的对象 // 【注意:此时的s仍然是在堆中】 System.out.println(str == "ab"); //返回true System.out.println(s == "ab"); //false ;

1|05.4、StringTable 的位置

  • jdk1.6 StringTable 位置是在永久代中
  • jdk1.8 StringTable 位置是在堆中

1|05.5、StringTable 垃圾回收

  • -Xmx10m 指定堆内存大小
  • -XX:+PrintStringTableStatistics 打印字符串常量池信息
  • -XX:+PrintGCDetails打印GC信息
  • -verbose:gc 打印 gc 的次数,耗费时间等信息``
/** * 演示 StringTable 垃圾回收 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc */ public class Code_05_StringTableTest { public static void main(String[] args) { int i = 0; try { for(int j = 0; j < 10000; j++) { // j = 100, j = 10000 String.valueOf(j).intern(); i++; } }catch (Exception e) { e.printStackTrace(); }finally { System.out.println(i); } } }

1|05.6、StringTable 性能调优

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
  • 考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池

1|06、直接内存

Direct Memory

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

1|06.1、使用直接内存的好处

文件读写流程:

、

因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。

使用了 DirectBuffer 文件读取流程

、

直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

减少了不必要的复制操作

1|06.2、直接内存回收原理

直接内存的回收不是通过 JVM 的垃圾回收来释放的,,而是通过unsafe.freeMemory 来手动释放。

public class Code_06_DirectMemoryTest { public static int _1GB = 1024 * 1024 * 1024; public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException { // method(); method1(); } // 演示 直接内存 是被 unsafe 创建与回收 private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe)field.get(Unsafe.class); long base = unsafe.allocateMemory(_1GB); unsafe.setMemory(base,_1GB, (byte)0); System.in.read(); unsafe.freeMemory(base); System.in.read(); } // 演示 直接内存被 释放 private static void method() throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB); System.out.println("分配完毕"); System.in.read(); System.out.println("开始释放"); byteBuffer = null; System.gc(); // 手动 gc System.in.read(); } }

直接内存的回收机制总结

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

简述:一但BtyeBuffer这个这个java类被回收,就会将我们的直接内存释放!

然而我们一般用 jvm 调优时,会加上下面的参数:

-XX:+DisableExplicitGC //禁止显示的 GC

意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。

所以我们就通过 unsafe 对象主动的调用 freeMemory 的方式释放内存。


__EOF__

本文作者宋淇祥
本文链接https://www.cnblogs.com/qxsong/p/15837248.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   爪洼ing  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示

喜欢请打赏

扫描二维码打赏

支付宝打赏