1、Java虚拟机原理
所谓虚拟机,就是一台虚拟的机器。他是一款软件,用来执行一系列虚拟计算指令,大体上虚拟机可以分为系统虚拟机和程序虚拟机, 大名鼎鼎的Vmare就属于系统虚拟机,他完全是对物理计算的仿真,提供了一个可以运行完整操作系统的软件平台。程序虚拟机典型代码就是Java虚拟机,它专门为执行单个计算程序而计算,在Java虚拟机中执行的指令我们称为Java字节码指令。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。到现在引用最广泛的是HotSpot虚拟机
2、Java内存结构
- 类加载子系统:负责从文件系统或者网络加载Class信息,加载的信息存放在一块称之方法区的内存空间。
- 方法区:就是存放类的信息、常量信息、常量池信息、包括字符串字面量和数字常量等。
- Java堆:在Java虚拟机启动的时候建立Java堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放到Java堆中,堆空间是所有线程共享。
- 直接内存:JavaNio库允许Java程序直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
- 每个虚拟机线程都有一个私有栈,一个线程的Java栈在线程创建的时候被创建,Java栈保存着局部变量、方法参数、Java的方法调用、返回值等。
- 本地方法栈:最大不同为本地方法栈用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通过使用C语言写)
- 垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理
- PC(Program Couneter)寄存器:也是每个线程私有的空间, Java虚拟机会为每个线程创建PC寄存器
- 虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般先进行编译成机器码后执行。
3、Java堆
堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名。
根据垃圾回收机制的不同,Java堆有可能拥有不同的结构,最为常见的就是将整个Java堆分为新生代和老年代。其中新生代存放新生的对象或者年龄不大的对象,老年代则存放老年对象。新生代分为den区、s0区、s1区,s0和s1也被称为from和to区域,他们是两块大小相等并且可以互相角色的空间。绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。
4、Java栈
java虚拟机也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表存放了编译期可知的各种基本数据类型(8个基本数据类型)、对象引用(地址指针)、returnAddress类型。局部变量表所需的内存空间在编译期间完成分配。在运行期间不会改变局部变量表的大小。
这个区域规定了两种异常状态:
- 如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常。
- 如果虚拟机栈可以动态扩展,在扩展是无法申请到足够的内存,就会抛出OutOfMemoryError异常。
Java栈是一块线程私有的空间,一个栈一般由三部分组成:局部变量表、操作数据栈和帧数据区
- 局部变量表:用于保存函数的参数及局部变量
- 操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。
- 帧数据区:帧数据区保存着访问常量池的指针
5、本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈所发挥作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。
6、Java方法区
Java方法区和堆一样,方法区是一块所有线程共享的内存区域,他保存系统的类信息。比如类的字段、方法、常量池、静态变量、即时编译器编译后的代码等。方法区的大小决定系统可以保存多少个类。如果系统定义太多的类,导致方法区溢出。虚拟机同样会抛出内存溢出的错误。方法区可以理解为永久区,它有个别命叫Non-Heap(非堆)。
7、运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在加载后进入方法区的运行时常量池中存放。
8、直接内存(Direct Memory)
直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分区域也被频繁使用,而且也可能导致OutOfMemoryError异常。在JDK1.4中新加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
9、程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
10、执行引擎
虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般先进行编译成机器码后执行。
11、什么是虚拟机参数配置
在虚拟机运行的过程中,可以跟踪系统的运行状态,对于问题的故障排查会有一定的帮助,在虚拟机提供了一些跟踪系统状态的参数,使用给定的参数执行Java虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题。我们进行虚拟机参数配置,其实就是围绕着堆、栈、方法区、进行配置。
堆的参数配置:
-XX:+PrintGC 每次触发GC的时候打印相关日志
-XX:+UseSerialGC 串行回收,XXX:+UseG1GC 应用G1收集器,最新的收集器
-XX:+PrintGCDetails 更详细的GC日志
-Xms 堆初始值
-Xmx 堆最大可用值,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率
-Xmn 新生代堆最大可用值,一般设为整个堆的1/3到1/4左右
-XX:NewRatio 设置新生代和老年代的比例,老年代/新生代
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例,SurvivorRatio=eden/from=den/to
-XX:PermSize JVM初始分配的非堆内存 。JDK8后换成Metaspace(元空间)
-XX:MaxPermSize JVM最大允许分配的非堆内存。MaxMetaspaceSize(最大元空间)
-Xss 设置栈内存大小(设置最大调用深度),栈溢出产生于递归调用,解决办法:设置线程最大调用深度
12、JVM参数调优总结
在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标:
- GC的时间足够的小
- GC的次数足够的少
- 发生Full GC(新生代和老年代)的周期足够的长
前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。
- 针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
- 年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小
- 年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,否则也就不会有调优。我们观察一下二者大小变化有哪些影响。更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC。更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率。
- 如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。通过观察应用一段时间,在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1,但应该给年老代至少预留1/3的增长空间
13、垃圾回收机制概述
Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。
内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。
14、垃圾回收简要过程
这里必须点出一个很重要的误区:不可达的对象并不会马上就会被直接回收,而是至少要经过两次标记的过程。
第一次被标记过的对象,会检查该对象是否重写了finalize()方法。如果重写了该方法,则将其放入一个F-Query队列中,否则直接将对象加入“即将回收”集合。在第二次标记之前,F-Query队列中的所有对象会逐个执行finalize()方法,但是不保证该队列中所有对象的finalize()方法都能被执行,这是因为JVM创建一个低优先级的线程去运行此队列中的方法,很可能在没有遍历完之前,就已经被剥夺了运行的权利。那么运行finalize()方法的意义何在呢?这是对象避免自己被清理的最后手段,如果在执行finalize()方法的过程中,使得此对象重新与GC Roots引用链相连,则会在第二次标记过程中将此对象从F-Query队列中清除,避免在这次回收中被清除,恢复成了一个“正常”的对象。但显然这种好事不能无限的发生,对于曾经执行过一次finalize()的对象来说,之后如果再被标记,则不会再执行finalize()方法,只能等待被清除的命运。之后GC将对F-Queue中的对象进行第二次小规模的标记,将队列中重新与GC Roots引用链恢复连接的对象清除出“即将回收”集合。所以此集合中的内容将被回收。
15、手动GC回收
package com.zhang.jvm; public class JVMDemo05 { public static void main(String[] args) { JVMDemo05 jvmDemo05 = new JVMDemo05(); jvmDemo05 = null; System.gc(); } protected void finalize() throws Throwable { System.out.println("gc在回收对象..."); } }
16、finalize作用
Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
17、内存泄露
对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。 下面图中可以看出,里面有被引用对象和未被引用对象。未被引用对象会被垃圾回收器回收,而被引用的对象却不会。未被引用的对象当然是不再被使用的对象,因为没有对象再引用它。然而无用对象却不全是未被引用对象。其中还有被引用的。就是这种情况导致了内存泄漏。
18、如何防止内存泄露
- 特别注意一些像HashMap、ArrayList的集合对象,它们经常会引发内存泄漏。当它们被声明为static时,它们的生命周期就会和应用程序一样长。
- 特别注意事件监听和回调函数。当一个监听器在使用的时候被注册,但不再使用之后却未被反注册。
19、新生代与老年代
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。默认的新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )其中新生代 ( Young )被细分为Eden和两个Survivor区域,这两个Survivor 区域分别被命名为 from 和 to。默认的Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )
垃圾回收算法
20、引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时计数器值就加1,当引用失效时计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
- 无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样他们的引用计数永远不可能为0.而且每次加减非常浪费内存。
- 效率问题,标记和清除两个过程的效率都不高。
- 空间问题,标记清除后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续空间而不得不提前触发另一次垃圾收集活动。
执行过程如下图:
21、根搜索算法
根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。在Java语言中,可以作为GCRoots的对象包括下面几种:
- 虚拟机栈中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
22、复制算法
S0和s1将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。复制算法的缺点显而易见,可使用的内存降为原来一半。复制算法用于在新生代垃圾回收
23、标记-清除算法
标记-清除(Mark-Sweep)算法顾名思义,主要就是两个动作,一个是标记,另一个就是清除。标记就是根据特定的算法(如:引用计数算法,可达性分析算法等)标出内存中哪些对象可以回收,哪些对象还要继续用。标记指示回收,那就直接收掉;标记指示对象还能用,那就原地不动留下。缺点是 标记与清除没有连续性效率低,清除之后内存会产生大量碎片;
24、标记-压缩算法
标记压缩法在标记清除基础之上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理。老年代使用的就是标记压缩法
25、Minor GC和Full GC区别
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以Minor GC 非常频繁,一般回收速度也比较快。当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GCFull GC触发机制。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的 Minor GC。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。当年老代满时会引发Full GC,Full GC将会同时回收年轻代、老年代,当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值)来设置。
26、JVM的永久代中会发生垃圾回收么
垃圾回收会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
27、元空间
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小。
- -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整,如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
- -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
- -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
- -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
28、分代算法
这种算法根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新创建的对象被分配在新生代,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。
29、为什么老年代使用标记压缩、新生代使用复制算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记--清理”或者“标记--整理”算法来进行回收。
30、垃圾回收时的停顿现象
垃圾回收的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以更高效的执行,大部分情况下,会要求系统进行一个停顿的状态。停顿的目的是为了终止所有的应用线程,只有这样的系统才不会有新垃圾的产生。同时停顿保证了系统状态在某一个瞬间的一致性,也有利于更好的标记垃圾对象。因此在垃圾回收时,都会产生应用程序的停顿
31、什么是Java垃圾回收器
如果说收集算法是内存回收的方法论,那么垃圾收集器则是内存回收的具体实现,Java垃圾回收器是Java虚拟机(JVM)的三个重要模块(另外两个是解释器和多线程机制)之一,为应用程序提供内存的自动分配(Memory Allocation)、自动回收(Garbage Collect)功能,这两个操作都发生在Java堆上(一段内存快)。某一个时刻一个对象如果有一个以上的引用(Rreference)指向它,那么该对象就为活着的(Live)否则死亡(Dead)视为垃圾,可被垃圾回收器回收再利用。垃圾回收操作需要消耗CPU、线程、时间等资源,所以容易理解的是垃圾回收操作不是实时的发生(对象死亡马上释放),当内存消耗完或者是达到某一个指标(Threshold,使用内存占总内存的比列,比如0.75)时,触发垃圾回收操作。有一个对象死亡的例外,java.lang.Thread类型的对象即使没有引用,只要线程还在运行,就不会被回收。
32、Java堆溢出
错误原因: java.lang.OutOfMemoryError: Java heap space 堆内存溢出
解决办法:设置堆内存大小 // -Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
package com.zhang.test; import java.util.ArrayList; import java.util.List; public class Test { public static void main(String[] args) { List<Object> listObject = new ArrayList<Object>(); for (int i = 0; i < 10; i++) { System.out.println("i:" + i); Byte[] bytes = new Byte[100 * 1024 * 1024];//每次分配100M listObject.add(bytes); } System.out.println("添加成功..."); } }
33、虚拟机栈溢出
错误原因: java.lang.StackOverflowError 栈内存溢出
栈溢出产生于递归调用,循环遍历是不会的
解决办法:设置线程最大调用深度-Xss5m 设置最大调用深度
package com.zhang.test; public class JvmDemo04 { private static int count; public static void count() { try { count++; count(); } catch (Throwable e) { System.out.println("最大深度:" + count); e.printStackTrace(); } } public static void main(String[] args) { count(); } }
34、串行回收器(Serial Collector)
单线程执行回收操作,回收期间暂停所有应用线程的执行,client模式下的默认回收器,通过-XX:+UseSerialGC命令行可选项强制指定。
新生代的回收算法(Minor Collection):复制算法
老年代的回收算法(Full Collection):标记-压缩算法
35、并行回收器(ParNew回收器)
并行回收器在串行回收器基础上做了改进,他可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机而言,可以有效的缩短垃圾回收的时间。ParNew回收器是一个工作在新生代的垃圾收集器,回收策略和算法和串行回收器一样。ParNew回收器工作时的线程数量可以使用XX:ParaleiGCThreads参数指定,一般最好和计算机的CPU相当,避免过多的线程影响性能。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
a:并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
b:并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。
36、并行回收集器(ParallelGC)
老年代ParallelOldGC回收器也是一种多线程的回收器和新生代的ParallelGC回收器一样,也是一种关注吞吐量的回收器,他使用了标记-压缩算法进行实现,-XX:+UseParallelOldGC 进行设置 XX:+ParallelCThread也可以设置垃圾收集时的线程教量。
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。可以同时并行多个垃圾收集线程,但此时用户线程必须停止。
-XX:+UseParNewGC:设置年轻代为多线程收集。可与CMS收集同时使用。在serial基础上实现的多线程收集器。
37、CMS收集器(Concurrent Mark Sweep)
是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记--清除”算法实现的, 他的运作过程相对于前几种收集器来说更复杂一些,整个过程分为4个步骤:
- 初始标记(CMS inital mark):需要“stop the world”,但只标记一下GC Roots能直接关联的对象,速度很快。
- 并发标记(CMS concurrent mark):是GC Roots Tracing的过程,花费时间长
- 重新标记(CMS remark):是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 并发清除(CMS concurrent sweep):是并发清除无用对象。
38、G1回收器
G1回收器(Garbage-First)是在]dk1.7中提出的垃圾回收器,从长期目标来看是为了取代CMS回收器,G1回收器拥有独特的垃圾回收策略,G1属于分代垃圾回收器,区分新生代和老年代,依然有eden和from/to区,它并不要求整个eden区或者新生代、老年代的空间都连续,它使用了分区算法。
G1收集器(Garbage-First):是当今收集器技术发展的最前沿的成果之一,G1是一款面向服务器端应用的垃圾收集器。与其他GC收集器相比,G1具备如下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能够独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记--整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。这个特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前出发下一次GC。
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时java(RTSJ)的垃圾收集器的特性了。
使用G1收集器时,java堆的内存布局就与其他收集器有很大差别,它将整个java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代与老年代的概念,但新生代与老年代不再试物理隔离的了,他们都是一部分Region(不需要连续)的集合。如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下几个步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
39、GC日志
通过以下两段典型的GC日志进行分析:
33.125:[GC [DefNew:3324K->152K(3712K),0.0025925secs] 3324K->152K(11904K),0.0031680 secs] 100.667:[Full GC [Tenured:0K->210K(10240K),0.0149142secs] 4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs] <br>[Times:user=0.01 sys=0.00,real=0.02 secs]
-
最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
-
GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的,例如下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC(System)”。
[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]
-
接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。GC发生区域日志与GC收集器对照列表如下:
GC日志区域名 | 对应GC收集器名 |
[DefNew (Default New Generation) | Serial收集器 |
[ParNew (Parallel New Generation) | ParNew收集器 |
[PSYoungGen | Parallel Scavenge收集器 |
[ParOldGen | Parallel Old收集器 |
后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。 而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)"。再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。有的收集器会给出更具体的时间数据,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。
CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。
40、GC收集器参数
1、 Serial串行收集器相关的参数
-XX:+UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
-XX:SurvivorRatio | 新生代中设置eden区大小和survivior区大小的比例。默认为8,代表Eden:Survivor =8:1 |
-XX:PretenureSizeThreshold | 设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄增加1,当超过这个参数值时就进入老年代 |
注意:当GC发生在新生代时,称为Minor GC次收集;当GC发生在年老代时,称为Major GC主收集。 一般的Minor GC的发生频率要比Major GC高很多。
2、 ParNew并行收集器相关的参数
-XX:+UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收 |
-XX:+UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS Mark Sweep)的收集器组合进行内存回收 |
-XX:+UseParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
-XX:ParallelGCThreads | 设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等,但在CPU数量比较多的情况下,设置相·对较小的数值也是合理的。 |
-XX:MaxGCPauseMills | 设置最大垃圾收集停顿时间。它的值是一个大于0的正数。收集器在工作时,会调整java堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMills以内。 仅在Parallel Scavenge收集器时生效 |
-XX:GCTimeRatio | 设置吞吐量大小,即GC时间占总时间的比率。它的值是一个0到100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。默认值为99,即允许1%的GC时间,仅在Parallel Scavenge收集器时生效 |
-XX:+UseAdaptiveSizePolicy | 打开自适应GC策略。在这种模式下,新生代的大小、eden和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。 |
-XX:HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
3、 CMS 收集器相关的参数
-XX:+UseConcMarkSweepGC | 打开此开关后,使用ParNew + CMS —— Serial Old的收集器组合进行内存回收。Serial Old收集器是CMS收集器出现Concurrent Mode Failure失败后的备用收集器 |
-XX:ParallelCMSThreads | 设定CMS的线程数量。 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发,默认为68%。仅在CMS收集器时生效 |
-XX:+UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。 仅在CMS收集器时生效 |
-XX:CMSFullGCsBeforeCompaction | 设定进行多少次CMS垃圾回收后,进行一次内存压缩(碎片整理)。 仅在CMS收集器时生效 |
-XX:+CMSClassUnloadingEnabled | 允许对类元数据区进行回收。 |
-XX:CMSInitiatingPermOccupancyFraction | 当永久区占用率达到这一百分比时,启动CMS回收(前提是-XX:+CMSClassUnloadingEnabled激活了)。 |
-XX:CMSInitiatingPermOccupancyOnly | 表示只在到达阈值的时候才进行CMS回收。 |
-XX:+CMSIncrementalMode | 使用增量模式,比较适合单CPU。增量模式在JDK8中标记为废弃,并且将在JDK9中彻底移除。 |
4、 G1收集器相关的参数
-XX:+UseG1GC | 使用G1回收器。 |
-XX:MaxGCPauseMillis | 设置最大垃圾收集停顿时间。 |
-XX:GCPauseIntervalMillis | 设置停顿间隔时间。 |