Java 8 内存管理原理解析及内存故障排查实践

2024年 3月 20日 59.5k 0

一、背景

Java是一种流行的编程语言,可以在不同的操作系统上运行。它具有跨平台、面向对象、自动内存管理等特点,Java程序在运行时需要使用内存来存储数据和程序状态。

Java的自动内存管理机制是由 JVM 中的垃圾收集器来实现的,垃圾收集器会定期扫描堆内存中的对象,检测并清除不再使用的对象,以释放内存资源。

Java的自动内存管理机制带来了许多好处,首先,它可以避免程序员手动管理内存时的错误,例如内存泄漏和悬空指针等问题。其次,它可以提高程序的运行效率,因为程序员不需要频繁地手动分配和释放内存,而是可以将更多时间和精力专注于程序的业务逻辑,最后,它可以提高程序的可靠性和稳定性,因为垃圾收集器可以自动检测和清除不再使用的内存资源,避免内存溢出等问题。

了解和掌握垃圾收集器原理可以帮助提高程序的性能、稳定性和可维护性。

名词解释:

响应速度:响应速度指程序或系统对一个请求的响应有多迅速。比如,用户查询数据响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。

吞吐量:吞吐量关注在一个特定时间段内应用系统的最大工作量,例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的GC停顿时间也是可以接受的,因为高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求。

GC导致的应用暂停时间影响系统响应速度,GC处理线程的CPU使用率影响系统吞吐量。

二、Java 的内存管理

2.1 JVM(Java虚拟机)内存划分

Java运行时数据区域划分,Java虚拟机在执行Java程序时,将其所管理的内存划分为不同的数据区域,每个区域都有特定的用途和创建销毁的时间。其中,有些区域在虚拟机进程启动时就存在,而有些区域则是随着用户线程的启动和结束而建立和销毁。这些数据区域包括程序计数器、虚拟机栈、本地方法栈、堆、方法区等,每个区域都有其自身的特点和作用。了解这些数据区域的使用方式和特点,可以更好地理解Java虚拟机的内存管理机制和运行原理。

JVM的内存区域划分可分为:1.堆内存空间、2.Java虚拟机栈区域、3.程序计数器、4.本地方法栈、5.元空间区域、6.直接内存。

图片

图片

  • 堆内存空间:JVM中占用内存空间最大的是堆,平常对象的创建大部分都是在堆上分配内存的,是垃圾回收的主要目标和方向。
  • 本地方法栈区域:Native Mehod Stack与Java虚拟机栈的作用非常相似,区别是Java虚拟机栈为虚拟机执行Java方法或者为字节码而服务,本地方法栈是为了Java 虚拟机栈得到Native方法。
  • Java虚拟机栈区域:负责Java的解释过程、程序的执行过程、入栈和出栈,它是与线程相关的,当启动一个新的线程时,Java程序就会分配一个Java 虚拟机栈提供运行;Java 虚拟机栈从方法入栈到具体字节码执行是一个双层栈结构,可以栈里包含栈。
  • 程序计数器:记录线程执行位置,线程私有,因为操作系统不停的调度,无法获取到线程被调度之前的位置,程序计数器提供了这样一个线程执行位置。
  • 元空间区域:在原来的老的Java 7之前划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。在现在Java8后类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
  • 直接内存:使用了Java 的直接内存的API的内存,例如缓冲ByteBuffer,可以控制虚拟机参数调整大小,而本地内存是使用了native函数操作的内存,是不受JVM管理控制。

堆内存空间

JVM回收的主要目标是堆内存,对象主要的创建分配内存在堆上进行,堆可以想象成一个对象池子,对象不停创建放入池子中,而JVM垃圾回收是不停的回收池子中一些被标记为可回收对象的对象,启动回收线程进行打扫战场,当回收对象的速度赶不上程序的创建时,池子就会立马满,当满了之后从而发生溢出,就是常见的OOM。

GC的速度和堆的内存中存活对象的数量有关,与堆内存所有的对象无关,GC的速度和堆内存的大小无关,如一个4GB大小的堆内存和一个16GB的堆内存,只要2个堆内存存活对象都是一样多的时候,GC速度都是基本差不多。每次垃圾回收也不是必须要把垃圾清理干净,重要的是保证不把正在使用的对象给标记清除掉。

2.2 堆内存管理

JVM中占用内存空间最大的是堆内存,平常对象的创建大部分都是在堆上分配内存的,是Java垃圾回收的主要目标和方向、是
Java内存管理机制的核心组成部分,它可以自动管理
Java程序的内存分配和释放,Java垃圾收集器可以自动检测和回收不再使用的内存,以便重新分配给其他需要内存的程序。这种自动内存管理的机制可以提高程序的运行效率和可靠性,防止因内存泄漏等问题导致程序崩溃或性能下降,Java
垃圾收集器使用了不同的垃圾回收算法和垃圾收集器实现,以适应不同的应用场景和需求。Java垃圾收集器的性能特征和优化技术也是
Java程序员需要了解和掌握的重要知识。

因此,了解 Java垃圾回收的背景、原理和实践经验对于编写高效、可靠的 Java程序非常重要。

2.2.1 对象如何被判断为可回收

JVM怎么判断堆内存里面的对象是否可回收的,就是当一个对象没有任何引用指向它了,它就是可回收对象,判断的方式有两种算法,一个是引用计数法,一个是可达性分析法。

可回收对象:

图片

(1)引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,这个计数器值加一,当引用失效断开时,计数器值就减一,在任何时刻时计数器为0的时候,代表这个对象是可以被回收的,没有任何引用使用它了。

