一、概述
JVM的性能优化基本上是面试过程中经常问到的问题,今天在这里做的总结。
垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(如数据库连接,网络IO等资源)
虽然java不需要开发人员显示的分配和回收内存,这对开发人员确实降低了不少编程难度,但也可能带来一些副作用:
1. 有可能不知不觉浪费了很多内存
2. JVM花费过多时间来进行内存回收
3. 内存泄露
因此,作为一名java编程人员,必须学会JVM内存管理和回收机制,这可以帮助我们在日常工作中排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序
二、JVM内存管理
根据JVM规范,JVM把内存划分成了如下几个区域:
1.方法区(Method Area)
2.堆区(Heap)
3.虚拟机栈(VM Stack)
4.本地方法栈(Native Method Stack)
5.程序计数器(Program Counter Register)
其中,方法区和堆所有线程共享。
这个JVM内存模型后,是直接内存
1.所有通过new创建的对象的内存都在堆中分配,堆大小通过-Xmx和-Xms来控制。
2. 而堆是所有线程共享的
3.栈(stack)是运行时的单位,堆(heap)是存储的单元。栈是解决程序的运行问题,即程序如何运行问题,或者说如何处理数据。堆区主要用于存放对象实例及数组,所有new出来的对象都存储在该区域。
4.堆和栈的分离的好处:使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益很多。堆中的共享常量和缓存可以被所有栈访问,节省了时间。
5.堆和栈分别存的是什么?栈(虚拟机栈,JVM Stack)因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关的信息。包括局部变量、程序运行状态、方法返回值(存储局部变量表、操作数栈、常量池引用)等等。堆中存的是对象。栈(虚拟机栈,JVM Stack)中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4byte的引用(堆栈分离的好处)。
6. 为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节---需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况---长度固定,因此栈中存储就够了,如果把它存在堆中是没有什么意义的(还会浪费空间,后面说明)。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,它们的处理方式是统一的。
2.1 方法区
方法区存放了要加载的类的信息(如类名,修饰符)、类中的静态变量、final定义的常量、类中的字段、方法信息。
和java堆一样,方法区也是属于线程共享的区域,存放的是java的类型信息、静态变量、运行时常量池以及jit编译后的代码等数据
当开发人员调用类对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是全局共享的,在一定条件下它也会被GC。当方法区使用的内存超过它允许的大小时,就会抛出OutOfMemory:PermGen Space异常。
在Hotspot虚拟机中,这块区域对应的是Permanent Generation(持久代),一般的,方法区上执行的垃圾收集是很少的,因此方法区又被称为持久代的原因之一,但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。在方法区上进行垃圾收集,条件苛刻而且相当困难,关于其回后面再介绍。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量,比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址。
JVM方法区的相关参数,最小值:--XX:PermSize;最大值 --XX:MaxPermSize
方法区是一个JVM规范,永久代和元空间都是其中一种实现的方法。
在JDK中1.8之前,方法区就是永久代。
在JDK 1.8之后,元空间是就是方法区。原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态边量和常量池等放入堆中
2.2 堆区
类的成员变量是放到堆中。
堆区是理解JavaGC机制最重要的区域。在JVM所管理的内存中,堆区是最大的一块,堆区也是JavaGC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区用来存储对象实例及数组值,可以认为java中所有通过new创建的对象都在此分配。
对于堆区大小,可以通过参数-Xms
和-Xmx
来控制,-Xms为JVM启动时申请的最新heap内存,默认为物理内存的1/64但小于1GB;-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4但小于1GB,默认当剩余堆空间小于40%时,JVM会增大Heap到-Xmx大小,可通过-XX:MinHeapFreeRadio
参数来控制这个比例;当空余堆内存大于70%时,JVM会减小Heap大小到-Xms指定大小,可通过-XX:MaxHeapFreeRatio
来指定这个比例。对于系统而言,为了避免在运行期间频繁的调整Heap大小,我们通常将-Xms和-Xmx设置成一样。
JAVA 1.8 中把持久代(永久代)去掉了!
2.3 虚拟机栈(JVM Stack)
局部变量是放到虚拟机栈
虚拟机栈占用的是操作系统内存,每个线程都对应着一个虚拟机栈,它是线程私有的,而且分配非常高效。一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)
2.4 本地方法栈(Native Method Stack)
本地方法栈用于支持native方法的执行,存储了每个native方法调用的状态。本地方法栈和虚拟机方法栈运行机制一致,它们唯一的区别就是,虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。
本地方法是指操作系统等的方法,应该是调用操作系统的方法
5 程序计数器(Program Counter Register)
程序计数器是一个比较小的内存区域,可能是CPU寄存器或者操作系统内存,其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。
为什么需要程序计数器?
1.应该cup的时间分片的切换,上下文切换,记录程序当前运行的地方。
一个线程中会有一个线程栈与之对应,因此需要一个独立的线程栈(虚拟机栈), 一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
个人理解为:一个线程中会有一个线程栈与之对应,因此需要一个独立的线程栈(虚拟机栈), 一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame)。即一个线程有一个线程栈(一个虚拟机栈),同时一个线程对于一个程序计数器。即在内存空间中有多个虚拟机栈,多个程序计数器。这就是说虚拟机栈和程序计数器于线程的生命周期有关)
直接内存
使用NIO类来使用JVM虚拟机中以外的内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
三、垃圾回收
什么是垃圾?垃圾回收回收的是什么?
如何确定“垃圾”
既然是垃圾回收机制,第一步肯定是要确定垃圾,知道了垃圾便可以进行回收。但是如何确定垃圾呢?什么是垃圾呢?
什么是“垃圾”
首先要明白什么是“垃圾”,垃圾回收机制是回收堆内存中的对象,对于栈中的对象是不需要回收机制去考虑的。在Java中堆内存中的对象是通过和栈内存中的引用相互关联,才被利用的。既然是对堆内存的回收,并且堆内存中存储的都是引用对象的实体,所以回收的就是没有被任何一个引用所关联的实体对象。
因此,“垃圾”实质上指的是java虚拟机中堆内存里没有被引用到的,永远也不会访问到的实体对象。
引用计数算法
引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减 1,当该对象的引用计数为 0 时,那么该对象就会被回收。
复制代码
String m = new String("jack");
先创建一个字符串,这时候 "jack" 有一个引用,就是 m。
然后将 m 设置为 null,这时候 "jack" 的引用次数就等于 0 了,在引用计数算法中,意味着这块内容就需要被回收了
m = null;
引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的 "Stop-The-World" 的垃圾收集机制。
看似很美好,但我们知道 JVM 的垃圾回收就是 "Stop-The-World" 的,那是什么原因导致我们最终放弃了引用计数算法呢?看下面的例子。
- 定义 2 个对象
- 相互引用
- 置空各自的声明引用
我们可以看到,最后这 2 个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为 0,通过引用计数算法,也就永远无法通知 GC 收集器回收它们。
可达性分析算法
可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
通过可达性算法,成功解决了引用计数所无法解决的问题 -“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于 GC Root。
Java 内存区域
在 Java 语言中,可作为 GC Root 的对象包括以下 4 种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
此时的 s,即为 GC Root,当 s 置空时,localParameter 对象也断掉了与 GC Root 的引用链,将被回收。
复制代码
| |
| |
| |
| |
| |
| |
|
方法区中类静态属性引用的对象
s 为 GC Root,s 置为 null,经过 GC 后,s 所指向的 properties 对象由于无法与 GC Root 建立关系被回收。
而 m 作为类的静态属性,也属于 GC Root,parameter 对象依然与 GC root 建立着连接,所以此时 parameter 对象并不会被回收。
复制代码
| |
| |
| |
| |
| |
| |
| |
| |
|
方法区中常量引用的对象
m 即为方法区中的常量引用,也为 GC Root,s 置为 null 后,final 对象也不会因没有与 GC Root 建立联系而被回收。
复制代码
| |
| |
| |
| |
| |
| |
| |
|
本地方法栈中引用的对象
任何 Native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
垃圾回收算法
在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于 Java 虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,这里我们讨论几种常见的垃圾收集算法的核心思想
标记-清除(Mark-Sweep)算法
这个是最简单的算法,也是最基础最容易实现的算法。标记清楚算法分为两个阶段:标记阶段和清除阶段。标记阶段是找出所有需要被回收的对象,并作出标记;清除阶段是回收被标记的对象所占用的空间。
所上图所示,可以简单的对内存进行回收,但是同样存在一个弊端,就是容易产生内存碎片,大量的内存碎片会无法为大对象分配足够的空间,进而导致内存利用率低。
复制(Copying)算法
该方法是针对标记清除法容易产生碎片的问题,而提出的新的思想。也比较容易理解。
首先将可用内存空间按照大小平均分为两部分,在使用时只使用其中的一部分,另一部分不使用。当那一部分满了之后,触发收集机制,将还存活的对象复制到另一块内存上面,然后把当前内存的空间一次清理掉,这样就不容易出现内存碎片了。
具体流程:如图,(1)上半部分内存使用。(2)用满后,执行算法,存活的复制到下半部分。(3)下半部分内存使用。(4)下半部分用满后,执行算法,存活的复制到上半部分。
虽然该方法简单,且不易产生碎片,但是却付出了高昂的代价——将可使用的内存空间缩减到原来的一半。而且该算法的效率跟存活对象的数目多少有很大的关系,如果存活的对象很多 那么效率就会大大降低
标记整理算法
标记整理算法(Mark-Compact)标记过程仍然与标记 — 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
标记整理算法一方面在标记 - 清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。
分代收集(Generational Collection)算法
目前大多数JVM的垃圾收集器采用的算法都是分代回收算法。它的核心思想是,根据对象存活的生命周期将内存划分为若干个不同的区域。因为每个对象的生命周期都是不一样的,有些对象是与业务相关的,比如线程、Scoket、Http请求中的Session等,生命周期就比较长;但是还有一些,如局部变量、临时变量等,这些的生命周期就会比较短。如果不根据存活时间进行区分,每次收集都扫描全部的对象的话,会花费较长的时间。而且对于长生命周期的对象而言,多次的这种遍历是没有效果的,他们仍然存在,导致效率低下。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记 - 清理或者标记 — 整理算法来进行回收。so,另一个问题来了,那内存区域到底被分为哪几块,每一块又有什么特别适合什么算法呢?
因此,分代垃圾回收机制是采用了分治的思想,进行代的划分,不同的生命周期对象放在不同代上,对于不同代采用不同的最适合它的算法进行垃圾回收。
目前大部分收集器会划分成三代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。
年轻代(新生代)
该区域主要存放生命周期较短的对象,所以每次垃圾回收中都需要回收大部分对象,因此该区域采用复制(Copying)算法,也就是说复制操作少,效率不会太低。
但是在实际中,对于新生代的空间并不是1:1的划分, 为了提高空间的利用率,一般将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,具体的划分如上图。
Java 堆(Java Heap)是 JVM 所管理的内存中最大的一块,堆又是垃圾收集器管理的主要区域,这里我们主要分析一下 Java 堆的结构。
Java 堆主要分为 2 个区域 - 年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2 个区。可能这时候大家会有疑问,为什么需要 Survivor 区,为什么 Survivor 还要分 2 个区。不着急,我们从头到尾,看看对象到底是怎么来的,而它又是怎么没的。
Eden 区
IBM 公司的专业研究表明,有将近 98% 的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。
通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
Survivor 区
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为 2 个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。
为啥需要?
不就是新生代到老年代么,直接 Eden 到 Old 不好了吗,为啥要这么复杂。想想如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。
所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
为啥需要俩?
设置两个 Survivor 区最大的好处就是解决内存碎片化。
我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有 2 个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。
这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个? 显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的最佳方案。
每次使用Eden空间和一块Servivor空间,当回收时,将Eden和Servivor中存活的对象复制到另一块Servivor空间中然后清理掉Eden和刚刚用过的Survivor空间。保证始终有一个Servivor空间是空闲的
在触发收集算法时,对于从上一个Survivor区复制来还存活的对象,将被复制到“老年代”。注意,在一块Servivor中可能同时存在从Eden和上一块Servivor中复制来的对象,但是只有从Servivor复制来的对象,可以被复制到老年代。
年老代(Old 区)
年老代一般都是生命周期较长的,或者在年轻代经历了N次垃圾及回收后仍然存活的对象,就会被放到年老代中。因此该区的特点是每次回收都只有少数对象被回收,所以一般使用的是标记整理(Mark-Compact)算法。
老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 — 整理算法。
除了上述所说,在内存担保机制下,无法安置的对象会直接进到老年代,以下几种情况也会进入老年代。
大对象
大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。这样做主要是为了避免在 Eden 区及 2 个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时,得注意了。
长期存活对象
虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加 1 岁。当年龄增加到 15 岁时,这时候就会被转移到老年代。当然,这里的 15,JVM 也支持进行特殊设置。
动态对象年龄
虚拟机并不重视要求对象年龄必须到 15 岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的总合大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你“成年”。
永久代(持久代)
它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如 Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
垃圾收集器
GC是由垃圾回收器来具体执行的,所以,在实际应用场景中我们需要根据应用情况选择合适的垃圾收集器
回收类型(什么情况下触发垃圾回收)
由于根据对象的生命周期进行了分代,所有不同区域的回收时间和方式是不一样的,主要有两种类型:Scavenge GC和Full GC。
Scavenge GC
这个是对新生代的回收方法,一般情况下,当新生代空间Eden申请失败时就会触发Scavenge GC,进行新生代的回收,执行复制算法,将存活的对象复制到Survivor区。
但是不会影响老年代,由于一般Eden区不少很大,所以Eden区的GC会频发进行。
Full GC
这个是对整个堆进行整理回收的方法,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。
有如下原因可能导致Full GC:
- 年老代(Tenured)被写满
- 持久代(Perm)被写满
- System.gc()被显示调用
- 上一次GC之后Heap的各域分配策略动态变化
强制垃圾回收的两种方式:
1。调用System类的gc()静态方法: System.gc()
2.调用Runtime的gc()实例方法:Runtime.getRuntime().gc()。
四、JVM的调优
https://blog.csdn.net/suifeng3051/article/details/48292193
https://www.i3geek.com/archives/1220
转载于:https://blog.csdn.net/yinni11/article/details/79994203
https://www.infoq.cn/article/ZOYqRI4c-BFKmUBmzmKN