当前位置:   article > 正文

深入探讨Java虚拟机(JVM):执行流程、内存管理和垃圾回收机制_java8 jvm新生代内存工作过程

java8 jvm新生代内存工作过程

目录

什么是JVM?

JVM 执行流程

JVM 运行时数据区

堆(线程共享)

Java虚拟机栈(线程私有)

什么是线程私有?

程序计数器(线程私有)

方法区(线程共享)

JDK 1.8 元空间的变化

运行时常量池

内存布局中的异常问题

1.  Java堆溢出

2.  虚拟机栈和本地方法栈溢出

JVM 类加载

1. 类加载过程

加载

验证

准备

解析

初始化

双亲委派模型

垃圾回收机制

死亡对象的判断算法

引用计数算法

可达性分析算法

垃圾回收算法

标记-清除算法(Mark and Sweep):

复制算法(Copying Garbage Collection):

标记-整理算法(Mark and Compact):

 分代算法(Generational Garbage Collection):


 

什么是JVM?

JVM(Java虚拟机)是Java编程语言的关键组成部分,它是一种虚拟计算机环境,用于执行Java程序。JVM的主要作用是将Java源代码编译成与特定计算机硬件无关的字节码,并在运行时将这些字节码转换为机器码,以便在不同平台上运行Java应用程序。

在JVM的运行环境中,Java程序能够实现跨平台的特性,因为它们不需要直接与底层操作系统进行交互,而是依赖JVM来处理与硬件的交互。这使得Java成为一种高度可移植和可扩展的编程语言。

JVM的关键功能包括:

  • 类加载:JVM负责加载Java类的字节码文件,通过类加载器实现这一任务。
  • 内存管理:JVM自动管理内存分配和垃圾回收,以确保应用程序不会出现内存泄漏和溢出。
  • 字节码执行:JVM解释或编译Java字节码,将其转换为本地机器码以执行应用程序。
  • 多线程支持:JVM提供多线程支持,允许并发执行Java应用程序的部分或全部代码。
  • 垃圾回收:JVM使用垃圾回收机制来自动释放不再被引用的内存,以提高内存利用率。

JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 JVM 是如何执行的呢?

JVM 执行流程

程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码 文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调 用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。

48a2aa2c525045259351cc961987305e.png

总结来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:

1. 类加载器(ClassLoader)

2. 运行时数据区(Runtime Data Area)

3. 执行引擎(Execution Engine)

4. 本地库接口(Native Interface) 

JVM 运行时数据区

JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称JMM)完全不同,属于完全不同的两个概念,它由以下5大部分组成:

6218dc8595064dc1a97fc207c1275c27.png

堆(线程共享)

堆的作用:程序中创建的所有对象都在保存在堆中。

我们常见的 JVM 参数设置 -Xms10m 最小启动内存是针对堆的,-Xmx10m 最大运行内存也是针对堆的。

ms 是 memory start 简称,mx 是 memory max 的简称。

堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。

f0f92bb4b8f74312aefa8e8cf687efee.png

垃圾回收的时候会将 Eden 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清楚掉。 

Java虚拟机栈(线程私有)

Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的 内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。

Java 虚拟机栈中包含了以下 4 部分:

ab304a4393e9493494e30a72d88c4be8.png

1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量     表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局     部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数       和局部变量。

2. 操作栈:每个方法会生成一个先进后出的操作栈。

3. 动态链接:指向运行时常量池的方法引用。

4. 方法返回地址:即在方法执行完成后将控制返回到调用方法的指令位置。方法出口通常用于支持方法调用的返回操作。

什么是线程私有?

由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器 (多核处理器则指的是一个内核) 都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存。

程序计数器(线程私有)

程序计数器的作用:用来记录当前线程执行的行号的。

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;

如果正在执行的是一个Native方法,这个计数器值为空。 程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!

方法区(线程共享)

方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。

在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。

  1. 汽车(Java虚拟机规范):在这个比喻中,汽车代表了Java虚拟机规范,它定义了Java虚拟机的基本结构和功能,其中包括了方法区的概念。

  2. 动能提供装置(方法区):这相当于汽车的一个重要部分,就像Java虚拟机中的方法区一样。方法区是用于存储类的结构信息、静态变量、常量池、方法的字节码等的内存区域。它提供了Java应用程序所需的关键信息。

  3. 发动机和电机(永久代和元空间):在不同类型的汽车中,动能提供装置可以有不同的实现。对于燃油车,它使用发动机作为动能提供装置,而电动汽车使用电机。同样地,Java虚拟机可以使用永久代或元空间来实现方法区。

    • 永久代(PermGen):就像燃油车使用汽油发动机一样,一些早期版本的Java虚拟机使用永久代作为方法区的实现。永久代有一些限制,如固定大小,可能会导致内存溢出问题。

    • 元空间(Metaspace):与之不同,元空间是Java虚拟机规范的一种新实现方式。它更灵活,不再受到永久代的限制,可以动态调整大小,避免了一些与永久代相关的问题。