图片

引用计数法是有缺点,当对象直接互相依赖引用时,这些对象的计数器都不能为0,都不能被回收。

(2)可达性分析法

它使用tracing(链路追踪)方式寻找存活对象的方法,通过一些列称为“GC Roots”的对象作为初始点,从这些初始点开始向下查找,直到向下查找没有任何链路时,代表这个对象可以被回收,这种算法是目前Java唯一且默认使用来判定可回收的算法。

图片

2.2.2 GC Roots的概念和对象类型

  • Java 虚拟机栈中引用的对象,例如各个线程被调用的方法栈用到的参数、局部变量或者临时变量等。
  • 方法区的静态类属性引用对象或者说Java类中的引用类型的静态变量。
  • 方法区中的常量引用或者运行时常量池中的引用类型变量。
  • JVM内部的内存数据结构的一些引用、同步的监控对象(被修饰同步锁)。
  • JNI中的引用对象。
  • 当然,被GC
    Roots追溯到的对象不是一定不会被垃圾回收,具体需要看情况,Java
    对象与对象引用存在四种引用级别:分别是强引用、软引用、弱引用、虚引用,默认的对象关系是强引用,只有在和GCRoots没有关系时才会被回收;软引用用于维护一些可有可无的对象,当内存足够时不会被回收;弱引用只要发生了垃圾回收就会被清理;虚引用人如其名形同虚设,任何对象都与它无关。

    2.2.3 垃圾对象回收算法

    当JVM定位到了那些对象可回收时,这个时候是通过三个算法标记清除,分别是标记清除算法、复制算法、标记压缩算法。

    (1)标记清除算法

    首先标记出所有需要回 收的对象,在标记完成后,统一回收掉所有被标记的对象,但是该算法缺点是执行效率低,当大量对象时需要大量标记和清理动作,而且容易产生内存碎片化,当需要一块连续内存时,会因为碎片化无法分配。

    图片

    (2)标记压缩算法

    标记压缩算法跟清除算法很像,只不过它对内存进行了整理, 让存活对象都向内存空间的一端移动,然后将边界的其它对象全部清理,这样能达到内存碎片化问题,不过它比清除算法多了移步动作。

    图片

    (3)复制算法

    为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,将存活对象复制到一块空置的空间里,然后将原来的区域全部清理,缺点是需要额外空间存放存活对象。

    图片

    2.2.4 分代垃圾回收模型概念和原理

    堆内存分代模型图

    图片

    当JVM进行GC(垃圾回收)时,JVM会发起“Stop the world”,所有的业务线程都进行停止,进入SafePoint状态,JVM回收垃圾线程开始进行标记和追溯,如何解决这种停止和如何减少STW的时间呢?

    目前主流垃圾收集器采用分代垃圾回收方式,大部分对象的声明周期都比较短,只有少部分的对象才存活的比较长,分代垃圾回收会在逻辑上把堆内存空间分为两部分,一部分为年轻代,一部分为老年代。

    (1)年轻代空间

    年轻代主要是存放新生成的对象,一般占用堆空间的三分之一空间,因为会频繁创建对象,所以年轻代GC频率是最高的。

    分为Eden空间、Survivor1(from)区、Survivor2(to)区,S1和S2总要有一块空间是空的,为了方便年轻代存活对象来回存放,晋升存活对象年龄。

    三个区的默认比例是8:1:1,可以通过配置参数调整比例。

    年轻代回收发起Minor GC(YongGC),当Eden内存区域被占满之后就发起GC,短暂的STW,基于垃圾收集器。

    (2)老年代空间

    是堆内存中最大的空间, ,里面的对象都是比较稳定或者老顽固,GC频率不会频繁执行。

    图片

    老年代对象:

  • 正常提升:由年轻代存活对象年龄到达阈值时,这个对象则会被移动到老年代中。
  • 分配担保:如果年轻代中的空间不足时,此时有新的对象需要分配对象空间,需要依赖其它内存进行分配担保,老年代担保直接创建。
  • 大对象:当创建需要大量连续内存空间的对象时,如长字符串或者数组等,大小超过了阈值时,直接在老年代分配。
  • 动态年龄对象:有的垃圾收集器不需要到达指定年龄大小直接晋升老年代,比如相同年龄的对象的大小总和 > Survivor空间的50%, 年龄大于等于该年龄对象直接移动老年代,无需等待正常提升。
  • 老年代回收发起Major GC / FULL GC,当老年代满时会触发MajorGC,通常至少经历过一次Minor GC,再紧接着进行Major GC, Major GC清理Tenured区,用于回收老年代(CMS才能单独清理)。

    FUll GC:清除整个堆空间,一般来说是针对整个新生代、老生代、元空间的全局范围的清理。

    不管是Major GC还是 Full GC, STW的耗时都是Ygc的十倍以上,所以说对象能在年轻代被回收是最优的。

    Full GC触发条件:

    • 老年代空间不足。
    • 元空间不足扩容导致。
    • 程序代码执行System.gc时可能会执行。
    • 当程序创建一个大对象时,Eden区域放不下大对象,老年代内存担保分配,老年代也不足空间时。
    • 年轻代存留对象晋升老年代时,老年代空间不足时。

    2.2.5 Java对象内存分配过程

    图片

     对象的分配过程

  • 编译器通过逃逸分析优化手段,确定对象是否在栈上分配还是堆上分配。
  • 如果在堆上分配,则确定是否大对象,如果是则直接进入老年代空间分配, 不然则走3。
  • 对比tlab, 如果tlab_top + size
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论