资料名称 | 来源地址 |
---|---|
《深入理解JVM&G1 GC》 | 图书 |
《深入理解Java虚拟机》 | 图书 |
JAVA 8官方文档 | https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ |
Java一个特点就是引入了垃圾回收机制,Java垃圾回收是指JVM自动对内存中无用对象所占用空间的释放,使Java程序员在编写程序的时候不再需要考虑内存管理,可以有效的防止内存泄露。
JVM运行过程中会为对象分配内存空间,当对象不会再被使用后,垃圾收集器需要对其所占用空间进行回收,否则迟早会导致内存占用越来越大直到内存溢出,程序就会无法运行。就比如家里的垃圾一直不扔,迟早会塞满整个家,人也没有空间在家里生活下去。
垃圾回收主要针对堆和方法区的对象进行回收。线程私有的程序计数器、虚拟机栈和本地方法栈在线程结束之后就被清除,不参与垃圾回收。
为对象添加一个引用计数器,每当有一个地方引用它时,计数器值加一;当引用失效时,计数器减一;任何时刻当计数器值为0时说明该对象不可能再被使用,可以被垃圾回收。
如下代码,当存在两个对象之间的循环引用时,两者的引用计数永远不为0,导致无法对它们进行回收。
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
dosomething();
}
}
因为循环引用问题难以处理,JAVA虚拟机没有选用引用计数法来管理内存。
在主流的商用语言中如Java、C#的主流实现中都是通过可达性分析来判断对象是存活。这个算法的基本思路就是通过一系列的GC Roots对象作为起点,从这些节点开始向下搜索引用的对象,走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的,所以会被判定为是可回收的对象。
在Java中,GC Roots包含以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
在可达性分析中不可达的对象,也不一定会被回收,这里涉及到finalize方法的知识:
finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法,如果对象覆盖了该方法并且在该方法中将自身重新被引用链中任意对象引用, 就能完成自救。
finalize()方法最多只能被GC执行一次,第二次被标记回收时不会再执行该方法
finalize()方法不被推荐使用,它能做的所有工作,try-finally都能做得更好更及时
该算法首先标记出需要回收的对象,标记完成后统一回收掉所有的被标记对象
缺点:
效率问题:标记和清除的效率不高
空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程中产生大量不连续的内存碎片。空间碎片太多可能会导致以后在程序运行中需要分配大对象时因为无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
该算法的标记过程与标记-整理算法一致,但不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:不会产生内存碎片
缺点:需要移动大量对象,处理效率较低
复制算法将可用内存分为大小相等的两块,每次只使用其中的一块,当这块内存用完了就将还存活的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。
优点:不存在内存碎片问题,实现简单,运行高效。
缺点:内存缩小为了原来的一半。如果对象存活率高就要进行较多的复制操作,效率会变低。
回收前状态:
回收后状态:
当前的商业虚拟机垃圾收集都使用分代收集算法。该算法思想是根据对象存活周期的不同将内存划分为几块,一般是分为年轻代和老年代,然后根据不同年代的特点采用最合适的收集算法。
新生代:每次垃圾收集都有大批对象死亡,少量存活,适合选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代:对象存活率高,适合使用标记-清理或标记-整理来进行回收。
在JDK1.8版本之前,JAVA堆被划分为年轻代、老年代和永久代,其中年轻代又被划分为eden、s1、s2。
在 JDK 1.8 中, HotSpot 已经没有 永久代这个区间了,取而代之的是 Metaspace(元空间)。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数设置来指定元空间的大小。
Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于
Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
#### Full GC的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
垃圾收集器是内存回收的具体实现。不同厂商、不同版本的虚拟机所提供的垃圾收集器可能有很大差别,一般来说会提供参数供用户根据自己的应用特点组合出各个年代所使用的收集器。
如下分为JDK1.7Update14之后HotSpot虚拟机的7种作用于不同分代的收集器,两个收集器存在连线则说明它们可以搭配使用:
Serial收集器是最基本、发展历史最悠久的收集器。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,这个停顿被称为“Stop The World”。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
启用配置:-XX:+UseSerialGC
ParNew收集器是Serial收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。CMS收集器的介绍后面会进行说明。
与 ParNew 一样是多线程收集器。
CMS等收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数-XX:+UseAdaptiveSizePolicy打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记-清除算法。
分为以下四个流程:
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
相关配置参数:
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
具备如下特点:
Java5以上版本JVM能够根据硬件配置等数据自动判断使用Server模式还是Client模式。
cilent模式与server模式的区别
1)编译器方面:
当虚拟机运行在client模式时,使用的是一个代号为c1的轻量级编译器,而server模式启动时,虚拟机采用的是相对重量级,代号为c2的编译器;c2编译器比c1编译器编译的相对彻底,服务起来之后,性能更高。
2)gc方面:
cilent模式下的新生代(Serial收集器)和老年代(Serial Old)选择的是串行gc
server模式下的新生代选择并行回收gc,老年代选择并行gc
3)启动方面:
client模式启动快,编译快,内存占用少,针对桌面应用程序设计,优化客户端环境的启动时间
server模式启动慢,编译更完全,编译器是自适应编译器,效率高,针对服务端应用设计,优化服务器环境的最大化程序执行速度
使用命令java –version可以查看当前JVM默认工作在哪个模式
原文:https://www.cnblogs.com/gongshiyun/p/12528253.html