注:本文浓缩了宋红康老师JVM 入门到精通上篇(内存与垃圾回收)的精华
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
特点:
Java编译器输入的指令流基本上是一种基于栈的指令集架构,
另外一种指令集架构则是基于寄存器的指令集架构。工具体来说:这两种架构之间的区别:
基于栈式架构的特点
设计和实现更简单,适用于资源受限的系统
避开了寄存器的分配难题:使用零地址指令方式分配
指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小编译器容易实现。
不需要硬件支持,可移植性更好,更好实现跨平台
基于寄存器架构的特点
典型的应用是x86的二进制指令集:比如传统的PC以及 Android的Davlik虚拟机。
指令集架构则完全依赖硬件,可移植性差
性能优秀和执行更高效
花费更少的指令去完成一项操作。
在大部分情況下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于根式架构的指令集却是以零地址指令为主
同样执行2+3这种逻辑操作,其指令分别如下:
基于栈的计算流程(以Java虚拟机为例)
iconst2//常量2入栈
istore_1
iconst_3//常量3入栈
istore_2
i1oad_1
iload_2
iadd
//常量2、3比,执行相加
istore_0//结果5入栈
而基于寄存器的计算流程
mov eaX,2//将eax奇存器的值设1
add eax,3//使eax高存的值加3
由于跨平台性的设计,Java的指令都是根据栈设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
1 虚拟机的启动
Java虚拟机的启动是通过引导类加载器( bootstrap class loader)创建个初始类( initial c1ass)米完成的,这个类是由虚拟机的具体实现指定的。
2 虚拟机的执行
3 虚拟机的退出
1 Sun Classic VM
早在1996年Java1.0版本的时候,Sun公司发布了一款名为 Sun Classic VM的Java虚拟机,它同时也是世界上第一款商用Java虚拟机,JDK1.4时
完全被淘汰。
这款虚拟机内部只提供解释器。
如果使用JIT编译器,就需要进行外挂。但是一旦使用了JIT编译器,JIT就
会接管虛拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作
现在 hotspot内置了此虚拟机
2 Exact VM
为了解決上。JDK1.2时,Sun提供了此虚拟机
3 Hotspot VM
3 BEA的 JRockit
4 IBM 的 J9
5 Graal VM
2018年4月,Oracle Labs公开了Graal VM,号称"Run Programs Faster Anywhere”,勃勃野心
与1995年java的” write once, run anywhere"遥相呼应。
Graal VM在 Hotspot VM基础上増强而成的跨语言全栈虚拟机,可以作为“任何语言的运行平台使用。语言包括:Java、 Scala、 Groovy、Kotlin;C、C++ JavaScript、Ruby、 Python、R等
支持不同语言中混用对方的接口和对象,支持这些语言使用己经编写好的本地库文件
工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中表示。Graal VM提供 Truffle工.具集快速构建而向一种新语言的解释器。在运行时还能进行即时編译优化,获得比原生编译器更优秀的执行效率
如果有一天Hotspot VM真的被取代,Graal VM希望最大。但是Java的软件生态没有丝毫变化。
验证( Verify)
准备( Prepare):
解析( Resolve)
启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载使用C/C++语言实现的,嵌套在JVM内部。
它用来加载Java的核心库( JAVA HOME/jre/lib/rt.jar resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
并不继承自java.lang.Classloader,没有父加载器
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
出于安全考虑, Bootstrap.启动类加载器只加载包名为Java、Javax、sun等开头的类
扩展类加载器( Extension ClassLoader)
应用程序类加载器(系统类加载器,AppClassloader)
用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式:
隔离加载类
修改类加载的方式
扩展加载
防止源码泄漏
方式一:获取当前类的Classloader
clazz. getclassloader()
方式二:获取当前线程上下文的Classloader
Thread currentthread().getcontextclassloader()
方式三:获取系统的Classloader
Classloader. getsystemclassloader ()
方式四:获取调用者的ClassLoader
Drivermanager. getcallerclassloader()
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用伸为类型息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着 虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些 与线程对应的数据区域会随着线程开始和结束而创建和销毁。
灰色的为单独线程私有的,红色的为多个线程共享的。即:
每个线程:独立包括程序计数器、栈、本地栈
线程间共享:堆、堆外内存(永久代或元空间、代码缓存)
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
在 Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。 当一个Java线程准备好执行以后,此时一个操作系统的本地线程 也冋时创建。Java线程执行终后,本地线程也会回收。 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本 地线程初始化成功,它就会调用Java线程中的run()方法。
这些主要的后台系统线程在 Hotspot:JVM里主要是以下几个
JVM中的程序计数寄存器(Program counter Register)中, Register的命名源于 CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。 这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴
切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC 寄存器的一种抽象模拟
它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存 储区域。 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命 周期与线程的生命周期保持一致。 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址:或者, 如果是在执行 native方法,则是未指定值( undefined)
PC寄存器用来存储指向下一条指令的地址也即将要执行的指令代码。由执行引擎读取下一条指令。
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础 功能都需要依赖这个计数器来完成。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的节码指令
它是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
1 为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续 执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
2 PC寄存器为什么会被设定为线程私有?
所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU 会不停地做任务切换,这样必然导致经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程 都分配一个pc寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理 器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。 这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
栈是运行时的单位,而堆是存储的单位。 一个栈帧对应着一个方法
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
Java虚拟机栈是什么?
Java虚拟机栈( Java virtual Machine stack),早期也叫Java栈。 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧
( stack Frame),对应着一次次的Java方法调用。 是线程私有的
生命周期
生命周期和线程一致。
作用
主管Java程序的运行,它保存方法的局部变量(基本数据类型,引用数据类型的引用地址)、部分结果,并参与方法的 调用和返回。
栈是一种快速有效的分配存储方式,访问速度仅次于程 序计数器
JVM直接对Java栈的操作只有两个:
对于栈来说不存在垃圾回收问题
栈中可能出现的异常
设置栈内存大小
可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用最大可达深度
JVM直接对Java栈操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前 在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧
( Current frame),与当前栈帧相对应的方法就是当前方法( Current Method),定义这个方法的类就是当前类( Current class)。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的 顶端,成为新的当前帧。
注意:
每个栈帧中存储着:
局部变量表也被称之为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
这些数据类型包括各类基本数据类型、对象引用( reference),以及 returnAddress类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据 安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code 属性的 maximum local variables数据项中。在方法运行期间不会改变局部变量表的大小的
方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次 数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀 它的栈帧就越大,以满足方法调所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过 局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后随着方法栈帧的销毁,局部变量表也会随之销毁。
普通方法和构造器方法的局部变量表里会有this 变量,其放在索引0位置
double 类型 及long类型会占据2个索引插槽,调用时使用的是起始插槽,其他类型均占有一个索引插槽
栈帧中的局部变量表中的索引槽是可以重复利用的。如果一个局部变量过了其作用域,那么在其作用域之后申眀的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候 个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maκ stack的值
栈中的任何一个元素都是可以任意的Java数据类型。
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准 的入栈(push)和出栈(pop)操作来完成一次数据访问。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作 数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译 器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题, Hotspot JVM的设计者们提出了栈顶 缓存(Top-of- Stack Cashing)技术,将栈顶元素全部缓存 在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接 ( Dynamic Linking)。比如: invokedynamic指令在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 class文件的常量池里 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接:
当一个字节码文件被装载进JM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转为直接引用的 过程称之为静态链接。
动态链接:
**如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,**由于这种引用转换过具备动态 性,因此也就被称之为动态链接。
对应的方法的绑定机制为早期绑定(Early Binding)和晚期绑定 ( Late Binding),绑定是一个字段、方法或者类在符号引用被替换为
直接引用的过程,这仅仅发生一次。
早期绑定:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时 即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
如果方法在编译期就确定了具体的调用版木,这个版本在运行时是不可变的。 这样的方法称为非虚方法。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
其他方法称为虚方法。
虚拟机中提供了以下几条方法调用指令
动态调用指令:
invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而 invokedynamic指令则支持由用户确定方法版本**。其中 invokestatic指令和 invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。**
存放调用该方法的PC存器的值
一个方法的结束,有两种方式:
正常执行完成
出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的 指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
当一个方法开始执行后,只有两种方式可以退出这个方法:
Java虛拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法栈,也是线程私有的。允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 stackoverflowError异常。如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存, 或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个 outofMemoryError异常。
本地方法是使用C语言实现的。它的具体做法是 Native method stack中登记 native方法,在Execution Engine执行时加载本地方法库。
一个进程对应一个JVM的实例,一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( Thread Local Allocation Buffer,TLAB)
ava8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
Young Generation Space 新生区
Young/New 又被划分为Eden区和 Survivor区
Tenure generation space养老区 Old/Tenure
Meta Space 元空间 Meta
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,
默认情况下:初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4
可以通过选项”-Xmx”和”-Xms〃来进行设置 :建议将初始值和最大的设置一致的
-Xms 用于表示堆区(年轻代和老年代)的起始内存,等价于-XX: InitialHeapsize
-Xmx"则用于表示堆区(年轻代和老年代)的最大内存,等价于-XX: MaxHeapsize
一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
查看设置的参数
存储在JVM中的Java对象可以被划分为两类
类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
Java堆区进一步细分的话,可以划分为年轻代( YoungGen)和老年代( oldGen)
其中年轻代又可以划分为Eden空间、 Survivor0空间和 Survivor1空间(有时也叫做from区、to区 )
配置新生代与老年代在堆结构的占比。
默认-XX: NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX: NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
在 Hotspot中,Eden空间和另外两个 Survivor空间缺省所占的比例是8:1: 1
当然开发人员可以通过选项“-XX: SurvivorRatio”调整这个空间比例。比
如 -XX: SurvivorRatio=8
几乎所有的Java对象都是在Eden区被new出来的(除非对象超过其大小,会移动到老年代)。
绝大部分的Java对象的销毁都在新生代进行了。
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
关于伊甸园:伊甸园区满了会触发新生代GC,此时也会检查s0 或者s1 的对象存活(被动触发GC)
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区元空间收集。
JDK命令行
Eclipse: Memory Analyzer Tool
Jconsole
VisualVM
Profiler
Java Flight Recorder
GCViewer
GC Easy
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代
针对 Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:
一种是部分收集 ( Partial GC),一种是整堆收集(Full GC)
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
新生代收集( Minor GC/ Young GC):只是新生代的垃圾收集
老年代收集( Major GC/ old GC):只是老年代的垃圾收集。
目前,只有 CMS GC会有单独收集老年代的行为。
注意,很多时候 Mayor GC会和Full GC混淆使用,需要具体分辨是老年代 回收还是整堆回收。
混合收集( Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
目前,只有G1 GC会有这种行为
整堆收集:(FullGC):收集整个Java堆和方法区的垃圾收集。
触发时机
触发Full GC执行的情况有如下五种:
说明:Full GC是开发或调优中尽量要避免的。这样暂时时间会短一些。 当出现OOM之前肯定执行了一次Full GC
如果对象在Eden出生并经过第一次 Minor GC后仍然存活,并且能被 Survivor容纳的话,将被移动到 Survivor空间中,并将对象年龄设为1。对象在 Survivor区中每熬过一次 Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中
对象晋升老年代的年龄阈值,可以通过选项-XX: MaxTenuringThreshold来设置
针对不同年龄段的对象分配原则如下:
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内
存空间是线程不安全的.为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题, 同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为 内存分配的首选。
在程序中,开发人员可以通过选项“-XX: UseTLAB”设置是否开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的,当然我们可以通 过选项“-XX: TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百 分比大小。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
-XX:+ PrintFlagsInitial:查看所有的参数的默认初始值
-XX+ PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改不再是初始值)
-Xms:初始堆空间内存(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX: SurvivorRatio:设置新生代中Eden和S0/S1空间的比
-XX: MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
打印GC简要信息:1 -XX:+PrintGc 2 - verbose:gc
-XX: HandLepromotionFailure 是否投置空间分配担保
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有种特殊情况,那就是如果经过逃逸分析( Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
如何将堆上的对象分配到栈,需要使用逃逸分析手段 ?
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析, Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:
在JDK6u23版本之后, Hotspot中默认就已经开启了逃逸分析。
如果使用的是较早的版本,开发人员则可以通过:
选项“-XX:+ DoEscapeAnalysis〃显式开启逃逸分析
通过选项“-XX:+ PrintEscapeAnalysis〃查看逃逸分析的筛选结果。
结论:开发中能使用局部变量的,就不要使用在方法外定义。
使用逃逸分析,编译器可以对代码做如下优化:
《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于 Hotspot JVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看作是一块独立于Java堆的内存空间。
方法区( Method area)与Java堆一样,是各个线程共享的内存区域
在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,便用元空间取代了永久代
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢岀错误:java.lang. outofMemoryError:PermGen space 或者 java. lang. OutofMemoryError: Metaspace
关闭JVM就会释放这个区域的内存。
方法区的大小不必是固定的,jVM可以根据应用的需要动态调整。
JDK7及以前:
通过-XX: Permsize来设置永久代初始分配空间。默认值是20.75M
-XX: MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是 82M
当JVM加载的类信息容量超过了这个值,会报异常 OutofMemoryerror: PermGen space
JDK8及以后:
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
对每个加载的类型(类class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:
①这个类型的完整有效名称(全名=包名,类名)
②这个类型直接父类的完整有效名(对于 interface或是java. lang Object,都没有父类)
③这个类型的修饰符(public, abstract,final的某个子集)
④这个类型直接接口的一个有序列表
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符( public, private, protected, static, final, volatile, transient的某个子集)
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
non-final的类变量
静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。
类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
全局常量: static final
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
字节码文件,内部包含了静态的常量池。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表( Constant pool Table),包括各种字面量和对类型、 域和方法的符号引用(描述)。
一个个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池将符号引用转换成直接引用。
几种在常量池内存储的数据类型包括:
静态常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
静态常量池表( Constant pool Table)是 Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池( Runtime Constant pool)是方法区(元空间)的一部分。
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样, 是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了, 使用栈帧的动态链接将符号引用转换为真实地址。
运行时常量池类似于传统编程语言中的符号表( symbol table),但是它所包含的数据却比符号表要更加丰富一些。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛 OutOfMemoryError异常。
1.首先明确:只有 Hotspot才有永久代
BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。
2 Hotspot中方法区的变化:
元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
1 永久代为什么要被元空间替换?
和JRockit进行融合,JRockit没有是元空间
永久代设置空间大小是很难确定的。
在某些场景下,如果动态加载类过多,容易产生Perm区的OOM
对永久代进行调优是很困难的
2 String Table为什么要调整?
JDK7中将 StringTable放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会触发。而Full GC是老年代的空间不足、永久代不足时才会触发。这就导致 Stringtable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
**3 静态变量放在哪里 ? **
静态引用对应的对象实体new 的结构始终都存在堆空间
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这
部分区域的回收有时又确实是必要的。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
方法区内常量池之中主要存放的两大类常量:字面量和符号引用。
字面量比较接近]ava语言层次的常量概念,如文本字符串、被声明为final的常量值等。
而符号引用则属于编译原理方面的概念,包括下面三类常量:
1、类和接口的全限定名
2、字段的名称和描述符
3、方法的名称和描述符
Hotspot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
回收废弃常量与回收Java堆中的对象非常类似。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。
需要同时满足下面三个条件:
对象实例化的过程
JVM是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?
定位,通过栈上 reference访问
对象访问方式:
不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
直接内存是在Java堆外的、直接向系统申请的内存区间。
来源于NIO,通过存在堆中的 DirectByteBuffer操作 Native内存通常,访问直接内存的速度会优于Java堆。即读写性能高。
因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
传统的内存:读写文件,需要与磁盘交互, 需要由用户态切换到内核态在内核态。
使用NIO时,操作系统划出的直接缓存区可以被java 代码直接访问,只有 一份。NIO适合对大文件的读写操作。
由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。 否则会报本地内存的OOM: OutOfMemoryError; Direct buffer memory
直接内存大小可以通过 MaxDirectMemorySize设置 ,如果不指定,默认与堆的最大值-Xmx参数值一致
缺点:
简单理解:
java process memory = java heap+ native memory
执行引擎是Java虚拟机核心的组成部分之一
虚拟机 是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集
和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执
行那些不被硬件直接支持的指令今集格式
JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JⅦM所识别的字节码指令、符号表,以及其他辅助信息那么,如果想要让一个Java程序运行起来,执行引擎( Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
执行引擎在执行的过程中究竟 需要执行什么样的字节码指令完全依赖于PC寄存器。
每当执行完一项指令操作后, PC寄存器就会更新下一条需要被执行的指令地址。
当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,
以及通过对象头中的元数据指针定位到目标对象的类型信息。
从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流处理过程是字节码解析执行的等效过程,输出的是执行结果
Java代码编译是由Java源码编译器来完成
Java字节码的执行是由JVM执行引擎来完成
什么是解释器( Interpreter),什么是JIT编译器?
解释器:
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行
JIT( Just In Time Compiler)编译器:
就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
为什么说Java是半编译半解释型语言?
在执行字节码的时候既可以使用解释器也可以使用JIT编译器
JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行
当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
基于解释器执行已经沦落为低效的代名词, 为了解决这个问题,JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。
不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献
Hotspot JVM是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
既然 HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRckitM内部就不包含解释器字节码全部都依靠即时编译器编译后执行
原因:
当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行编译器JIT要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码 需要一定的执行时间。但编译为本地代码后执行效率高
解释器响应速度快,JIT响应速度较慢但翻译为本地代码后执行效率高
当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率,所以要采用解释器与即时编译器并存的架构来换取一个平衡点
当然是否需要启动JI编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之 为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR( On stack Replacement)编译。
一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。
目前 Hotspot VM所采用的热点探测方式是基于计数器的热点探测
采用基于计数器的热点探测, Hotspot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器( Invocation Counter)和回边计数器(Back Edge Counter)。
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”( Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译
-Xint:完全采用解释器模式执行程序;
-Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
-Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
C1和C2编译器不同的优化策略:
在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除。
方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减 少参数传递以及跳转过程
去虚拟化:对唯一的实现类进行内联
冗余消除:在运行期间把一些不会执行的代码折叠掉
C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化 :
标量替换:用标量值代替聚合对象的属性值
栈上分配:对于未逃逸的对象分配对象在栈而不是堆
同步消除:清除同步操作,通常指 Synchronized
总结:
String再也不用char[]来存储啦,改成了byte[]加上编码标记,节约了一些空间
String:代表不可变的字符序列。简称:不可变性
字符串常量池中是不会存储相同内容的字符串的
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的, string类型的常量池比较特殊。它的主要使用方法有两种。
两个字符串s 和 t 如果s.intern() == t.intern() 成立那么 s.equals(t) 必然成立
如果不是用双引号声明的 string对象,可以使用 string提供的 intern方法: intern 方法会从字符串常量池中査询当前字符串是否存在,若不存在就会将当前字符串放入常池中。
Interned string就是确保守符串在内存里只有一份拷贝这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在符串内部池 (String Intern Pool )
对于程序中大量存在存在的字符串,尤其其中存在很多重复字符时,使用 intern()可以节省内存空间
如何保证变量s指向的是字符常量池中的数据呢?
有两种方式
方式一: string s=" jdk";//字面量定义的方式
方式二: string s= new String(" jdk"). intern();
new String( “a”)+new String( b") 几个对象?
对象1: new StringBuilder()
对象2: new String(“a”)
对象3:常量池中的”a"
对象4:; new String(“b”)
对象5:常量池中的”b”
对象6:StringBuilder toString() 方法的new String;强调一下, tostring()的调用,在字符串常量池中,没有生成”ab"
jdk7/8 :false;true
s 返回的是堆空间的地址,s2 是常量池的地址 false
s3 记录的地址为new String(“11”) “+”拼接形式的常量池中不存在11
s3.intern() :
jdk 6 中 会把此对象复制一份即创建一个新的对象“11” 所以是新的地址 ,并放入串池,并返回串池中的对象地址
jdk7/8 则会把对象已存在的引用地址复制一份即对空间的new String(“11”) 地址被放入常量池, 并返回常量池中11地址但s3并没有接收即s3=s3.intern(),所以仍然指向堆空间的创建的11的地址, s3 指向堆空间中 new String("11”)的地址 s4 会使用s3 方法生成的11 的地址也是指向new String("11”)
总结 string的 intern()的使用:
Xms15m-Xmx15m -XX: +PrintstringTablestatistics -XX: +PrintGCDetails
UseString Deduplication(bool):开启 string去重,默认是不开启的,需要手动开启。
PrintstringDeduplicationStatistics(bool):打印详细的去重统计信息
StringDeduplicationAgeThreshold( uinta):达到这个年龄的 string对象被认为是去重的候选对象
1 什么是垃圾( Garbage)呢?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空 间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
2 为什么需要GC?
对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完因为不断地分配内存空间而不进行回收,除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
没有垃圾回收器,java也会和c++一样,各种悬垂指针,野指针,泄露问题
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和放法区的回收。
其中,Java堆是垃圾收集器的工作重点。
从次数上讲:
频繁收集 Young区
较少收集old区
基本不动方法区(元空间)
Java语言提供了对象终止(finalzation)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一·个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等
永远不要主动调用某个对象的finalize()方法 应该交给垃圾回收机制调用
在finalize()时可能会导致对象复活。
finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下, 若不发生GC,则 finalize()方法将没有执行机会。
一个糟糕的finalize()会严重影响GC的性能。
从功能上来说, finalize()方法与c++中的析构函数比较相似,但是Java采用的是基
于垃圾回收器的自动内存管理机制,所以finalize()方法在本质上不同于c++中的函数。
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说, 此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那 么对它的回收就是不合理的,为此,
定义虚拟机中的对象可能的三种状态。如下:
以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
判定一个对象objA是否可回收,至少要经历两次标记过程:
在默认情况下,通过 System.gc()或者 Runtime. getRuntime().gc()的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放
被丢弃对象占用的内存。
System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不一定马上执行,无法确保执行时间)。
仅仅提醒JVM 需要执行一次垃圾回收,但不一定会执行
JVM实现者可以通过 System.gc()调用来决定JVM的GC行为。
一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用 System.gc()。
System. runFinalization() 调用后强制调用使用引用的对象的 finalize()方法
JavaDoc中对 OutOfMemoryError的解释是,没有空闲内存,并且垃圾集器也无法提供更多内存
没有空闲内存的情况:说明ava虚拟机的堆内存不够 :
在抛出OutOfMemoryError之前,通常垃圾收集器一定会被触发,尽其所能去清理出空间
当然,也不是在任何情况下垃圾收集器都会被触发的 比如,我们去分配一个超大对象类似一个超大数组超过堆的最大值,JVM可以判
断出垃圾收集并不能解决这个问题,所以直接抛出 OutOfMemoryError
只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
stop-the-world,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,
有点像卡死的感觉,这个停顿称为STW。
可达性分析算法中枚举根节点( GC Roots)会导致所有Java执行线程停顿 .分析工作必须在一个能确保一致性的快照中进行
一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性
无法保证
被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉
开发中不要用 System.gc() 会导致 Stop The World的发生
是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
当系统有一个以上CPU执行一个进程时,另一个CPU可以执行另一个进程 两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel) 其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的。
只有在多CPU或者一个CPU多核的情况中,才会发生并行。 否则,看似同时发生的事情,其实都是并发执行的
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
如 ParNew、Parallel Scavenge、Parallel old;
串行( Serial) 相较于并行的概念,单线程执行。
如果内存不够,则程序暂停,启动J垃圾回收器进行垃圾回收。回收完,再启动
并发( Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。 用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上; CMS G1
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点( Savepoint)”
Safe point的选择很重要**,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题**。大部分指令的执行时间都非常短暂, 通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择些执行时间较长的指令作为 Safe point,如方法调用、循环跳转和异常跳转等。
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC 的 Savepoint。但是,程序“不执行”的时候呢?例如线程处于Sleep状
态或Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全 区域( Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把 Safe Region看做是被扩展了的 Safepoint。
实际执行时:
对象标记:对象存活判断
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
引用计数算法( Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1:当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点: 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集( Tracing Garbage Collection)
基本思路:
所谓" GC Roots"根集合就是一组必须活跃的引用。
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对 象分配内存。
目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法(Mark swep)、复制算法( Copying)、标记-压缩算法(Mark-Compact )
标记-清除算是一种非常基础和常见的垃圾收集算算法
执行过程:
当堆中的有效内存空间( available memory)被耗尽的时候,就会停止整个程序(也被称为 stop the world),然后进行两项工作,
第一项则是标记,
第二项则是清除。
标记: Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header中记录为可达对象。 注意:标记的是可达对象(非垃圾对象)
清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header中没有标记为可达对象,则将其回收。
缺点
注意:何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够如果够,就存放。
核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
缺点:
此算法的缺点也是很明显的,就是需要两倍的内存空间。
对于G1这种分拆成为大量 region的GC,复制而不是移动,意味着GC需要维护 分区之间对象引用关系,不管是内存占用或者时间开销也不小。
**如果系统中的垃圾对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。 **。
应用场景:
在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使其他的算法。
执行过程:
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。
是否移动回收后的存活对象是一项优缺点并存的风险决策
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时
JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
优点:
缺点:
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的 阶段,比标记-清除多了一个整理内存的阶段
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
目前几乎所有的GC都是采用分代收集( Generational Collecting)算法执行垃圾回收的。
在 Hotspot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点
年轻代( Young Gen)
老年代( Tenured Gen)
上述现有的算法,在垃圾回收过程中,应用软件将处于一种 Stop the World 的状态。在 stop the world狀态下,应用程序所有的线程都会挂起,暂停正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集( Incremental Collecting)算法的诞生
基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-淸除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
缺点
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,使得垃圾回收的总体成本上升,造成系统吞吐量的下降
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC 的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合貍地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间region。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用( strong Reference)、软引用( Soft reference)、弱引用( Weak Reference)和虚引用 (Phantom reference)4种,这4种引用强度依次逐渐减弱
强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象
相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成Java内存泄漏的主要原因之一
软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地艳引用存放到一个引用队列( Reference Queue)。
JDK1.2版之后提供了java.lang.ref.SoftReference类来实现软引用。
弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是 否充足,都会回收掉只被弱引用关联的对象。
但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引 对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。
软引用、弱引用都非常适合来保存那些可有可无的缓存数据
弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。
也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。
一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收
它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。
虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知
因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
在JDK1.2版之后提供了 PhantomReference类来实现虚引用。
垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。
由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。
从不同角度分析垃圾收集器,可以将GC分为不同的类型。
简单来说,主要抓住两点:
1 吞吐量
2 暂停时间
在设计(或使用)GC算法时我们必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折衷。
现在标准:在最大吞吐量优先的情况下,降低停顿时间。
串行回收器: Serial、 Serialold
并行回收器: ParNew、Parallel Scavenge、Parallel0ld
并发回收器:CMS、G1
1.两个收集器间有连线,表明它们可以搭配使用:
Serial/ Serialold、 Serial/CMS、 ParNew/ Serialold、 ParNew/CMS、Parallel Scavenge/Serial old, Parallel Scavenge/Parallel old, G1
2.其中 serialold作为CMS出现" Concurrent Mode Failure"失败的后备预案。
3.(红色虚线)由于维护和兼容性测试的成本,在JDK8时将 Serial+CMS、ParNew+ Serialold这两个组合声明为废弃(EP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
4.(绿色虚线) JDK14中:弃用Parallel Scavenge和 Serial0ldGC组合(JEP 366)
5.(青色虚线) JDK14中:删除CMS垃圾回收器(JEP363)
Serial收集器作为 HotSpot中client模式下的默认新生代垃圾收集器。
Serial收集器采用复制算法、串行回收和〃stop-the-World"机制的
方式执行内存回收。
除了年轻代之外, Serial收集器还提供用于执行老年代垃圾收集的
Serialold收集器。 serialold收集器同样也采用了串行回收
和〃Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法
Serialold是运行在 Client模式下默认的老年代的垃圾回收器
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束( Stop The World)。
优势:
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说, Seria收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
在 Hotspot虚拟机中,使用-XX:+ UseSerialgc参数可以指定年轻代和老年代都使用串行收集器
ParNew收集器则是 Seral收集器的多线程版本 只能处理的是新生代
ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。
ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the- World"机制。
ParNew是很多JVM运行在 Server模式下新生代的默认垃圾收集器。
对于新生代,回收次数频繁,使用并行方式高效。
对于老年代,闻收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
但是在单个CPU的环境下, ParNeW收集器不比 Serial收集器更高效。虽然 Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销
在程序中,开发人员可以通过选项"-XX:+ UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数
Hotspot的年轻代中除了拥有 ParDew收集器是基于并行回收的以外, Parallel Scavenge收集器同样也采用了复制算法、并行回收和"stop the World"机制。
那么Parallel收集器的出现是否多此一举?
和 ParNell收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量( Throughput),它也被称为吞吐量优先的垃圾收集器。
自适应调节策略也是Parallel Scavenge与 ParNew一个重要区别。
参数配置:
-XX: +UseParallelGC手动指定年轻代使用parallel并行收集器执行内存回收任务。
-XX+ UseParalleloldGC手动指定老年代都是使用并行回收收集器。分别适用于新生代和老年代。默认jdk8是开启的。 上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
-XX:ParallelGcThreads设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能
-XX: MaxGCPauseMillis设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
为了尽可能地把停顿时间控制在 MaxGcpauseMllls以内,收集器在工作时会调整Java堆大小或者其他一些参数。
对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
该参数使用需谨慎。
-XX: GCTimeRatlo垃圾收集时间占总时间的比例(=1/(N+1)) 用于衡量吞吐量的大小。
取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1号。
与前一个-XX: MaxGcPauseMillis参数有一定矛盾性。暂停时间越长, Radio参数就容易超过设定的比例
-XX:+ UseAdaptiveSizePolicy设置Parallel Scavenge收集器 具有自适应调节策略
在这种模式下,年轻代的大小、Eden和 Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿 时间之间的平衡点。
在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间 (MaxGCPauseMills ) 让虚拟机自己完成调优工作
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时 间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the- world"
CMS整个过程比之前的收集器要复杂,整个过程分为4个要阶段,即初始标记阶段、并发
标记阶段、重新标记阶段和并发清除阶段。
初始标记( Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop- the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出 GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
并发标记( Concurrent-Mark)阶段:从 GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起
重新标记( Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
并发清除( Concurrent- Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
CMS收集器的垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分 配内存空间时,将无法使用指针碰撞( Bump the Pointer)技术,而只能够选择空闲列表( Free list)执行内存分配。
既然 Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
因为当并发清除的时候,用 Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提 的它运行的资源不受影响Mark Compact更适合“ Stop the World” 这种场景下使用
CMS的优点
CMS的弊端:
会产生内存碎片
CMS收集器对CPU资源非常敏想感
CMS收集器无法处理浮动垃圾
在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收
-XX:+ Use ConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务
-XX: CMSlnitiatingoccupanyFraction设置堆内存使用率的阈值一旦达到该阈值,便开始进行回收
-XX:+ UseCMSCompactAtFullCollection用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX CMSFullGCsBeforeCompaction设置在执行多少次FullGC后对 内存空间进行压缩整理。
-XX:ParallelCMSThreads设置CMS的线程数量。
如果你想要最小化地使用内存和并行开销,请选 Serial GC;
如果你想要最大化应用程序的吞吐量,请选ParallelGC
如果你想要最小化GC的中断或停顿时间,请选 CMS GC。
既可以适用于新生代也可以适用于老年代
因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域( Region)(物理上不连续的)。使用不同的 Region来表示Eden、幸存者0区,幸存者1区,老年代等。
G1GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region
G1( Garbage- First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征
G1使用了全新的分区(Region)算法
兼具并行与并发
分代收集
空间整合
可预测的停顿时间模型
G1除了追求低停顿外,还能建立可预测的停顿 时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
缺点
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中G1无论是为了垃圾收集产生的内存占用( Footprint)还是程序运行时的额外执行负载都要比CMS要高
G1的设计原则就是简化JWM性能调优,开发人员只需要简单的三步即可完成调优:
第一步:开启G1垃圾收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间
G1中提供了三种垃圾回收模式: YoungGC、 Mixed Go和FullGC,在不同 的条件下被触发
面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
如:在堆大小约6GB或更大时,可预测的暂停时间可以低于8.5秒;(G1通过每次只清理一部分而不是全部的 Region的增量式清理来保证每次GC停顿时间不会过长)。
用来替换掉JDK1.5中的CMS收集器
在下面的情况时,使用G1可能比CMS好:
①超过58%的Java堆被活动数据占用;
②对象分配频率或年代提升频率变化很大
③GC停顿时间过长(长于8.5至1秒)
G1 GC的垃圾回收过程主要包括如下三个环节:
一个对象被不同区域引用的问题
一个 Region不可能是孤立的,一个 Region中的对象可能被其他任意 Region中对象引用, 判断对象存活时,是否需要扫描整个Java堆才能保证准确?
在其他的分代收集器,也存在这样的问题(而G1更突出)
回收新生代也不得不同时扫描老年代,这样的话会降低 Minor GC的效率;
解决方法:
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。
年轻代垃圾回收只会回收Eden区和 Survivor区。
YGC时,首先G1停止应用程序的执行(Stop-The- World),G1创建回收集(collection set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和 Survivor区所有的内存分段。
第一阶段,扫描根
根是指 static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同Rset 记录的外部引用作为扫描存活对象的入口。
第二阶段,更新Rset(记忆集)
处理 dirty card queue中的card,更新Rset。此阶段完成后,Rset可以准确的反映老年代对所在的内存分段中对象的引用
第三阶段,处理RSet
识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象
第四阶段,复制对象
此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到 Survivor区中空的内存分段Survivor区內存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到0ld区中空的内存分段。如果 Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
第五阶段,处理引用
处理Soft,Weak, Phantom,Final, JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,
虚拟机会触发一个混合的垃圾收集器, 即 Mixed GC,该算法并不是一个old GC,除了回收整个 Young Region,
还会回收一部分的old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是 Mixed GC并不是FullGC。
G1的初衷就是要避免FullGc的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
要避免FullGC的发生,一旦发生需要进行调整。什么时候会发生FullGC
呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用, 则会回退到full gc,这种情况可以通过增大内存解决。 导致G1 Full GC的原因可能有两个
内存分配与垃圾回收的参数列表
-XX:+PrintGC 输出GC日志。类似:- verbose:gc
-XX:+ PrintGcDetalls 输出GC的详细日志
-XX:+ PrintGcTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+ PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2013-05 04T21:53:59.234+0800)
-XX: +PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc: …/logs/gc.log 日志文件的输出路径
可以用一些工具去分析这些gc日志。
常用的日志分析工具有: GCViewer、 GCEasy、 GCHisto、 GCLogVlewer、
Hpjmeter、 garbagecat等。