写点什么

JVM 调优艺术:JVM 内存管理机制深度剖析

发布于: 2021 年 02 月 09 日
JVM调优艺术:JVM内存管理机制深度剖析

什么是 JVM

JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别.class 后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。

JVM 的运行过程:

HelloWorld.java 通过 javac 的编译,编译成 HelloWorld.class,class 文件通过 Java 类加载器 ClassLoader 加载到运行时数据区(JVM 管理的内存),通过执行引擎解释执行。 JVM 的工作就是把 Class 文件解释成机器可以识别的机器码,解释执行就是,字节码和机器码,

运行时数据区

1.定义

Java 虚拟机在执行 Java 程序的过程中,会把它所管理的内存划分成若干个不同的数据区域。

2.分类

这些不同的数据区域分为:虚拟机栈、本地方法栈、程序计数器、方法区、堆。其中前三个区域是线程私有的区域,后两个区域是由所有线程共享的数据区。

2.1 程序计数器:

2.1.1 定义

指向当前线程正在执行的字节码的指令地址。

2.1.2 为什么需要程序计数器?(时间的轮转机制)

2.1.3 为什么程序计数器是 JVM 中唯一不会 OOM 的区域?

程序计数器是一块很小的内存区域,它只需要记录程序运行的地址,一个 int 类型的长度就足够了。

2.2 虚拟机栈:

2.2.1 定义

存储当前线程运行方法所需的数据、指令、返回地址。

对于栈这个数据模型,它的特征就是先进后出,后进先出。虚拟机栈也不例外。Java 中每一个方法在执行的同时,都会创建一个栈帧,栈帧中存储了局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法调用到结束的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。 如果把虚拟机栈比喻成子弹夹,那么栈帧就是子弹。

2.2.2 局部变量表

2.2.2.1 定义

所谓局部变量表,就是在方法执行时,各种局部变量存放的地方。

局部变量表可以存放的数据类型只有 8 种基本类型、引用类型和 returnAddress 类型。引用类型它是指向对象起始地址的引用指针或者指向一个代表对象的句柄。returnAddress 类型,是指向一条字节码指令的地址,当带有返回值的方法完成时,方法完成就要出栈,出栈的地址在哪,就是使用这个值记着的。一般来说,是在该方法的下一行。 局部变量表所需的空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小。

2.2.2.2 异常处理

Java 虚拟机规范中,对这个区域规定了两种的异常状况:StackOverflowError 和 OutOfMemoryError。 如果线程请求的栈深度>虚拟机栈的深度,将抛出 StackOverflowError 异常。如果虚拟机栈可以动态扩展,如果在扩展时无法申请到足够的内存,将抛出 OutOfMemoryError。

2.2.2.3 方法执行时,栈帧是如何工作的?

public class Person{
public int work(){ int x = 1; int y = 2; int z = (x+y)*10; return z; }
public static void main(String[] args){ Person person = new Person(); person.work(); }}复制代码
复制代码

这样的一段代码,使用 javac 编译之后,得到 Person.class 文件,使用 Javap -c 进行反汇编,得到下面的代码:

分析一下这段代码执行时,虚拟机栈中的内存变化是怎样的。 执行过程:

  1. 执行 main(),main()的栈帧入栈

  2. 执行 work(),work()栈帧将 main()栈帧压入栈底

  1. 执行 work()中的 int x = 1;

  1. 执行 work()中的 int y = 2;

5.执行代码 int z = (x+y)*10; 执行完成之后,操作数栈清空

6 最终,11: iload_3 12: ireturn 将局部变量表中下标为 3 的值压入操作数栈中,作为返回

以上就是 work()执行时,在虚拟机栈中内存的变化过程

JVM 指令集可以参照腾讯云社区的这篇 java 虚拟机 JVM 字节码 指令集 bytecode 操作码 指令分类用法 助记符

2.3 本地方法栈

本地方法栈和虚拟机栈发挥的作用是很类似的,只不过虚拟机栈用于管理 Java 方法的调用,而本地方法栈则用于管理 Native 方法的调用。本地方法栈和虚拟机栈十分类似,虚拟机规范对其中方法使用的语言、使用方式和数据结构并没有强制规定,因此各虚拟机可以自由的实现它。HotSpot 虚拟机,直接把本地方法栈和虚拟机栈合二为一。

2.4 方法区

方法区用于存储已被虚拟机加载的类信息(ClassLoader 加载类信息就加载在这里)、常量、静态变量、即时编译器编译后的代码等数据。是所有线程共享的区域。

JVM 在执行某个类的时候,必须先加载,在加载类(加载、验证、准备、解析、初始化)的时候,JVM 会先加载 class 文件,而在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池,用于存放编译期间,生成的各种字面量和符号引用。

字面量

字符串(String a = "b")、基本类型常量(final 修饰的变量)

符号引用

类和方法的全限定名(“Java/lang/String”)、字段的名称和描述符、方法的名称和描述符

当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时常量池中;在解析阶段,JVM 会把符号引用替换成直接引用(对象的索引值)。运行时常量池是全局共享的,多个类共用一个运行时常量池。class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。

