scala jvm_可视化JVM中的内存管理(Java,Kotlin,Scala,Groovy,Clojure)

scala jvm

最初发表于deepu.tech

请在Twitter上关注我以获取更新,并让我知道帖子中是否可以改进。


在这个由多个部分组成的系列文章中,我旨在揭露内存管理背后的概念,并深入研究某些现代编程语言中的内存管理。 我希望该系列文章可以使您对这些语言在内存管理方面正在发生的事情有所了解。 在本章中,我们将研究Java,Kotlin,Scala,Clojure,JRuby等语言所使用的Java虚拟机(JVM)的内存管理。

如果您还没有阅读本系列的第一部分 ,请先阅读它,因为我在那解释了堆栈和堆内存之间的区别,这对于理解本章很有用。

JVM内存结构

首先,让我们看看JVM的内存结构是什么。 这是从JDK 11开始的。 以下是JVM进程可用的内存,并由操作系统(OS)分配。

JVM内存结构

这是操作系统分配的本机内存,其数量取决于操作系统,处理器和JRE。 让我们看看不同的领域是什么:

堆内存

JVM在此处存储对象或动态数据。 这是最大的内存区域,这是垃圾回收(GC)发生的地方。 可以使用Xms (初始)和Xmx (最大)标志来控制堆内存的大小。 整个堆内存未提交给虚拟机(VM),因为其中一些保留为虚拟空间,并且堆可以增长使用此空间。 堆又分为“年轻”“老”一代空间。

  • 年轻一代 :年轻一代或“新空间”是居住新物体的地方,并进一步分为“伊甸园空间”和“幸存者空间”。 该空间由“次要GC”(有时也称为“ Young GC”)管理
    • 伊甸园空间 :这是创建新对象的地方。 创建新对象时,将在此处分配内存。
    • 幸存者空间 :这是在次要GC中幸存的对象的存储位置。 这分为两半S0S1
  • 老一代 :老一代或“租用空间”是在次要GC生存期间达到最大使用期限阈值的对象。 该空间由“主要GC”管理

线程栈

这是堆栈存储区,进程中每个线程有一个堆栈存储区。 在此存储线程特定的静态数据,包括方法/功能框架和对象的指针。 可以使用Xss标志设置堆栈内存限制。

元空间

这是本机内存的一部分,默认情况下没有上限。 这是早期版本的JVM中的永久生成(PermGen)空间 。 类加载器使用此空间来存储类定义。 如果此空间持续增长,则操作系统可能会将此处存储的数据从RAM移至虚拟内存,这可能会使应用程序变慢。 为避免可能对使用XX:MetaspaceSize-XX:MaxMetaspaceSize标志的元空间设置限制,在这种情况下,应用程序可能会抛出内存不足错误。

代码缓存

即时(JIT)编译器在这里存储经常访问的已编译代码块。 通常,JVM必须将字节代码解释为本机机器代码,而无需解释JIT编译的代码,因为它已经采用本机格式并已缓存在此处。

共享库

在此存储使用的任何共享库的本机代码。 操作系统每个进程仅加载一次。


JVM内存使用情况(堆栈vs堆)

既然我们已经清楚了内存的组织方式,那么让我们看看执行程序时如何使用内存中最重要的部分。

让我们使用下面的Java程序,代码没有针对正确性进行优化,因此忽略诸如不必要的中间变量,不正确的修饰符之类的问题,重点是可视化堆栈和堆内存的使用情况。

class Employee {
    String name ;
    Integer salary ;
    Integer sales ;
    Integer bonus ;

    public Employee ( String name , Integer salary , Integer sales ) {
        this . name = name ;
        this . salary = salary ;
        this . sales = sales ;
    }
}

public class Test {
    static int BONUS_PERCENTAGE = 10 ;

    static int getBonusPercentage ( int salary ) {
        int percentage = salary * BONUS_PERCENTAGE / 100 ;
        return percentage ;
    }

    static int findEmployeeBonus ( int salary , int noOfSales ) {
        int bonusPercentage = getBonusPercentage ( salary );
        int bonus = bonusPercentage * noOfSales ;
        return bonus ;
    }

    public static void main ( String [] args ) {
        Employee john = new Employee ( "John" , 5000 , 5 );
        john . bonus = findEmployeeBonus ( john . salary , john . sales );
        System . out . println ( john . bonus );
    }
}

单击幻灯片,然后使用箭头键向前/向后移动,以查看如何执行上述程序以及如何使用堆栈和堆存储器:

注意:如果幻灯片的边缘看起来被切掉,请单击幻灯片的标题或在此处直接在SpeakerDeck中打开它。

