概念
Java是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要JVM进行一番转换。
从图中可以看到,有了JVM这个抽象层之后,Java就可以实现跨平台了。JVM只需要保证能够正确执行.class文件,就可以运行在诸如Linux、Windows、MacOS等平台上了。
内存模型
五大区
JVM的内存结构包括五大区域:
程序计数器
(线程私有,无GC):指向当前线程正在执行的字节码的地址、行号。虚拟机栈
(线程私有,无GC):存储当前线程运行方法所需要的局部变量、操作栈等。每一个方法都对应一个栈帧,可以通过配置Xss来配置栈帧的大小本地方法栈
(线程私有,无GC):和虚拟机栈类似,不同的是,本地方法栈存储的是本地方法的数据。方法区
(线程共享,要GC):被所有方法线程共享的一块内存区域。用于存储已经被JVM加载的类信息、常量、静态变量等。这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载。通过永久代(Java8之前)或者元空间来实现。堆区
(线程共享,要GC):被所有线程共享的一块内存区域,在虚拟机启动的时候创建,用于存放对象实例。一个JVM实例只有一个堆内存。
总结:
程序计数器、虚拟机栈、本地方法栈
3
个区域随线程而生、随线程而灭
,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。- 堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分
Heap(堆内存)
参考资料:JVM系列(二) Java 堆内存分析_java堆内存比例-优快云博客
堆(Heap)是GC执行垃圾回收的重点区域,如果出现OOM或者出现内存泄露,一定是出在堆内存上,因为堆是JVM中最大的一块内存空间,所有线程共享Java堆,物理上不连续的逻辑上连续的内存空间,几乎所有的实例都在这里分配内存,在方法结束后,堆中的对象不会马上删除,仅仅在垃圾收集的时候被删除
。
Java7及以前将JVM空间逻辑上分成三部分::
- 年轻代(YoungGen);年轻代分为Eden(生成区)和Survivor(幸存区),大小比例默认为8:2;其中Survivor又分为s0(From Space)和s1(To Space)。这两个空间大小是一模一样的,就是一对双胞胎,他俩是1:1的比例;
- 老年代(OldGen);较大的对象数据,年轻代存不下会放入老年代;存活很久,没有被清除掉的对象也会放入老年代。
- 永久代(PermGen);也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
Java8废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现。他们最大区别是:元空间并不在JVM中,而是使用本地内存。
OOM(内存溢出)
第一种:OutOfMemoryError: Java heap space
发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap space有关。解决这类问题有两种思路:
1、检查程序,看是否有死循环或不必要地重复创建大量对象。找到原因后,修改程序和算法。
2、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。如:set JAVA_OPTS= -Xms4G –Xmx4G
第二种: java.lang.OutOfMemoryError: Metaspace
发生这种问题的原意是程序中使用了大量的jar或class,使java虚拟机装载类的空间不够,与Metaspace有关。解决这类问题有以下两种办法:
1、增加java虚拟机中的XX:MetaspaceSize和XX:MaxMetaspaceSize参数的大小,其中XX:MetaspaceSize是初始元数据区域大小,XX:MaxMetaspaceSize是最大元数据区域大小。
2、JAVA_OPTS=" -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m"
第三种:OutOfMemoryError:unable to create new nativethread
一般由于两个原因导致的:
1)内存空间不足以满足创建线程所需的stack size
virtual memory < stack size*the number of threads
2)线程数已达到操作系统的上限
内存分析工具
- MAT(Memory Analyzer Tool)
- File-Tool
JVM参数
参考资料:面试必问之JVM常用参数_maxheapsize-优快云博客
参数 | 描述 |
-Xms (-XX:InitialHeapSize) | 堆内存初始大小,默认为物理内存1/64 |
-Xmx (-XX:MaxHeapSize) | 堆的最大分配内存,默认为物理内存1/4。 在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。 |
-Xss (-XX:ThreadStackSize) | 规定了每个线程虚拟机栈的大小,一般情况下,256k是足够的,此配置将会影响此进程中并发线程数的大小。 |
-XX:MetaspaceSize | 方法区(元空间)内存大小,一般初始化200m,最大1024m就够了 |
-XX:MaxMetaspaceSize | 方法区(元空间)内存最大允许大小。元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制。 |
-XX:NewSize(-Xns) | 年轻代内存初始大小 |
-XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小 |
-XX:SurvivorRatio | 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1:1 |
-XX:NewRatio | 设置年轻代和年老代的比值。默认值为2,表示年轻代和年老代比值为1 : 2 |
-XX:+PrintGC | jvm启动后,只要遇到GC就会打印日志 |
-XX:+PrintGCDetails | 输出详细Gc收集日志信息 |
-XX:+DisableExplicitGC | 关闭System.gc() |
-XX:+CollectGen0First | FullGC时是否先YGC,默认false |
-XX:MaxTenuringThreshold | 设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代。 |
-XX:PretenureSizeThreshold | 当创建的对象超过指定大小时,直接把对象分配在老年代。 |
GC (Garbage Collection)
参考资料:Java GC、Full GC_java full gc-优快云博客
概念
GC(Garbage Collection)是JVM自动管理内存的一项重要功能,它负责在运行时自动回收不再被使用的对象,并释放它们占用的内存空间。Java的GC系统通过以下几个步骤来执行垃圾回收:
- 标记(Mark):GC系统首先标记所有活跃对象,即那些仍然被引用的对象。它从根对象(如线程栈、静态变量)开始遍历对象图,并将活跃对象进行标记。
- 清除(Sweep):在标记阶段之后,GC系统会清除所有未标记的对象,即那些不再被引用的对象。清除的对象会被认为是垃圾,其占用的内存将被释放。
- 压缩(Compact):在清除阶段之后,GC系统可能会进行内存压缩操作。内存压缩的目的是为了消除内存碎片化,将存活对象紧凑地排列在一起,以便为新对象分配连续的内存空间。
GC系统采用了不同的垃圾回收算法和垃圾收集器,可以根据应用程序的需求和硬件环境选择适合的组合。常见的垃圾回收算法有标记-清除算法、复制算法、标记-整理算法等。
GC回收对象过程
第一步,新生成的对象首先放到Eden区,当Eden区满了会触发Minor GC。
第二步,第一步GC活下来的对象,会被移动到survivor区中的S0区,S0区满了之后会触发Minor GC,S0区存活下来的对象会被移动到S1区,S0区空闲。
S1满了之后在GC,存活下来的再次移动到S0区,S1区空闲,这样反反复复GC,每GC一次,对象的年龄就涨一岁,达到某个值后(15),就会进入老年代。
第三步,在发生一次Minor GC后(前提条件),老年代可能会出现Major GC,这个视垃圾回收器而定。
Young GC(Minor GC)
Young GC又称Minor GC,对新生代进行GC。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。Young GC同样会会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
Full GC(Major GC)
Full GC(Full Garbage Collection)是Java虚拟机对整个堆内存进行垃圾回收的过程,包括新生代和老年代的垃圾回收。Full GC的速度一般会比Young GC慢10倍以上,STW的时间更长,如果Full GC 后,内存还不足,就报OOM了。
触发Full GC 执行的情况有以下几种:
- 老年代空间不足:当老年代中的对象占用的空间超过了老年代的阈值时,会触发Full GC来回收老年代中的垃圾对象。
- 方法区(永久代)空间不足:在Java7之前的版本中,永久代(PermGen)用于存储类信息和常量池等数据。如果永久代空间不足,也会触发Full GC来回收永久代中的垃圾对象。
- 手动调用:通过调用System.gc()方法或者使用垃圾收集器的API触发Full GC。
- 垃圾收集器策略变更:在切换垃圾收集器或者调整垃圾收集器的参数时,可能会触发Full GC来完成垃圾收集器的切换或者参数的调整。
- 内存分配失败:当Java虚拟机尝试分配新对象时,但堆内存已经无法分配足够的空间时,可能会触发Full GC。
- JVM启动和关闭阶段:在JVM启动和关闭过程中,可能会执行Full GC来清理未完成的垃圾回收任务。
请注意,Full GC的发生是Java虚拟机自动管理内存的一部分,开发人员无法直接控制或干预
。优化应用程序的内存使用和垃圾回收策略,可以减少Full GC的频率和影响。
GC优化措施
优化GC的目标是减少GC的频率和时间,以提高应用程序的性能和稳定性。下面是一些优化GC的常见策略:
- 堆内存调优:通过适当调整堆内存大小来减少Full GC的频率。可以根据应用程序的内存需求和垃圾回收的特点,调整-Xms(初始堆大小)和-Xmx(最大堆大小)参数。
- 避免过度创建对象:频繁创建大量临时对象会增加Full GC的负担。可以通过使用对象池、复用对象、使用StringBuilder等手段来减少对象的创建和销毁。
- 注意对象引用的生命周期:及时释放不再使用的对象引用,帮助垃圾回收器判断对象是否可回收。尤其是在长时间运行的任务中,需要特别注意避免意外持有对象的引用,导致内存无法释放。
- 使用并行GC或G1收集器:Parallel GC和G1收集器在Full GC的性能方面有较好的表现。可以根据应用程序的需求和硬件环境选择合适的垃圾收集器。
- 减少Full GC的时间窗口:
Full GC
通常会导致应用程序的停顿,影响用户体验
。可以通过调整Full GC的时间窗口,将Full GC的发生时间尽量安排在业务低峰期。 - 监控和调优:通过监控GC日志、堆内存使用情况等指标,定位Full GC的原因和性能瓶颈,进行有针对性的调优。
- 使用内存分析工具:使用内存分析工具(如VisualVM、MAT等)来分析内存使用情况、对象引用关系等,帮助定位内存泄漏或过度使用的问题。
需要根据具体的应用程序和环境来选择合适的优化策略。对于复杂的应用程序,可能需要结合性能测试和调优实验,不断优化Full GC的配置和策略,以达到最佳性能和稳定性。
GC分类
参考资料:https://zhuanlan.zhihu.com/p/259740590
常见的七种经典垃圾收集器,按作用于不同分代分类:
新生代:Serial,ParNew,Parallel Scavenge;
老年代:Serial Old,Parallel Old,CMS;
新生代+老年代:G1。
上图中,新生代和老年代区域的回收器之间进行连线,说明他们之间可以搭配使用。
GC回收策略
常见内存回收策略可以从以下几个维度来理解:
- 串行 & 并行
串行(Serial):单线程执行内存回收工作。十分简单,无需考虑同步等问题,但耗时较长,不适合多cpu。
并行(Parallel):多线程并发进行回收工作。适合多CPU,效率高。
- 并发 & stop the world
stop the world:jvm里的应用线程会挂起,只有垃圾回收线程在工作进行垃圾清理工作。简单,无需考虑回收不干净等问题。
并发(仅CMS支持):在垃圾回收的同时,应用也在跑。保证应用的响应时间。会存在回收不干净需要二次回收的情况。
- 压缩&非压缩
压缩:在进行垃圾回收后,会通过滑动,把存活对象滑动到连续的空间里,清理碎片,保证剩余的空间是连续的。
非压缩:保留碎片,不进行压缩。
copy:将存活对象移到新空间,老空间全部释放(需要较大的内存)。
一个垃圾回收算法,可以从上面几个维度来考虑和设计,而最终产生拥有不同特性适合不同场景的垃圾回收器。
查看默认GC
在要查询的机器的命令行中输入以下命令:
java -XX:+PrintCommandLineFlags -version
得到的结果大抵如下所示:
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
上面的-XX:+UseParallelGC ,表示的正是目前在使用的是Parallel垃圾收集器,如果-XX:+UseCompressedOops后面就没有输出结果了,则是Serial垃圾收集器。
如果默认是Serial垃圾收集器, jvm所在的机器的配置非常低。jdk并没有默认不变的垃圾收集器,jdk会根据所在的机器的环境,自动适配比较适合的垃圾收集器。
Serial 收集器
- 针对区域:新生代。
- 原理:使用标记复制算法。
- 优点:简单、高效(相对于其他单线程),额外内存消耗最小, 总体说来 Serial收集器对于运行在客户端模式(例如swing构建的应用)下的虚拟机来说是一个很好的选择。
- 缺点:垃圾收集停顿时间长。
Serial收集器是最基础、历史最悠久的收集器。迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器。
ParNew 收集器
- 针对区域:新生代。
- 原理:Serial收集器的多线程并行版本,行为与Serial一致,使用标记复制算法,同时使用多条垃圾收集线程进行垃圾收集。
- 优点:除了Serial收集器外,只有它能与老年代收集器CMS收集器配合工作, 新生代收集器选用ParNew,老年代选用CMS是自jdk5以来到jdk8,官方一直推荐的服务器端的收集器最佳拍档。
- 缺点:自JDK 9开始,ParNew加CMS收集器的组合就不再是官方 推荐的服务端模式下的收集器解决方案了,ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。
Parallel Scavenge 收集器
- 针对区域:新生代。
- 原理:是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
- 优点:可控制的吞吐量,高吞吐量,高效利用 CPU,垃圾收集的自适应的调节策略。
- 缺点:自JDK 9开始,ParNew加CMS收集器的组合就不再是官方 推荐的服务端模式下的收集器解决方案了,ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。
Serial Old 收集器
- 针对区域:老年代。
- 原理:使用标记-整理算法,它是Serial收集器的老年代版本,它同样是一个单线程收集器
- 用途:这个收集器的主要意义也是供客户端模式使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用; 另外一种就是作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old 收集器
- 针对区域:老年代。
- 原理:使用标记-整理算法,它是Parallel Scavenge收集器的老年代版本,支持多线程并发收集。
- 用途:在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
- 针对区域:老年代。
- 使用:在使用参数-XX:+UseConcMarkSweepGC后,老年代启用CMS收集器, 而对应就会默认新生代收集器为ParNew。
- 原理:基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,分别是 1.初始标记, 2.并发标记, 3.重新标记, 4.并发清除。
- 其中1.初始标记和 3.重新标记,这两个步骤仍然需要STW,但在整个过程中耗时最长的2.并发标记和4.并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说:CMS收集器的内存回收过程是与用户线程一起并发执行的。
G1 收集器
Garbage First(简称G1)是一款在server端运行的垃圾收集器,专门针对于拥有多核处理器和大内存的机器,在JDK9中更被指定为官方GC收集器。它满足高吞吐量的同时满足GC停顿的时间尽可能短, 到了JDK 8 Update 40的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。这个版本以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
在G1收集器出现之前的所有 其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老 年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任 何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而 是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。