在 JDK1.7 之前,在 HotSpot 虚拟机中,使用永久代来实现方法区。这样做的好处是,HotSpot 的垃圾回收器可以像管理 Java 堆一样来管理这部分的内存,省去了专门为方法区编写内存管理代码的工作,但是这样做会导致别的问题发生:1. 永久代里面的数据,回收的效率很低,但堆中放的是对象和数组,是需要频繁回收的数据。如果跟堆中一样进行垃圾回收,无疑是一种资源的浪费;2. 永久代里的内存经常不使用容易发生内存溢出,永久代从 Java 堆中划分,它的大小仍然是受制于堆的大小,它长时间无法回收,这块区域就很容易发生内存溢出,因此,在 HotSpot 虚拟机,JDK1.7 版本中,已经将永久代的静态变量和运行时常量池转移到了堆中。在 JDK1.8 之后,更是去掉了方法区中的永久代,改为元空间,元空间所的存储位置是在机器内存中,它的大小不再受制于 Java 堆。也就能解决永久代内存溢出的问题。

2.5 堆

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

另外 Java 堆也是垃圾回收器管理的主要区域,随着对象的不断创建,堆空间占用越来越多,就需要不定期的对不再使用的对象进行回收。从内存回收的角度看,由现在收集器基本都采用分代算法,所以 Java 堆中还可以细分为新生代和老年代,新生代中又分为 Eden 区、From Survivor 区、To Survivor 区。

根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

堆的大小参数:

2.5.1 使用 HSDB 工具从底层深入理解运行时数据区

HSDB 是 JDK 自带的 Hotspot Debuger 工具,透过它能够让我们更直观地查看运行中的 java 对象在内存中的存在形式和状态,如对象的 oops、类信息、线程栈信息、堆信息、方法字节码和 JIT 编译后的汇编代码等。

对下面这段代码进行内存的分析

public class Test {
public final static String MAN_TYPE = "man"; public static String WOMAN_TYPE = "woman";
public static void main(String[] args)throws Exception{ Person p1 = new Person(); p1.name = "niuniu"; p1.age = 18; p1.sex = MAN_TYPE; // 垃圾回收15次 for(int i = 0;i<15;i++){ System.gc(); } Person p2 = new Person(); p2.name = "yangyang"; p2.age = 19; p2.sex = WOMAN_TYPE; Thread.sleep(Integer.MAX_VALUE); }}
class Person{ String name; String sex; int age;}复制代码
复制代码

2.5.1.1 打开 HSDB

要是用 HSDB,第一步,必须把 sawindbg.dll 复制到对应目录的 jre 下,否则运行 HSDB 时,就会报这样的错误:

复制到 Java 目录下的 jre 的 bin 目录下,在我的电脑中就是 C:\Program Files\Java\jre1.8.0_271\bin

第二步:在 cmd 中,打开 sa-jdi.jar 所在的目录,使用命令,java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB 打开 HSDB 工具。

2.5.1.2 向 HSDB 中添加进程

使用 jps 找到所要添加的进程

可以看到,2732 就是我们所要找的进程号,把 2732 添加到 HDSB 中。

第一步:选择 File -> Attach to HotSpot Process

第二步:在弹出的对话框中输入进程号 2732

然后就可以看到要找的线程 main

2.5.1.3 分析栈内的内存分布情况

点击如图所示的 stack memory 按钮

得到如下图所示的内容:上半部分是 Sleep 方法的栈帧,下半部分是 main 方法的栈帧,类似于 0x000000008143dfe0 的数据,是栈内存的地址。

把 Main 方法的栈帧放大来看

2.5.1.4 分析堆内内存的分配情况

首先,来看下堆区在内存中的分配情况 依次点击 HSDB 中的 Tools-> Heap Parameters 得到如下图所示的内容。

其中的 PSYoungGen(年轻代)、PSOldGen(年老代),年轻代中又有三个区,分别是 eden 区,from 区,to 区。 根据上面的图,我们可以总结出内存的分布情况如下表所示:年轻代的三个区域是在一块连续的内存中。

然后我们来看上面那一段代码中的两个 Person 对象在堆中的哪里。 依次点击 HSDB 工具中的 Tools -> Object Histogram,找到 Person 类

双击 Person 类,获得两个 Person 的具体情况

第一个 name“yangyang”的 Person 对象,地址是 0x00000000d5c00000,根据比对,这个 Person 对象位于堆区的年轻代的 eden 区,根据内存回收机制(下面的内容里面会详细讲解),新创建的对象,位于年轻代的 eden 区。 第二个 name“niuniu”的 Person 对象,地址是 0x00000000814199d0,根据比对,这个 Person 对象位于堆区的老年代,根据内存回收机制,被回收 15 次的对象,如果还持有强引用,位于堆区的老年代。

虚拟机中的对象

1、对象的创建

2、对象的内存布局

3、对象的访问定位

原文链接:https://juejin.cn/post/6926762990897528845

如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份 Java 核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!


发布于: 2021 年 02 月 09 日阅读数: 44
用户头像

领取资料添加小助理vx:bjmsb2020 2020.12.19 加入

Java领域;架构知识;面试心得;互联网行业最新资讯

评论

发布
暂无评论
JVM调优艺术:JVM内存管理机制深度剖析