总之,永久代和元空间都是Java虚拟机规范中对方法区的不同实现方式,就像汽油发动机和电动机都是动能提供装置的不同实现一样。选择使用哪种实现方式取决于Java虚拟机的版本和配置,以及应用程序的需求。这个比喻很好地概括了它们之间的关系。

JDK 1.8 元空间的变化

1. 对于 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的大小就不在受 JVM 最大内存的参数影响了,而是与本地内存的大小有关。

2. JDK 8 中将字符串常量池移动到了堆中。

运行时常量池

运行时常量池是方法区的一部分,存放字面量与符号引用。

字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。

符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

内存布局中的异常问题

1.  Java堆溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来 GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。

我们可以设置JVM参数-Xms:设置堆的最小值、-Xmx:设置堆最大值。下面我们来看一个 Java堆OOM的测试,测试以下代码之前先设置 Idea 的启动参数,如下图所示:

8d320626792e4a6cafe1cbd05b4d922f.png

d3d7ecf4eafd4830b6878435ebd24dd4.png JVM 参数为:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError

示例:

  1. public class Main {
  2. static class OOMObject {
  3. }
  4. public static void main(String[] args) {
  5. List<OOMObject> list =
  6. new ArrayList<>();
  7. while(true) {
  8. list.add(new OOMObject());
  9. }
  10. }
  11. }

以上程序的执行结果如下:

bc91bb414d184742916a4e2acbf7eaed.pngJava堆内存的OOM异常是实际应用中最常见的内存溢出情况。当出现Java堆内存溢出时,异常堆栈信 息"java.lang.OutOfMemoryError"会进一步提示"Java heap space"。当出现"Java heap space"则很明 确的告知我们,OOM发生在堆上。 

此时要对Dump出来的文件进行分析,以MAT为例。分析问题的产生到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

内存泄漏 : 内存泄漏是指在程序中分配了一些内存(通常是用于存储对象或数据),但在后续的程序执行中,无法释放或回收这些内存,导致内存消耗不断增加,最终可能导致程序性能下降或崩溃。

内存溢出 : 内存溢出(Memory Overflow)是指在程序运行时,尝试分配的内存超出了可用的物理内存或虚拟内存的范围,导致程序无法继续正常执行的情况。内存溢出通常是由于程序内存管理不当或程序本身存在缺陷引起的。内存溢出会导致程序崩溃或异常终止。

2.  虚拟机栈和本地方法栈溢出

由于我们HotSpot虚拟机将虚拟机栈与本地方法栈合二为一,因此对于HotSpot来说,栈容量只需要由-Xss参数来设置。

关于虚拟机栈会产生的两种异常:

1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常

2. 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常

示例:

观察StackOverFlow异常(单线程环境下)

  1. /**
  2. * JVM参数为:-Xss128k
  3. */
  4. public class Test {
  5. private int stackLength = 1;
  6. public void stackLeak() {
  7. stackLength++;
  8. stackLeak();
  9. }
  10. public static void main(String[] args) {
  11. Test test = new Test();
  12. try {
  13. test.stackLeak();
  14. } catch (Throwable e) {
  15. System.out.println("Stack Length: " + test.stackLength);
  16. throw e;
  17. }
  18. }
  19. }

会出现如下运行结果: 

75b03da3434e442eb3be839b64446c99.png

出现StackOverflowError异常时有错误堆栈可以阅读,比较好找到问题所在。如果使用虚拟机默认参数,栈深度在多多数情况下达到1000-2000完全没问题,对于正常的方法调用(包括递归),完全够用。

如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的 方式来换取更多线程。

  1. /**
  2. * JVM参数为:-Xss2M
  3. */
  4. public class Test {
  5. private void dontStop() {
  6. while(true) {
  7. }
  8. }
  9. public void stackLeakByThread() {
  10. while(true) {
  11. Thread thread = new Thread(new Runnable() {
  12. @Override
  13. public void run() {
  14. dontStop();
  15. }
  16. });
  17. thread.start();
  18. }
  19. }
  20. public static void main(String[] args) {
  21. Test test = new Test();
  22. test.stackLeakByThread();
  23. }
  24. }

以上代码运行电脑可能会崩,记得保存所有工作~~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/834848
推荐阅读
相关标签