如你看到的:

  • 静态字段保存在堆栈中的单独块中
  • 每个函数调用都作为一个框架块添加到线程的堆栈内存中
  • 所有局部变量,包括参数和返回值,均保存在堆栈上的功能框内
  • 所有基本类型(如int都直接存储在堆栈中。 这也适用于静态字段
  • 所有对象类型(例如EmployeeIntegerString都在堆上创建,并使用堆栈指针从堆栈中引用。 这也适用于静态字段
  • 从当前函数调用的函数被推入栈顶
  • 函数返回时,将其框架从堆栈中删除
  • 一旦主过程完成,堆上的对象将不再具有来自Stack的指针并成为孤立对象
  • 除非您明确进行复制,否则其他对象内的所有对象引用都将使用指针完成

如您所见,堆栈是由操作系统自动管理的,而不是由JVM本身进行管理。 因此,我们不必担心堆栈。 另一方面,Heap并不是由操作系统自动管理的,并且由于其最大的内存空间并保存动态数据,因此它可能呈指数增长,从而导致我们的程序随着时间的推移而耗尽内存。 随着时间的流逝,它也变得支离破碎,使应用程序变慢。 这就是JVM的帮助。 它使用垃圾收集过程自动管理堆。


JVM内存管理:垃圾回收

现在我们知道了JVM如何分配内存,让我们看看它如何自动管理Heap内存,这对于应用程序的性能非常重要。 当程序尝试在堆上分配的内存大于可用内存(取决于Xmx配置)时,我们会遇到内存不足错误

JVM通过垃圾回收管理堆内存。 简单来说,它释放了孤立对象(即不再从堆栈中直接或间接(通过另一个对象中的引用)引用的对象)使用的内存,从而为创建新对象腾出了空间。

GC根

JVM中的垃圾收集器负责:

  • 从OS分配内存,然后再分配回OS。
  • 根据请求向应用程序分配已分配的内存。
  • 确定应用程序仍在使用分配的内存的哪些部分。
  • 回收未使用的内存以供应用程序重用。

JVM垃圾收集器是世代的(Heap中的对象按其年龄分组,并在不同阶段清除)。 有许多不同的算法可用于垃圾回收,但“ 标记和清除”是最常用的算法。

标记和清除垃圾收集

JVM使用一个单独的后台驻留程序线程,该线程在后台运行以进行垃圾回收,并且该进程在满足某些条件时运行。 Mark&Sweep GC通常涉及两个阶段,根据所使用的算法,有时会有一个可选的第三阶段。

标记并扫描GC

  • 标记 :第一步,垃圾收集器标识正在使用的对象和未使用的对象。 从GC根目录(堆栈指针)递归使用或使用的对象被标记为活动对象。
  • 清扫 :垃圾收集器穿过堆和移除未标记活着的任何对象。 现在,该空间被标记为空闲。
  • 压缩 :删除未使用的对象后,所有剩余的对象将一起移动。 这将减少碎片并提高为新对象分配内存的性能

这种类型的GC也称为“世界停止GC”,因为它们在执行GC时会在应用程序中引入暂停时间。

当涉及到GC时,JVM提供了几种不同的算法供您选择,并且根据您所使用的JDK供应商的不同,可能还有更多可用的选项(例如, 谢南多厄GC ,OpenJDK上可用)。 不同的实现侧重于不同的目标,例如:

  • 吞吐量 :收集垃圾而不是应用程序所花费的时间会影响吞吐量。 理想情况下,吞吐量应该很高(即,GC时间很短时)。
  • 暂停时间 :GC停止执行应用程序的持续时间。 理想的暂停时间应该非常短。
  • 占地面积 :使用的堆的大小。 理想情况下,应将其保持在较低水平。

从JDK 11开始可用的收集器

从当前的LTE版本JDK 11开始,以下垃圾收集器可用,并且JVM根据使用的硬件和OS选择使用的默认垃圾收集器。 我们总是可以指定与-XX开关一起使用的GC。

  • 串行收集器 :它使用单个线程进行GC,对于具有小数据集的应用程序非常有效,并且最适合于单处理器计算机。 可以使用-XX:+UseSerialGC开关启用-XX:+UseSerialGC
  • 并行收集器 :这是一个专注于高吞吐量的应用程序,它使用多个线程来加速GC进程。 这适用于具有在多线程/多处理器硬件上运行的中到大型数据集的应用程序。 可以使用-XX:+UseParallelGC开关启用它。
  • Garbage-First(G1)收集器 :G1收集器是大多数并发收集器(意味着仅同时执行昂贵的工作)。 这适用于具有大量内存的多处理器计算机,并且在大多数现代计算机和OS上均默认启用。 它着重于低暂停时间和高吞吐量。 可以使用-XX:+UseG1GC开关启用-XX:+UseG1GC
  • Z Garbage Collector :这是JDK11中引入的新实验GC。 它是一个可伸缩的低延迟收集器。 它是并发的,不会停止应用程序线程的执行,因此不会停下来。 它适用于要求低延迟和/或使用非常大的堆(数TB)的应用程序。 可以使用-XX:+UseZGC开关启用-XX:+UseZGC

GCCraft.io

不管使用哪种收集器,JVM都有两种类型的GC进程,取决于执行的时间和地点,即次要GC和主要GC。

次要GC

这种类型的GC可使年轻一代的空间保持紧凑和清洁。 当满足以下条件时触发:

  • JVM无法从Eden空间获取所需的内存来分配新对象

最初,堆空间的所有区域都是空的。 伊甸园的记忆是第一个被填充的记忆,其次是幸存者空间,最后是终身空间。

让我们看一下次要的GC流程:

单击幻灯片,然后使用箭头键向前/向后移动以查看该过程:

注意:如果幻灯片的边缘看起来被切掉,请单击幻灯片的标题或在此处直接在SpeakerDeck中打开它。

  1. 让我们假设开始时伊甸园空间中已经有物体(将块01至06标记为已用内存)
  2. 应用程序创建一个新对象(07)
  3. JVM尝试从Eden空间获取所需的内存,但是Eden中没有可用空间来容纳我们的对象,因此JVM会触发次要GC
  4. GC从堆栈指针开始递归遍历对象图,以标记用作活动对象(已用内存)和其余对象作为垃圾对象(孤儿)
  5. JVM从S0和S1中选择一个随机块作为“ To Space”,让我们假设它是S0。 现在,GC将所有活动对象移动到“ To Space”(S0)中,该空间在我们开始时是空的,并且将它们的寿命增加一。
  6. 现在,GC清空了Eden空间,并且为新对象分配了Eden空间中的内存
  7. 让我们假设已经过去了一段时间,现在伊甸园空间中还有更多的对象(块07至13标记为已用内存)
  8. 应用程序创建一个新对象(14)
  9. JVM尝试从Eden空间获取所需的内存,但是Eden中没有可用空间来容纳我们的对象,因此JVM触发了第二个次要GC
  10. 重复标记阶段,并标记包括存活空间“ To Space”中的活动/孤立对象的活动/孤立对象
  11. JVM现在将空闲的S1选择为“ To Space”,而S0变为“ From Space”。 现在,GC将所有活动对象从Eden空间和“ From Space” S0移到“ To Space” S1,当我们开始时该对象为空,并将它们的年龄增加一。 由于某些对象不适合放置在此处,因此由于幸存者空间无法增长,因此将它们移到“租用空间”,此过程称为过早提升。 即使其中一个幸存者空间是空闲的,也会发生这种情况
  12. 现在,GC清空了Eden空间和“ From Space” S0,并且为新对象分配了Eden空间中的内存
  13. 每个次要GC都会重复此操作,并且幸存者会在S0和S1之间移动,并且他们的年龄会增加。 年龄达到“最大年龄阈值”(默认为15岁)后,该对象将移至“租用空间”

因此,我们看到了次要GC如何从年轻一代那里回收空间。 这是一个停滞不前的过程,但是它是如此之快,以致在大多数情况下可以忽略不计。

主要GC

这种类型的GC可使旧的(Tenured)空间保持紧凑和清洁。 当满足以下条件时触发:

  • 开发人员从程序中调用System.gc()Runtime.getRunTime().gc()
  • JVM决定在较小的GC周期中将其填满后,没有足够的使用期限空间。
  • 在次要GC期间,如果JVM无法从Eden或幸存者空间回收足够的内存。
  • 如果我们为JVM设置了MaxMetaspaceSize选项,并且没有足够的空间来加载新类。

让我们看一下主要的GC流程,它不像次要的GC那样复杂:

  1. 让我们假设已经过去了许多次要的GC周期,并且使用空间几乎已满,并且JVM决定触发“主要的GC”
  2. GC递归地遍历对象图,从堆栈指针开始,以标记使用权空间中用作活动对象(已用内存)和剩余对象作为垃圾对象(孤儿)的对象。 如果在次要GC期间触发了主要GC,则该过程将包括年轻(Eden&Survivor)和保有权空间
  3. 现在,GC删除了所有孤立对象并回收了内存
  4. 在重大GC事件期间,如果堆中没有更多对象,则JVM还会通过从元空间中删除已加载的类来回收元空间中的内存,这也称为完整GC

结论

这篇文章应该给您JVM内存结构和内存管理的概述。 这还不是详尽无遗,针对特定用例还有很多更高级的概念和调优选项可用,您可以从https://docs.oracle.com了解它们。 但是对于大多数JVM(Java,Kotlin,Scala,Clojure,JRuby,Jython)开发人员而言,此级别的信息就足够了,我希望它可以帮助您编写更好的代码(考虑到这些),以获得更高性能的应用程序,并牢记这些帮助您避免否则可能遇到的下一个内存泄漏问题。

希望您对JVM内部有一个愉快的了解,请继续关注本系列的下一篇文章。


参考资料


如果您喜欢这篇文章,请留下喜欢或评论。

您可以在TwitterLinkedIn上关注我。

翻译自: https://dev.to/deepu105/visualizing-memory-management-in-jvm-java-kotlin-scala-groovy-clojure-19le

scala jvm

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值