一、背景
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的概念和对象类型
当然,被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频率不会频繁执行。
老年代对象:
老年代回收发起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对象内存分配过程
对象的分配过程