-
详解JVM
Java虚拟机的作用
JVM(Java Virtual Machine),Java虚拟机
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。
在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。
Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。
Java对象的创建过程及类加载的执行过程
见文章:Java对象的创建过程
双亲委派模型
类加载器通过SPI打破双亲委派加载机制
从JDK1.2开始,类加载过程采用了父类委托机制,类的加载首先请求父类加载器,父类加载器无能为力时才由子类加载器自行加载。保证了使用不同的类加载器最终得到的都是同一个Object对象。
双亲委派模型打破:
第一次打破出现在双亲委派模型出现之前,兼容已有的ClassLoader。
第二次自身缺陷导致:JNDI服务,代码由启动类加载器来完成加载,需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口。 设计了线程上下文类加载器(Thread Context ClassLoader)
这个类加载器通过setContextLoader()方法进行设置,如果创建线程还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
第三次被破坏:用户对程序动态性的追求而导致的
JVM的主要组成部分及其作用
类加载器Class Loader
类加载器:Java虚拟机设计团队有意把类加载阶段中的"通过类的全限定名获取该类的二进制字节流。"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。
实现这个动作的代码被称为"类加载器"
Class Loader只管加载,只要符合文件结构就加载,至于能否运行,它不负责,由Exectution Engine负责的。
常用Java类加载器
根加载器(BootStrap)
一般用本地代码实现,负责加载JVM核心类库
负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可的类。
扩展加载器(Extension)
加载JAVA_HOME\lib\ext目录中的,或通过java.ext.dirs系统属性所指定的目录中加载的类库,父加载器是BootStrap。
应用类加载器(System ClassLoader)
父类是Extension,是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,是用户自定义加载器的默认父加载器。
用户自定义类加载器
执行引擎Execution Engine
执行引擎也叫解释器,负责解释命令,交由操作系统执行。
本地库接口Native Interface
本地接口的作用是融合不同的语言为java所用。
运行时数据区(重点)
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
程序计数器
程序计数器是当前线程所执行的字节码的行号指示器
内存空间小,线程私有。字节码解释器工作是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是本地(native)方法,这个计数器的值应为空。
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。
虚拟机栈
虚拟机栈描述的是Java方法执行的线程内存模型。
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译器可知的各种Java虚拟机基本数据类型、对象引用(reference类型,它并不等同于对象本身)和returnAddress类型(指向了一条字节码指令的地址)
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
本地方法栈
为虚拟机使用到的native方法服务。
堆
存放对象实例和数组,JVM所管理的内存中最大的一块,被所有线程共享。
几乎所有的对象实例都在这里分配内存。
Java堆也是垃圾收集器管理的内存区域。
堆的区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分。----《深入理解Java虚拟机》(由于现代VM采用分代收集算法,因此Java堆从GC的角度还可以细分为:新生代(Eden区、From Survivor区和To Survivor区)和老年代。)
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机会抛出OutOfMemoryError异常。
方法区
存储已被虚拟机加载的类信息、常量、静态变量(Class和Meta(元数据))、即时编译器编译后的代码等数据,是内存的永久保存区域。
方法区并不等于永久代,仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已。
这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。
到了JDK8,完全废弃了永久代的概念,改用与JRockit,J9一样在本地内存中实现的元空间来代替,把JDK7中永久代还剩余的内存全部移到元空间中。
如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
JVM的作用
首先通过类加载器ClassLoader把Java代码转换成字节码,运行时数据区再把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
四种引用类型
强引用
一个对象赋值给一个引用就是强引用。
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。
如果想中断强引用与对象之间的联系,可以显示地将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。
软引用
用SoftReference类实现,一般不会轻易回收,只有内存不够才会回收。如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
弱引用
用WeekReference类实现,一旦垃圾回收已启动,就会回收。
虚引用
用PhantomReference类实现,不能单独存在,必须和引用队列联合使用,主要作用是跟踪对象被回收的状态。(持有虚引用就和没有引用一样,在任何时候都可能被回收)
垃圾收集器与内存分配策略
为什么要了解垃圾收集和内存分配:当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更好并发量的瓶颈时,我们就必须对这些"自动化"的技术实施必要的监控和调节。
对象存活检测算法
引用计数器算法
一个对象如何没有任何与之关联的引用,即他们的引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值+1,当应用失效时-1,任何时刻计数器为0的对象不可能再被使用
问题:很难解决对象之间相互循环引用的问题。在Java领域,主流的Java虚拟机里面都没有选用引用计数法来管理内存,Java虚拟机不是用引用计数算法来判断对象是否存活。
可达性分析法
通过一系列GC Roots对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC roots没有任何引用链相连时,则证明此对象不可用,即不可达GC roots。
注意:不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
可作为GC Roots的对象
-
虚拟机栈(栈帧中的本地变量表)中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI(Native方法)引用的对象
-
JAVA虚拟机内部的引用
-
所有被同步锁(synchronized关键字)持有的对象
-
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
Java回收算法
分代收集算法
分代收集理论建立在两个分代假说之上:
-
弱分代假说:绝大多数对象都是朝生夕灭的
-
强分代假说:熬过越多次垃圾收集的对象就越难消亡
这两个分代假说共同奠定了多款常用的垃圾回收器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储。
故至少会把Java堆划分为新生代和老生代两个区域。
分代收集并非只是简单划分一下内存区域那么容易,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用。由此需要对分代收集理论添加第三条经验法则:
-
跨代引用相对于同代引用来说仅占极少数。
根据这条假说,我们不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需要在新生代上建立一个全局的数据结构,标识出老年代的哪一块内存会存在跨代引用。
此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
名词解释:
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
老年代收集(Major GC/Old gc):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
标记清除算法
标记清除算法分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
算法存在两个问题
- 执行效率不稳定,如果Java堆中包含大量对象,而且大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量的增长而降低。
- 内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
复制算法
复制算法为解决标记-清除算法面对大量可回收对象时执行效率低的问题。
复制算法按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
算法最大的问题在于可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。
现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。
优化的半区复制分代策略:Appel式回收(HotSpot虚拟机的Serial,ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局)
把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也即每次新生代中可用内存空间为整个新生代容量的90%,只有一个Survivor空间,即10%的新生代是会被"浪费"的。
Appel式回收还有一个充当罕见情况的"逃生门"的安全设计,当Survivor空间不足以容纳一次MinorGC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。
如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的。
标记整理算法
标记整理算法,标记阶段和标记清除算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清理掉边界以外的内存。
标记清除算法与标记整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。
如果移动对象存活,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。像这种停顿被称为Stop The World。
如果不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决,假如在这个环节上增加了额外的负担,势必会直接影响应用程序的吞吐量。
HotSpot虚拟机的Parallel Old收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。
GC垃圾收集器
新生代GC垃圾收集器
Serial垃圾收集器(单线程,复制算法)
Serial是最基础,历史最悠久的收集器。
Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,更重要的是在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。
Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,
因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
ParNew垃圾收集器(Serial+多线程)
Serial收集器的多线程版本。
ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
ParNew是很多java虚拟机运行在 Server 模式下新生代的默认垃圾收集器,同时除了Serial收集器外,目前只有它能与CMS收集器配合工作,是激活CMS后的默认新生代收集器。
Parallel Scavenge收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器。
它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),
高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
老年代GC垃圾收集器
Serial Old 收集器(单线程,标记整理算法)
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法。
这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。
在 Server 模式下,主要有两个用途:
- 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
- 作为使用 CMS 收集器发生失败时的后备垃圾收集方案。
Parallel Old 收集器(多线程标记整理算法)
Parallel Old 收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,
Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
CMS垃圾收集器(多线程标记清除算法)
CMS:Concurrent Mark Sweep
CMS是一款并发,使用标记-清除算法的GC。是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器。它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS是针对老年代进行回收的GC,以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。
最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
—XX:+UseConcMarkSweepGC 来指定使用CMS垃圾回收器
CMS垃圾收集器执行过程:
-
初始标记
标记GC Roots能直接关联到的对象,速度很快。 -
并发标记
从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。 -
重新标记
为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记稍微长一点,但也远比并发标记阶段的时间短。 -
并发清除
清理删除掉标记阶段的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
其中初始标记、重新标记这两个步骤仍然需要Stop the World
CMS垃圾收集器的缺点:
-
对处理器资源非常敏感,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。
同时,CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不少于25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。
但是当处理器核心数量不足4个时,CMS对用户程序的影响就可能变得很大。 -
无法处理浮动垃圾,有可能出现Concurrent Mode Failure 失败而导致另一次完全 Stop The World 的 Full GC的产生。
浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程还是在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好等待下一次垃圾收集时再清理掉。 -
标记清除算法会产生大量空间碎片,出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前出发一次Full GC的情况。
-XX:+UseCMSCompactAtFullCollection 用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程
-XX:CMSFullGCsBeforeCompaction 要求CMS收集器在执行若干次不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认为0,表示每次进入Full GC时都进行碎片整理)。
以上参数从JDK 9开始废弃。
CMS垃圾收集流程
1.初始标记:这是CMS中两次STW事件中的第一次。这一步的作用是标记存活的对象,有两部分:
标记老年代中所有的GC Roots对象
标记年轻代中活着的对象引用到的老年代的对象
在Java语言里,可作为GC Roots对象的包括如下几种:
虚拟机栈(栈帧中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI的引用的对象
为了加快次阶段处理速度,减少停顿时间,可以开启初始标记并行化,XX:+CMSParallelInitialMarkEnabled ,同时调大并行标记的线程数,线程数不要超过CPU的核数。
2.并发标记
从“初始标记”阶段标记的对象开始找出所有存活的对象
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象,或者更新老年代对象的引用关系等等。对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。由于这个阶段是和用户线程并发的,可能会导致concurrent mode failure。
注:发生concurrent mode failure会引起Full GC,这种情况下会使用Serial Old收集器,是单线程的,对GC的影响很大。concurrent mode failure产生的原因是老年代剩余的空间不够,导致了和gc线程并发执行的用户线程创建的大对象(由PretenureSizeThreshold控制新生代直接晋升老年代的对象size阀值)不能进入到老年代,只要stop the world来暂停用户线程,执行GC清理,单线程对全堆以及 metaspace 进行回收,STW 的时间会特别长,对业务系统的可用性影响比较大。可以通过设置CMSInitiatingOccupancyFraction预留合适的CMS执行时剩余的空间
3.预清理阶段
前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Dirty的Card。
4.可终止的预处理
这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖很多的因素,因为这个阶段是重复的做相同的事情直到发生abort的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
注:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻代的引用,使得下个阶段的重新标记阶段,扫描年轻代指向老年代的引用的时间减少。
5.重新标记
这个阶段会导致第二次STW,该阶段的任务是完成标记整个老年代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包括_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做CMS的“gc root”,来扫描老年代;
因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数--XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻代的对象无用的对象,并将对象放入幸存代或晋升到老年代,这些再进行年轻代扫描时,只需要扫描幸存区的对象即可,一般幸存代非常小,这大大减少了扫描时间。
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻代的对象对老年代的引用一句发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长时间的STW,所有通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。
另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled
6.并发清理
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了 。
这个阶段主要是清除那些没有标记的对象并且回收空间
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”
7.并发重置
并发重置阶段,将清理并回复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。
G1收集器
G1收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
G1收集器是一款主要面向服务端应用的垃圾收集器。
JDK9时,G1成为服务端模式下的默认垃圾收集器,CMS被声明为不推荐使用。
停顿时间模型:能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
G1可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,
即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过一个Region容量一半的对象即可判定为大对象。Region大小应为2的N次幂。
对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都被Humongous Region作为老年代的一部分来进行看待。
G1收集器的运作过程大致可划分为以下四个步骤:
初始标记:仅仅只是标记一个GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
当对象图扫描完成之后,还要重新处理SATB记录下的在并发时有引用变动的对象。
最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来指定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1收集器除了并发标记歪,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
G1从整体来看是基于标记-整理算法实现的收集器,但从局部上看又是基于标记-复制算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。
这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。
目前在小内存应用上CMS的表现大概率仍然会优于G1,而在大内存上G1则大多能发挥其优势,这个优劣势的Java堆的平衡点通常在6GB至8GB之间。
虚拟机性能监控、故障处理工具
jps:虚拟机进程状况工具
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID。
jstat:虚拟机统计信息监视工具
监视虚拟机各种运行状态信息的命令行工具。
jinfo:Java配置信息工具
实时查看和调整虚拟机各项参数。
jmap:java内存映像工具
用于生成堆转储快照,还可以查询finalize执行队列,Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
jhat:虚拟机堆转储快照分析工具
jhat命令与jmap搭配使用,来分析jmap生成的堆存储快照(一般不会这样使用,耗时且耗费硬件资源,同时工具也比较简陋)。
jstack:Java堆栈跟踪工具
jstack命令用于生成虚拟机当时时刻的线程快照。
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁,死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。
可视化故障处理工具
JHSDB:基于服务性代理的调试工具
JConsole:Java监视与管理控制台
VisualVM:多合-故障处理工具
Java Mission Contro:可持续的在线的监控工具
JVM调优(参数调优)
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
Extra Question
-
String str = new String("hello")时,创建了几个对象?
String str = new String("hello"),会进行两步操作:
1.JVM先在堆中创建一个指定的String对象"hello",并让str引用指向该对象。
2.JVM会在常量池中寻找或新建一个"hello",并让堆中对象与之关联。所以当方法为形参赋值时(str = "world"),只是为形参在常量值中新建一个"world"并引用,也只修改了形参,成员变量str本身未被修改。
所以,当String str = new String("hello")时,产生了一个String对象,如果常量池没有"hello"常量还会产生一个"hello"常量。(故可能产生一个或两个String对象) -
Java 1.8 GC变化
在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew):
1. 串行GC
在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来指定;
2. Parallel GC -- (Java1.8 的默认GC),它的速度是最快;
并行回收GC:在新生代采用复制算法,在老年代采用标记-压缩算法,在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来指定,用-XX:ParallelGCThreads=4来指定线程数;
- JDK8默认使用 Parallel Scavenge + Parallel Old
出处: https://www.cnblogs.com/winter0730/p/14505569.html