JAVA-JVM 内存模型&类加载器&GC算法&GC调优
- 什么是JVM
- JVM 内存模型
- JVM的GC算法
- JVM类加载器
什么是JVM
?
[[jvm]]是Java Virtual Machine(Java虚拟机)的缩写,JVM是一个虚构出来的计算机,有着自己完善的硬件架构,如处理器、堆栈等。
为什么需要JVM?
?
我们都知道 Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件, 而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。 也就是如下: ① Java 源文件—->编译器—->字节码文件
② 字节码文件—->JVM—->机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够 跨平台
的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会 存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享
JVM内存模型
内存区域 | 作用 | 特点 |
---|---|---|
程序计数器 | 一块较小的内存空间,当前线程所执行字节码的行号指示器 | 线程私有,不会内存溢出 |
Java 虚拟机栈 | 存储栈帧,用于方法执行 | 线程私有,可能出现 StackOverflowError 和 OutOfMemoryError |
本地方法栈 | 存储本地方法的栈帧 | 线程私有,情况与 Java 虚拟机栈类似 |
Java 堆 | 存储对象实例和数组等 | 被所有线程共享,可能出现 OutOfMemoryError |
方法区 | 存储已被虚拟机加载的类信息、常量、静态变量等 | 被所有线程共享,JDK 8 及之后用元空间实现,可能出现 OutOfMemoryError |
运行时常量池 | 存放编译期生成的各种字面量和符号引用 | 方法区的一部分,具有动态性 |
Jvm Heap 堆分区
Heap内存逻辑分区
?
堆大小 = 新生代
+ 老年代
。其中,堆的大小可以通过参数 –Xms
、-Xmx
指定。
Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年 代。
1、新生代
Eden + Survivor
- eden:对象最初创建的区域
- survivor:eden幸存下来的对象所在的区域。
2、老年代
主要存放程序中年龄较大和需要占用大量内存空间的对象
老年代的对象比较稳定,所以 ·MajorGC
不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
在 HotSpot 虚拟机中,新生代默认情况下 Eden 和 Survivor 的比例是 8:1:1。
即 Eden 区占新生代空间的 8/10,Survivor0(S0)和 Survivor1(S1)各占新生代空间的 1/10。
这样的比例设置主要是为了在进行 Minor GC 时,能够有足够的空间存储存活的对象,同时也尽量减少复制操作的开销。如果 Survivor 区设置得太小,可能会导致一些存活时间较长的对象过早地晋升到老年代,增加老年代的压力;如果设置得太大,又会浪费新生代的空间。
MajorGC 采用标记清除算法
:首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出 OOM(Out of Memory)异常。
新生代和老年代的作用?
?
title: 答
**新生代**
作用:
主要用来存放新创建的对象。大多数对象在创建后不久就会变得不可达,因此新生代适合使用`复制算法`进行垃圾回收。
新生代的目的是`快速回收那些生命周期短的对象,减少内存碎片的产生`。
组成:
一般分为
- Eden 区
- Survivor0(S0)区
- Survivor1(S1)区。
新创建的对象首先在 Eden 区分配内存。当 Eden 区满时,会触发一次 Minor GC(小型垃圾回收),将 Eden 区和其中一个 Survivor 区(比如 S0)中仍然存活的对象复制到另一个 Survivor 区(S1)中。经过多次 Minor GC 后仍然存活的对象会被晋升到老年代。
**老年代**
作用
存放生命周期较长的对象。这些对象经过多次垃圾回收后仍然存活,或者是一些大对象直接在老年代分配内存。
==老年代的垃圾回收频率相对较低,但是每次回收所花费的时间较长==。
**特点**:
一般使用`标记 - 整理算法`或`标记 - 清除算法`进行垃圾回收。因为老年代中的对象通常比较稳定,不适合使用复制算法,否则会浪费大量的内存空间进行复制操作。
Minor GC(小型垃圾回收)
?
Minor GC(小型垃圾回收)是 Java 虚拟机中针对新生代的垃圾回收操作。
一、触发时机
当新生代内存空间不足时,就会触发 Minor GC。例如,新创建的对象不断在新生代的 Eden 区分配内存,当 Eden 区满了之后,就会触发一次 Minor GC。
二、执行过程
- 首先,将 Eden 区和其中一个 Survivor 区(比如 S0)中仍然存活的对象复制到另一个 Survivor 区(S1)中。
- 然后,清空 Eden 区和刚才作为源 Survivor 区(S0)的内存空间。
- 经过多次 Minor GC 后,仍然存活的对象会被晋升到老年代。
三、特点 - 回收速度相对较快,因为新生代中的对象通常生命周期较短,数量相对较少,所以垃圾回收的时间较短。
- 较为频繁,由于新创建的对象大多在新生代,而且很多对象很快就会变成垃圾,所以 Minor GC 可能会比较频繁地发生
Major GC(大型垃圾回收)
?
Major GC(大型垃圾回收)是 Java 虚拟机中针对老年代的垃圾回收操作。
一、触发时机
- 老年代空间不足时触发。当老年代中的对象占用的内存空间达到一定比例或者老年代剩余空间无法容纳新的对象时,就会触发 Major GC。
- 有时候在 Minor GC 之前,JVM 可能会检查老年代的可用空间,如果老年代空间不足以容纳从新生代晋升过来的对象,也会触发 Major GC。
二、执行过程 - 标记阶段:首先标记出老年代中仍然存活的对象。这个过程可能会遍历整个老年代以及从老年代到新生代的引用,以确定哪些对象是可达的。
- 清理阶段:对于标记为不可达的对象进行清理,释放它们占用的内存空间。根据不同的垃圾回收算法,清理阶段的具体操作会有所不同。
- 如果采用标记 - 清除算法,会直接清理被标记为不可达的对象,但可能会产生内存碎片。
- 如果采用标记 - 整理算法,在清理的同时会对存活的对象进行整理,将它们移动到一端,使内存空间更加连续。
三、特点
- 回收速度相对较慢。因为老年代中的对象通常数量较多,而且可能存在大量的长期存活对象,所以垃圾回收的时间会比较长。
- 频率较低。由于老年代中的对象相对稳定,不像新生代中的对象那样频繁地变成垃圾,所以 Major GC 的发生频率要比 Minor GC 低得多。
Full GC(全量垃圾回收)
?
Full GC(全量垃圾回收)是对整个 Java 堆(包括新生代和老年代)进行的垃圾回收操作。
触发时机:
- 老年代空间不足:当老年代中的对象占用的内存空间达到一定比例或者老年代剩余空间无法容纳新的对象时,会触发 Full GC。
- 永久代 / 元空间(在 JDK 8 及之后)空间不足:如果永久代 / 元空间中存储的类信息、常量、静态变量等占用的内存空间达到一定限制,也会触发 Full GC。
- System.gc () 被调用:在代码中显式地调用 System.gc () 方法会建议 JVM 进行垃圾回收,但 JVM 不一定会立即执行 Full GC,具体是否执行以及执行的时机取决于 JVM 的实现和当前的运行状态。
- CMS 垃圾回收器在进行并发标记和并发清理过程中出现 “Concurrent Mode Failure”:当 CMS 垃圾回收器在并发标记阶段,由于老年代中的对象增长速度过快,导致在并发清理阶段开始之前,老年代已经没有足够的空间来容纳新分配的对象,这时会触发 Full GC,并切换到 Serial Old 垃圾回收器进行老年代的回收。
影响: - 停顿时间较长:由于 Full GC 需要扫描整个堆内存,所以它的执行时间通常比 Minor GC 要长得多。在 Full GC 执行期间,应用程序会暂停所有的线程,这可能会导致应用程序出现明显的停顿,影响用户体验。
- 资源消耗较大:Full GC 需要消耗大量的 CPU 和内存资源,这可能会影响应用程序的其他部分的性能。
为了减少 Full GC 的发生频率,可以采取合理调整堆内存大小、优化对象创建和生命周期管理、选择合适的垃圾回收器、监控和分析垃圾回收情况等措施。
如何确定垃圾?
?
垃圾搜索算法
- 引用计数法
- 可达性分析法 (根搜索 Root Seaching)
- 三色标记
NOTE:
在Java语言中,可作为GC Roots
的对象包括下面几种: 虚拟机栈
(栈桢中的本地变量表)中的引用的对象- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI(Native方法)的引用的对象
垃圾收集算法
??
一、标记清除算法
1. 算法过程
- 分为标注和清除两个阶段。首先从根集合进行两次扫描,在标记阶段标记出所有需要回收的对象,然后在清除阶段回收被标记的对象所占用的空间。
2. 适用场景 - 存活对象比较多的情况下使用,
多用于老年代
。
3. 优缺点 - 优点:算法相对简单,实现容易。
- 缺点:需要扫描两次,效率比较低;容易产生
内存碎片化
问题,可能导致后续大对象无法找到可利用空间。
二、复制算法
1. 算法过程 - 按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后,从根集合进行一次扫描,对存活的对象进行标记,然后将尚存活的对象复制到另一块上去,最后把已使用的内存清掉。
2. 适用场景 - 存活对象比较少的情况下使用,多用于年轻代。
3. 优缺点 - 优点:实现简单,内存效率高,不易产生碎片。
- 缺点:可用内存被压缩到了原本的一半,且存活对象增多的话,效率会大大降低;需要调整对象的引用。
三、标记整理算法
1. 算法过程 - 标记阶段和标记清除算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象。
2. 适用场景 - 可用于老年代等场景。
3. 优缺点 - 优点:不会产生内存碎片,不会造成空间浪费。
- 缺点:需要扫描两次,需要调整对象的引用。
四、分代收集算法
1. 算法思想 - 根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代。
- 在新生代中,由于每次垃圾收集时都有大批对象死去,只有少量存活,选用复制算法。
- 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记 — 清理” 或者 “标记 — 整理” 算法来进行回收。
GC事件分类
?
垃圾回收(GC)事件可以分为以下几类:
- Young GC (又称Minor GC)
新生代内存的垃圾收集事件称为Young GC(又称Minor GC),当JVM无法为新对象分配在新生代内存空间时总会触发 Young GC,比如 Eden 区占满时。新对象分配频率越高, Young GC 的频率就越高
Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代GC的造成的停顿,几乎可以忽略不计 - Old GC (又称Major GC)
只清理老年代空间的GC事件,只有CMS的并发收集是这个模式 - Full GC
清理整个堆的GC事件,包括新生代、老年代、元空间等 - Mixed GC
清理整个新生代以及部分老年代的GC,只有G1有这个模式
避免 Full GC 或减少其发生频率的常用方法:
?
一、合理设置 JVM 参数
- 设置合适的堆大小:
-Xms(初始堆大小)
和-Xmx(最大堆大小
)应该根据应用程序的实际需求进行设置。如果堆设置得过小,会导致频繁的垃圾回收;如果设置得过大,虽然可以减少垃圾回收的频率,但会增加垃圾回收的时间。一般来说,可以通过监控工具观察应用程序的内存使用情况,逐步调整堆大小以找到一个合适的平衡点。- 调整新生代和老年代的比例:通过 -XX:NewRatio 可以设置新生代和老年代的比例。如果新生代设置得太小,对象会很快晋升到老年代,增加老年代的压力;如果设置得太大,可能会浪费新生代的空间。可以根据应用程序的对象生命周期特点来调整这个比例。
- 选择合适的垃圾回收器:
不同的垃圾回收器有不同的特点和适用场景。例如,对于响应时间要求较高的应用程序,可以选择 CMS(Concurrent Mark Sweep)或 G1(Garbage-First)垃圾回收器。
G1 垃圾回收器可以更好地控制垃圾回收的停顿时间,并且可以同时对新生代和老年代进行垃圾回收。但它需要一定的调优才能发挥最佳性能。二、优化代码
- 避免在循环中创建大量不必要的对象:尽量减少在循环中创建对象的次数,可以考虑对象复用或者使用数据结构来减少对象的创建。
- 及时释放资源:对于不再使用的对象,及时将其引用置为 null,以便垃圾回收器能够回收它们所占用的内存空间。特别是对于一些占用较大内存的资源,如数据库连接、文件流等,要及时关闭和释放。
- 选择合适的数据结构:使用合适的数据结构可以减少内存占用和对象的创建。例如,使用基本数据类型代替包装类,使用 StringBuilder 或 StringBuffer 代替频繁拼接字符串等。
三、监控和分析
- 使用监控工具:如 JConsole、VisualVM 等工具可以实时监控 JVM 的内存使用情况、垃圾回收情况等。通过监控可以及时发现内存问题,并采取相应的措施进行优化。
- 分析垃圾回收日志:垃圾回收日志中包含了丰富的信息,如垃圾回收的类型、时间、回收的对象数量等。通过分析垃圾回收日志可以了解垃圾回收的情况,找出可能导致 Full GC 的原因,并进行针对性的优化。
例如,在一个 Web 应用程序中,可以通过以下方式来避免 Full GC:
根据应用程序的实际需求,合理设置堆大小和新生代、老年代的比例。例如,如果应用程序处理的请求较多,可能需要较大的堆空间来避免频繁的垃圾回收。
在代码中,避免在循环中频繁创建大对象,可以考虑使用对象池来复用对象。对于数据库连接等资源,要及时关闭和释放。
使用监控工具实时监控应用程序的内存使用情况和垃圾回收情况。如果发现老年代的使用增长较快,可以分析代码中是否存在内存泄漏或者对象晋升过快的问题,并进行相应的优化。
垃圾回收器
?
G1
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收 集器两个最突出的改进是:
- 基于
标记-整理算法
,不产生内存碎片。 - 可以非常精确控制停顿时间,在
不牺牲吞吐量前提下,实现低停顿垃圾回收
。
G1 收集器避免全区域垃圾收集,它把堆内存划分为
大小固定的几个独立区域
,并且跟踪这些区域 的垃圾收集进度,同时在后台维护一个优先级列表
,每次根据所允许的收集时间,优先回收垃圾 最多的区域
。区域划分
和优先级区域回收机制
,确保 G1 收集器可以在有限时间获得最高的垃圾收 集效率。
GC调优
GC调优目标
?
大多数情况下对 Java 程序进行GC调优, 主要关注两个目标:响应速度、吞吐量
- 响应速度(Responsiveness) 响应速度指程序或系统对一个请求的响应有多迅速。比如,用户订单查询响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。
- 调优的重点是在短的时间内快速响应
- 吞吐量(Throughput) 吞吐量关注在一个特定时间段内应用系统的最大工作量,例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的GC停顿时间也是可以接受的,因为高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求
GC日志分析
?
GC日志是一个很重要的工具,它准确记录了每一次的GC的执行时间和执行结果,通过分析GC日志可以调优堆设置和GC设置,或者改进应用程序的对象分配模式
开启的JVM启动参数
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
免费的GC日志图形分析工具推荐下面2个:
?
- GCViewer,下载jar包直接运行
- gceasy,web工具,上传GC日志在线使用
垃圾回收器的优化策略
?
堆内存的优化
对GC优化最有效最基础的办法就是调整堆的大小。堆是否需要扩大、要扩多大是一个取舍的问题。如果堆太小,JVM会频繁的进行GC
我们可以给JVM设置一个非常大的堆,但是如果设置一个非常大的堆会面临三个问题:
- 需要占用更多的系统资源。
- 虽然GC频率降低了但是每次GC要扫描的内容变多了,每次GC的停顿会增加了。
- 可能遇见虚拟内存的问题。如果系统进行了内存交换,将内存不活跃的数据复制到磁盘中,这个时候JVM尝试进行GC的时候会生成很大的性能开销,这个时候GC的开销会有数量级的增长。
特别注意:调整堆大小永远不要超过机器的物理内存,如果存在多个JVM,则需要考虑所有JVM设置的总和。通常要给操作系统预留1G的空间
[[JVM性能调优#垃圾回收器的优化策略]]
java3种类加载器
?
JVM 提 供了 3 种类加载器
启动类加载器(Bootstrap ClassLoader) ·
负责加载 JAVA_HOME/lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被 虚拟机认可(按文件名识别,如 rt.jar)的类。扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME/lib/ext 目录中的,或通过java.ext.dirs
系统变量指定路径中的类 库。应用程序类加载器(Application ClassLoader)
:
负责加载用户路径(classpath)上的类库。 JVM 通过双亲委派模型
进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器
类加载器的双亲委派机制
?
双亲委派
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中, 只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的 Class),子类加载器才会尝试自己去加载。
NOTE:
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载 器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载 器最终得到的都是同样一个 Object 对象。