为什么要学习JVM?
jvm和算法是java程序员的内功
内存与对象
1.Java内存区域分布于概述
五个区域有各种划分方式,接下来我是以生命周期以及线程共享/私有性维度去划分
与线程生命周期相关的区域/线程私有
下面三个都是随线程生而生随线程销毁而销毁的,而且是每个线程独有
- 程序计数器
- Java虚拟机栈
- 本地方法栈
与JVM生命周期相关的区域/线程共享
- 堆:
1.整个JVM进程中只有一个堆空间,在JVM启动时创建,JVM关闭时销毁。
- 所有线程共享一个堆空间,用于存放对象实例和数组。
- 方法区:
1.方法区也与JVM生命周期一致,JVM启动时创建,JVM关闭时销毁。
2.所有线程共享一个方法区,用于存放类信息、常量、静态变量等。
程序计算器
是什么
- 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器
- 线程是一个独立的执行单元,是由CPU控制执行的
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
比如有一个java文件Test.java,程序计数器指的不是这个java文件的某一行代码,而是这个java文件编译后即 Test.class对应的执行代码行数
javac Test.java -> Test.class
为什么为了线程切换后能恢复到正确的执行位置
每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存
特点
- 内存区域中唯⼀⼀ 个没有规定任何 OutOfMemoryError 情况的区域
JAVA虚拟机栈
是什么?
- 用于作用于方法执行的一块Java内存区域
为什么?
- 每个方法在执行的同时都会创建一个栈帧(Stack Framel)
栈 是一种数据结构 先进后出
用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
说一下栈帧
局部变量表(Local Variable Array)
局部变量表用于存储方法参数和方法内部定义的局部变量。
它是一个数组,索引从0开始。对于实例方法,索引为0的位置通常存储this引用。操作数栈(Operand Stack)
操作数栈用于字节码指令的操作数临时存储。
在方法执行过程中,字节码指令会将数据压入或弹出操作数栈,进行计算或操作。动态链接(Dynamic Linking)
动态链接用于支持方法调用过程中的符号引用转换为实际的内存地址。
每个栈帧包含一个指向运行时常量池的引用,用于解析方法调用和字段访问的符号引用。方法出口(Method Return Address)
方法出口用于保存方法返回地址,当一个方法执行完成后需要返回到调用它的方法时,程序计数器(PC寄存器)需要恢复到正确的位置继续执行。
public class JVMStackExample {
public static void main(String[] args) {
int result = add(3, 5);
System.out.println("Result: " + result);
}
public static int add(int a, int b) {
int sum = a + b;
return sum;
}
}
//
add方法栈帧
局部变量表:
a:方法参数(索引0,值为3)
b:方法参数(索引1,值为5)
sum:局部变量(索引2,值为a + b的结果8)
操作数栈:
a和b的值被压入操作数栈用于计算。
计算结果sum被压入操作数栈,并作为返回值弹出。
动态链接:
用于解析加法操作和返回指令。
方法出口:
add方法返回到调用它的main方法。
//
main方法栈帧
局部变量表:
args:命令行参数(索引0)
result:add方法的返回值(索引1)
操作数栈:
调用add(3, 5)时,操作数栈用于传递参数3和5,以及保存返回值。
动态链接:
用于解析add方法调用和System.out.println方法调用。
方法出口:
main方法返回到JVM引导程序。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
特点?
- 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用(reference 类型)
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常
- b调用b属于递归调用会报StackOverflowError 栈溢出
本地方法栈
-
是什么?
- 用于作用域本地方法执行的一块Java内存区域
- 本地方法栈与Java虚拟机栈类似,不过它是为本地方法服务的。
一个本地方法是用非Java语言(通常是C或C++)编写的,并通过JNI(Java Native Interface)调用。
-
为什么?
- 与Java虚拟机栈相同,每个方法在执行的同时都会创建一个栈帧(Stack Framel)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
-
特点?
- Hotshot将Java虚拟机栈和本地方法栈合二为一
堆
- 是什么?
- 是Java内存区域中一块用来存放对象实例的区域,【几乎所有的对象实例都在这里分配内存】
- 为什么?
- 此内存区域的唯一目的就是存放对象实例
- Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块
- Java 堆是被所有线程共享的一块内存区域
- 特点
- Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”
- -Xmx -Xms 最大和最小堆内存
- Java堆可以分成新生代和老年代 新生代可分为To Space、From Space、Eden
方法区
- 是什么?
- 是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 什么是类信息:类版本号、方法、接口
- 为什么?
内存中存放类信息、静态变量等数据,属于线程共享的一块区域
Hotspot使用永久代来实现方法区 JRockit、IBM J9VM Java堆一样管理这部分内存
在JDK 8之前,方法区通常被称为永久代(PermGen),在JDK 8及之后,永久代被移除,改为使用元空间(Metaspace)。元空间不再使用堆内存,而是直接使用本地内存,这大大改善了内存管理和性能问题。
方法区是Java虚拟机规范中的一个逻辑概念
元空间是HotSpot JVM在JDK 8及以后对方法区的一种实现-
为什么移除永久代?
永久代存在以下几个问题:- 固定大小:永久代的大小在启动时确定,运行时无法动态调整,容易导致内存溢出。
- 垃圾回收复杂:永久代的垃圾回收与堆的垃圾回收策略不同,导致回收效率低下。
- 内存调优困难:永久代的内存调优比较复杂,不容易调整到最优状态。
-
元空间解决了这些问题:
- 使用本地内存:元空间使用操作系统的本地内存,大小可以动态调整。
- 提高回收效率:元空间的回收与其他区域(如堆)的回收分开进行,简化了垃圾回收过程。
- 更好的性能和稳定性:由于使用本地内存,元空间的性能和稳定性得到了提升
-
特点
- 并非数据进入了方法区就如永久代的名字一样“永久”存在了。
这区域的内存回收目标主要是针对常量池的回收和对类型的卸载 - 方法区也会抛出OutofMemoryError,当它无法满足内存分配需求时
- 运行时常量池 运行时常量池是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 并非数据进入了方法区就如永久代的名字一样“永久”存在了。
public class A {
public static void main(String[] args) {
String a = "abc";//"abc"存放在方法区常量池
String b = "abc";
System.out.println(a==b);//true
String c = new String("abc");//存放在堆里
System.out.println(a==c);//false
System.out.println(a==c.intern());//true
}
}
2.对象是怎么来的
1.虚拟机遇到一条new指令时,首先检查这个对应的类能否在常量池中定位到一个类的符号引用
2.判断这个类是否已被加载、解析和初始化
-
3.为这个新生对象在Java堆中分配内存空间,
其中Java堆分配内存空间的方式主要有以下两种- 指针碰撞
- 分配内存空间包括开辟一块内存和移动指针两个步骤
- 非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性(乐观锁原理,下面同理)
- 空闲列表
- 分配内存空间包括开辟一块内存和修改空闲列表两个步骤
- 非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
- 指针碰撞
4.将分配到的内存空间都初始化为零值
-
设置对象头相关数据
- GC分代年龄
- 对象的哈希码 hashCode
- 元数据信息
5.执行对象<init>方法
对象结构
-
对象头 用于存储对象的元数据信息:
- Mark Word 部分数据的长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit,存储对象自身的运行时数据如哈希值等。Mark Word一般被设计为非固定的数据结构,以便存储更多的数据信息和复用自己的存储空间。
- 类型指针 指向它的类元数据的指针,用于判断对象属于哪个类的实例。
实例数据 存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面。
对齐填充部分 仅仅起到占位符的作用
3.创建了对象我们是如何访问的
- 当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种(HotSpot虚拟机采用的是第二种):
1. 使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针。
2. 直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。
- 对比
从垃圾回收的角度句柄访问更优
从访问效率的角度直接指针访问更优垃圾回收分析:方式1️⃣当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;方式2️⃣垃圾回收时需要修改reference中存储的地址。
访问效率分析,方式二优于方式一,因为方式二只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。
句柄访问对象
直接指针访问对象
垃圾回收
1.什么是垃圾回收
战略意义 能做出一个需求的同时也要懂得其对应的战略意义
-
为什么要垃圾回收?
- Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存
-
垃圾回收的过程是怎样的?
1. 对象都是放在堆里面,所以绝大部分(几乎所有)的垃圾回收进行在堆 2.怎么触发?触发机制 2.1 想要放一个内存但是放不进去 2.2 堆空间达到一个比例的时候触发
-
如果让你考虑垃圾回收算法你会怎么设计
-
完成哪些对象回收哪些对象不回收的功能需求
通过算法 例如通过引用计数法 简单来说调用的时候+1,断开调用-1
-
-
对象是否存活判断
- 堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。
- 当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),
- 但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。
- 任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1
2.什么时候被回收
对象存活算法之引用计数法
引用计数法存在的特点分析
-
优缺点
- 优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
-
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.
对象存活之可达性分析
可达性算法解决了上面引用计数的缺点问题
-
可达性分析算法的概念(又叫跟搜索法)
- 根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点
一个对象从某个根节点开始,任何一条引用链能引用到的
- 根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点
-
java中可作为GC Root的对象有
虚拟机栈中引用的对象(本地变量表)
本地方法栈中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
3.回收算法
3.1标记清除
标记-清除算法是垃圾回收机制中的一种基础算法。它包括两个主要阶段:标记(Mark)和清除(Sweep)。
- 标记阶段
在标记阶段,垃圾回收器会遍历所有可达对象,并将其标记为“存活”。遍历从根集合(Root Set)开始,根集合通常包括栈中的引用、全局静态变量和寄存器中的引用。所有从根集合可以访问到的对象都被认为是存活的。
- 标记阶段
- 清除阶段
在清除阶段,垃圾回收器会遍历堆中的所有对象,清除未被标记的对象,即那些不可达的对象。被清除的对象的内存会被释放,以供新对象分配使用。
- 清除阶段
标记-清除算法的优缺点
- 优点
简单:算法逻辑简单,容易实现。
无需移动对象:清除阶段只是释放未标记对象的内存,而不需要移动对象,因此减少了内存复制的开销。 - 缺点
内存碎片:清除阶段后,释放的内存块可能是不连续的,导致内存碎片问题。这会使得大对象的分配变得困难。
停顿时间长:标记和清除阶段需要暂停应用程序(即STW,Stop-The-World),对实时性要求高的应用影响较大。
3.2复制算法
- 为甚么出现复制算法?
- 为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按量划分为大小相等的两块,每次只使用其中的一块
- 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
- 现在的商业虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。 Survivor from 和Survivor to ,内存比例 8:1:1
-
当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1, 也就是每次新生代中可用内存空间为整个新生代容量的 90% (80%+10%),只有 10% 的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
上面和下面各自为S1和S2
复制算法(Copying Algorithm)
复制算法(Copying Algorithm)是一种垃圾回收算法,通过将存活对象从一个内存区域复制到另一个内存区域,从而有效地管理内存并避免内存碎片问题。该算法通常用于新生代(Young Generation)的垃圾回收。
基本原理
复制算法将堆内存分为两个等大的空间:from-space和to-space。在任何时间点,一个空间是活动的,另一个空间是空闲的。当垃圾回收发生时,所有存活对象从活动空间复制到空闲空间,并释放整个活动空间。
- 步骤
1.初始状态:对象分配在活动空间(from-space)。
2.标记存活对象:从根集合开始,标记所有存活对象。
3.复制存活对象:将存活对象从活动空间复制到空闲空间(to-space),并更新相应的引用。
4.交换空间:空闲空间变为新的活动空间,旧的活动空间被清空,成为新的空闲空间。 - 优点
消除内存碎片:通过复制存活对象到连续的内存区域,有效地消除内存碎片。
分配速度快:由于总是从连续的空闲区域分配内存,内存分配速度非常快。 - 缺点
内存开销大:需要双倍的内存空间(from-space和to-space),内存利用率较低。
复制开销:存活对象的复制会增加一定的开销,尤其是在存活对象较多时。
本质就是 空间换时间
3.3标记整理算法与分代收集算法
-
标记整理算法解决了什么问题
- 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100%存活的极端情况,所以在老年代一般不能直接选用这种算法
-
标记-整理
- 根据老年代的特点,有人提出了另外一种“标记-整理(Mark- Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 分代收集
- 一般把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记一整理”算法来进行回收
4.垃圾回收器有哪些
- JDK 8
默认垃圾回收器:Parallel GC
可选垃圾回收器:
Serial GC
Parallel GC
CMS(Concurrent Mark-Sweep)GC
G1(Garbage First)GC(可以选择使用,但不是默认) - JDK 9
默认垃圾回收器:G1 GC
可选垃圾回收器:
Serial GC
Parallel GC
CMS GC
G1 GC - JDK 11
默认垃圾回收器:G1 GC
可选垃圾回收器:
Serial GC
Parallel GC
CMS GC
G1 GC
ZGC(Z Garbage Collector) - JDK 12
默认垃圾回收器:G1 GC
可选垃圾回收器:
Serial GC
Parallel GC
CMS GC(JDK 14开始废弃)
弃用原因:
随着 JDK 版本的迭代,CMS 收集器的停顿时间较长,在大堆内存应用中性能表现不佳,逐渐被 G1 收集器替代。
G1 GC
ZGC
Shenandoah GC
- 详细说明
JDK 8:默认使用Parallel GC,用户可以选择Serial GC、CMS GC或G1 GC。
JDK 9:G1 GC成为默认垃圾回收器,因为它提供了更好的性能和较短的停顿时间,尤其适合大堆内存的应用程序。但其他垃圾回收器仍然可用。
JDK 11:继续以G1 GC作为默认垃圾回收器,并引入了ZGC。ZGC设计用于超大堆内存,目标是提供低于10ms的停顿时间。其他垃圾回收器仍然可用。
JDK 12及之后:默认仍为G1 GC,但增加了Shenandoah GC,Shenandoah GC的目标是实现更低的停顿时间,适用于大堆内存应用。CMS GC在JDK 14中被废弃,但在之前版本中仍可用。
每种垃圾回收器有哪些特色
Serial 收集器(Serial GC):
它是一种单线程的垃圾回收器,使用标记-复制算法进行新生代的垃圾回收,使用标记-清除算法进行老年代的垃圾回收。适用于小型或中等规模的应用。
优点:简单高效,适用于单核或小型应用。
缺点:单线程处理,无法充分利用多核处理器。
serial垃圾收集器的特点
- “Stop The World”,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。在用户不可见的情况下把用户正常工作的线程全部停掉
- 使用场景:多用于桌面应用,Client端的垃圾回收器
- 桌面应用内存小,进行垃圾回收的时间比较短,只要不频繁发生停顿就可以接受
Parallel 收集器(Parallel GC):
ParNew 收集器其实就是 Serial 收集器的多线程版本
ParNew 收集器除了多线程收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
- 使用-XX: ParallelGCThreads 参数来限制垃圾收集的线程数
- 多线程操作存在上下文切换的问题,所以建议将-XX: ParallelGCThreads设置成和CPU核数相同,如果设置太多的话就会产生上下文切换消耗
它是一种多线程的垃圾回收器,也称为吞吐量优先的垃圾回收器。在新生代使用标记-复制算法,在老年代使用标记-清除算法。适用于多核服务器应用,可提高吞吐量。
优点:多线程处理,适用于多核处理器,提高吞吐量。
缺点:全停顿,可能造成较长的停顿时间。
CMS 收集器(Concurrent Mark-Sweep GC):
它是一种并发垃圾回收器,使用标记-清除算法。在并发标记和并发清除阶段尽量减少应用程序停顿时间。适用于需要较短停顿时间的应用。
优点:并发标记和清除,减少停顿时间。
缺点:产生碎片,可能导致 Full GC 频繁执行,吞吐量不如 Parallel 收集器高。
- 对CPU资源非常敏感
- 无法处理浮动垃圾,程序在进行并发清除阶段用户线程所产生的新垃圾
- 标记-清除暂时空间碎片
G1 收集器(Garbage-First GC):
它是一种面向整个堆的并发垃圾回收器,适用于大堆内存应用。使用分代收集的思想,将堆内存分为若干个大小相等或相近的区域,实现了更灵活和高效的垃圾回收。在垃圾回收过程中,G1优先选择包含垃圾最多的区域,并将其中的存活对象复制到空闲区域。
优点:面向大堆内存,可预测的停顿时间。
- 空间整合:基于“标记一整理”算法实现为主和Region之间采用复制算法实现的垃圾收集
- 可预测的停顿:这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型
- 在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大差别,它将整个 Java 雄划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔髙的了,它们都是一部分 Region(不需要连续)的集合。
- G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Regions 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage- Firsti 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高
缺点:初始标记和最终标记暂停时间较长,吞吐量相对较低。
ZGC(Z Garbage Collector):
ZGC是一种低延迟的、可伸缩的垃圾回收器,旨在减少GC停顿时间并提供可预测的性能。它是一种并发垃圾回收器,能够在几毫秒甚至数毫秒的时间内处理几十GB甚至数百GB的堆内存。ZGC适用于需要低延迟和大内存堆的应用场景,比如金融、游戏和电商等领域。
Shenandoah GC:
Shenandoah GC是一种低停顿时间的垃圾回收器,旨在减少GC停顿时间,并提供可预测的性能。它是一种并发垃圾回收器,能够在几毫秒的时间内处理几十GB甚至数百GB的堆内存。Shenandoah GC适用于需要低停顿时间和大内存堆的应用场景,比如云计算和大数据等领域。
Epsilon GC:
Epsilon GC是一种实验性的垃圾回收器,它完全不进行内存回收。Epsilon GC适用于特定的测试场景和性能调优场景,可以用来验证GC对应用程序性能的影响,或者用作一种特殊的性能优化工具。
堆内存到底是怎么分配
Java堆内存是运行时数据区域,所有类的实例和数组在堆中分配内存。堆内存根据分代垃圾回收的理念分为不同的区域,以优化垃圾回收过程。这些区域主要包括新生代(Young Generation)、老年代(Old Generation),以及在JDK 8之前的永久代(Permanent Generation)或在JDK 8之后的元空间(Metaspace)。以下是对这些区域的详细解释及其内存分配方式:
新生代(Young Generation)
新生代主要存放短生命周期的对象。它进一步细分为以下三个区域:
- Eden区:所有的新对象首先在Eden区分配内存。
- Survivor区:新生代包含两个Survivor区,分别是Survivor 0(S0)和Survivor 1(S1)。在进行垃圾回收时,Eden区和一个Survivor区中的存活对象将被复制到另一个Survivor区。
新生代垃圾回收(Minor GC)
- 当Eden区填满时,会触发一次Minor GC。在这次回收过程中,存活的对象会被复制到空闲的Survivor区(S0或S1),Eden区和被使用的Survivor区将被清空。
- 经历过多次Minor GC且仍然存活的对象将被晋升到老年代。
老年代(Old Generation)
老年代用于存放生命周期较长的对象。新生代中的对象经过多次Minor GC仍然存活后,会被移到老年代。老年代的空间相对较大,但垃圾回收的频率较低,采用的垃圾回收算法通常是标记-清除或标记-整理。
老年代垃圾回收(Major GC / Full GC)
- 老年代的垃圾回收频率较低,但回收过程会引起较长时间的应用停顿。
- 老年代的垃圾回收通常是标记-清除(Mark-Sweep)或标记-整理(Mark-Compact)算法。
永久代(Permanent Generation)
在JDK 8之前,永久代用于存放类的元数据,如类的结构信息、方法信息和常量池。永久代的大小是固定的,由-XX:PermSize
和-XX:MaxPermSize
参数设置。
JDK 8之后:元空间(Metaspace)
- 从JDK 8开始,永久代被移除,改为元空间(Metaspace)。元空间不在堆内存中,而是在本地内存中分配。
- 元空间的大小可以通过
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
参数进行配置。 - 元空间的动态扩展解决了永久代可能出现的OutOfMemoryError问题。
内存分配示例
以下是一个示例代码和堆内存分配的解释:
public class HeapMemoryAllocation {
public static void main(String[] args) {
// 对象分配在Eden区
byte[] array1 = new byte[1024 * 1024]; // 1 MB
byte[] array2 = new byte[1024 * 1024]; // 1 MB
// 触发Minor GC
byte[] array3 = new byte[1024 * 1024]; // 1 MB
// 在Minor GC后,存活对象被复制到Survivor区
// 对象晋升到老年代
for (int i = 0; i < 10; i++) {
byte[] array4 = new byte[1024 * 1024]; // 1 MB
}
// 在多次Minor GC后,存活对象晋升到老年代
}
}
在这个示例中:
-
array1
和array2
对象首先分配在Eden区。 - 创建
array3
对象后,Eden区可能会满,触发一次Minor GC,将存活的array1
和array2
复制到Survivor区。 - 在循环中创建
array4
对象,可能会触发多次Minor GC,最终将一些存活对象晋升到老年代。
总结
- 新生代:包括Eden区和两个Survivor区,用于存放短生命周期对象,采用标记-复制算法。
- 老年代:存放生命周期较长的对象,采用标记-清除或标记-整理算法。
- 永久代/元空间:存放类元数据,从JDK 8开始由永久代改为元空间,使用本地内存进行分配。
内存分配和垃圾回收策略旨在优化应用程序性能,减少停顿时间,提高内存使用效率。不同的垃圾回收器可能有不同的实现细节,但基本思想是类似的。
大对象的分配
对象大小超过了堆的10%,即一个S1的空间,会直接分配到老年代
FullGC与MinorGC
Minor GC触发条件:当Eden区满时,触发Minor GC
-
FullGC触发条件
调用 System.gc()
此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC
此项目中出现频繁FullGC,也就是系统空间分配不足导致的系统堆内存强制回收
-
问题解决方法分析
- 由于本机单服务内存过大导致,此场景下Full GC,而且需要回收的内存很大,持续时间过长
- 解决停顿时间过长问题,缩短GC时间
即调优目的为减少Full GC次数以及 一次Full GC时间
解决Full GC 方案一
假设是单机32G内存 -那么默认最大堆内存为30G 即Xmx30G
- 32G内存-xmx30G,系统每次进行FullGC时长太长
- 可以减少-xmx大小成4G,从而缩短Full GC
- 最终解决方案:集群部署,第一个节点4G 第二个节点4G 第三个节点4G 用nginx配置转发 upstream
互联网项目常见JVM问题讲述
- 为什么访问量大就越容易出问题
- 判断一个用户是否在白名单 List.contain(用户) true,==》Set.contain(用户) 通过hash比较==》布隆过滤器
- 结论==》用户量大和用户量小的项目遇到的问题和解决方案也就不一样
- 案例1,关于死锁问题
- 解决方案==》jstack -m命令查看帮我们检测是否有死锁,或者jconsole、jvisualVM也能检查
- new Thread的时候最好带上名称
- 案例2,堆内存泄漏问题
- 现象:出现OOM或者Full GC,heap使用率明显上升,经常达到Xmx
- Full GC出现的正常频率是大概一天一到两次
- 解决方案==》jmap dump下内存/heap dump on OOM/heap dump on FullGC+jhat本地分析,
或者jconsole、jvisualVM也能检查
-
案例3,堆外内存泄漏
现象:heap使用率很低,但是出现了OOM或者Full GC
解决方案==》可以用btrace跟踪DirectByteBuffer的构造函数来定位