前言
该读书笔记用于记录在学习《深入理解Java虚拟机——JVM高级特性与最佳实践》一书中的一些重要知识点,对其中的部分内容进行归纳,或者是对其中不明白的地方做一些注释。主要是方便之后进行复习。
目录
垃圾收集器与内存分配策略
哪些内存需要垃圾回收
在上一节中有提到在运行时数据区域包括:堆、虚拟机栈、本地方法栈、程序计数器、方法区(JDK1.7及之前)、元空间(JDK1.8及之后)。在这些区域中,程序计数器占用内存极小,可以忽略;栈区域在编译期就可以确定下来,并且其声明周期随线程保持一致,也不用管;而Java堆和方法区、元空间中接口的不同实现类需要的内存不同,方法的不同实现需要的内存也不同,而且这些所需的内存需要在运行时才能确定,所以垃圾回收关注的主要内容就是这些区域。
对象是否不再使用
引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一;引用失效的时候,计数器就减一;在任何时候只要计数器为0就代表该对象就是不会再被使用的。
该方法的优点:
- 实现较为简单
- 判定效率很高,基本没有其他额外的操作
缺点:
很难解决对象之间相互循环引用的问题。即两个对象相互持有对方的引用,除此之外再没有别的地方使用这两个对象,但是因为相互引用导致计数器不可能为0,所以无法被回收
可达性分析算法
算法描述
通过选择一些满足一定条件的对象作为节点,从这些节点开始往下搜索,搜索经过的路径被称为引用链(有直接或间接引用关系的对象都在引用链上),这些对象被成为"GC Roots",当一个对象达到GC Roots没有任何引用链时则判定该对象不可用,即使该对象仍旧被其他对象引用,只要其与GC Roots没有关系既是不可用的。
可作为GC Roots的对象
- 虚拟机栈中引用的对象。
- 方法区中常量引用的对象。
- 方法区中类静态属性引用的对象。
- 本地方法区中JNI引用的对象。
简单来说包括以下几种类型:
- Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的
- Thread - 活着的线程
- Stack Local - Java方法的local变量或参数
- JNI Local - JNI方法的local变量或参数
- JNI Global - 全局JNI引用
- Monitor Used - 用于同步的监控对象
Java中的引用
在最初的Java中,引用仅仅是指一个对象的数据中存储的值是另外一块内存的起始地址。在JDK1.2之后将引用分为多种:
强引用:强引用是类似于
User user = new User()
,是在代码中最常用的一种方式。只要强引用存在,那么垃圾回收器就永远不会回收掉被引用的对象。软引用:软引用用于描述一些有用但是并不是一定需要的对象,对于软引用的对象,当内存将要发生溢出时,会将这些对象列入回收范围中进行一次回收,如果将软引用的对象回收后内存还是不足才会抛出内存溢出异常。在JDK中使用
SoftReference
类实现软引用。SoftReference<Object> softReference = new SoftReference<Object>(new Object());
弱引用:弱引用用于描述非必须的对象,弱引用对象在下一次垃圾回收时一定会被回收,无论当前内存是否足够。在JDK中使用
WeakReference
定义弱引用。虚引用:一个对象是否存在虚引用对其生存时间不会有任何关系,只是在这个对象呗收集器回收时收到一个系统通知。在JDK中使用
PhantomReference
来实现虚引用。
对象的自救
实际上,在可达性算法中即使是不可达的对象也并非一定会被回收的,判断其是否会被回收还需要走以下流程:
如果对象在可达性分析中被判定没有与GC Roots相连接的引用链那么改对象将会被标记,然后进行一次筛选。
筛选的条件是判断该对象是否有必要执行finalize()方法。是否有必要执行finalize()方法的条件是当对象没有覆盖finalize()方法或者该对象的finalize()方法已经被虚拟机调用过,这两种情况都会被判定为没有必要执行。
如果被判定为有必要执行finalize方法,则会将其放在一个队列中,稍后执行。在finalize()方法中是对象逃脱被回收的最后机会,只要重新与引用链中的任何一个对象建立关系即可。
public class FinalizeEscape {
private static FinalizeEscape escape = null;
public static void main(String[] args) throws InterruptedException {
escape = new FinalizeEscape();
//模拟对象使用后断开引用链
escape = null;
//对象自救
System.gc();
Thread.sleep(500);
if(escape != null){
System.out.println("对象没有被清除!");
}else {
System.out.println("对象已经被清除!");
}
//模拟第二次逃脱gc
escape = null;
System.gc();
Thread.sleep(500);
if(escape != null){
System.out.println("对象没有被清除!");
}else {
System.out.println("对象已经被清除!");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize execute!");
escape = this;
}
}
运行结果:
finalize execute!
对象没有被清除!
对象已经被清除!
在对同一对象进行两次模拟逃脱gc,第一次成功第二次失败,是因为一个对象的finalize()方法只会被调用一次。
方法区回收
在方法区的回收主要包括两个方面:
- 废弃常量
- 无用的类
废弃常量
废弃常量是指在常量池中存在一个值,假设为一个字面量,但是在当前系统中没有任何的一个对象引用了该字面量。那么久认为该字面量是废弃的,在下一次垃圾回收的时候将其进行回收。同理常量池中的其他类、接口、方法、字段等的符号引用的回收也是类似。
无用的类
要判定一个类是否可以被回收需要满足以下几个条件:
- 该类所有的实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的字节码对象Class没有在任何地方被引用,无法再任何地方通过反射访问到该类的方法
当一个类满足以上条件后就允许被回收,但不是一定会被回收。是否对类进行回收再HotSpot虚拟机中提供了-Xnoclassgc
参数进行控制。也可以使用-verbose:class,-XX:+TraceClassLoading,-XX:+TraceClassUnloading
参数查看类加载和卸载信息。
在使用反射、动态代理、动态生成jsp和OSGI等频繁自定义ClassLoader的场景都需要虚拟机具备卸载类的功能,保证永久代不会溢出。
需要注意的是在JDK1.7时HotSpot就已经将运行时常量池迁移到堆中,在JDK1.8中更是直接移除了方法区,所以上面的介绍需要对应到具体的版本,并不是指着一定是在方法区完成。虽然区域发生变化但是回收的原则基本还是这样。
元空间回收
JDK1.8开始把类的元数据放到本地堆内存(native heap)中,如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和进行类卸载,这一块的回收要求较高,上文中有简单说过。
有关元空间的JVM参数:
- -XX:MetaspaceSize :是分配给类元数据空间(字节)的初始大小。该值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
- -XX:MaxMetaspaceSize :是分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
- -XX:MinMetaspaceFreeRatio/-XX:MaxMetaspaceFreeRatio :表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小/最大比例,不够就会触发垃圾回收。
垃圾收集算法
标记-清除算法
标记-清除算法的基本内容就同其名字一样,存在着标记和清除两个阶段:首先查找与GC Roots无任何关联的对象,标记处所需回收的对象(如何标记在内存回收中已经介绍了,通过判断是否有必要或已经执行了finalize()方法),在标记完成之后再统一清除。
标记过程:虚拟机从作为GC Roots的根节点出发进行搜索,对可被访问到的对象做一个标记,其他未被标记的对象就是需要被回收的。效率低是因为目前来说项目中的对象极多,单单是进行遍历就需要耗费较长的时间。
好处:实现简单,标记-清除算法流程十分简单,实现也没有很复杂的地方。
缺点:
1.效率较低:因为标记和清除的过程效率都不高
2.浪费内存空间:在清除标记的对象后造成了内存中大量不连续的空间,一旦有大的对象进入可能会因为没有合适的存放的地方而造成再一次的GC。
复制算法(多用于新生代)
复制算法的基本内容是要求虚拟机并不将所有的内存空间用来存放对象。复制算法将内存分为两块,每一次都只是使用其中的一块,当触发GC时,将存放对象的那一块内存上还存活的对象复制到另一块上去,然后将之前的内存块全部清除。
优点:实现简单,而且因为在将存活对象转移时顺序内存存放不用考虑内存碎片的问题,效率较高。
缺点:
1.始终有一部分内存没有得到使用,造成空间浪费。要保证存活的对象能够完全复制,那么就要求两块内存大小一致(50%),因为可能存在没有任何对象死亡的极端情况,但是这样将会极其浪费,而如果不这样分配,就必须引入其他机制保证对象能够被完整的复制。
标记-整理算法
标记整理算法的标记阶段同标记-清除算法一致,不过标记后并不立即清除,而是将存活(不会被GC)的对象移向内存的一端,将存活的对象全部移动后将边界外的清除掉。
优点:解决了内存碎片的问题
缺点:标记阶段效率本身较低,还多加了一个整理阶段,还是在于总体效率较低
分代收集算法
分代收集算法实际上并不是一个新的实现方式,只是将虚拟机分成几块,每一块根据它的实际作用来选择适合的算法,这些算法可以是标记-清除,复制算法等等。
基于分代的收集思想,将堆内存分为以下几个部分:
将堆内存分为新生代(Young)和老年代(Old),新生代又分为Eden区、from区和to区,默认Eden:from:to=8:1:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象可能会直接放到老年代),具体的内存分配在后面记录。
HotSpot中的算法实现
枚举根节点
在可达性分析中,可作为GC Roots的节点主要是全局性的引用与执行上下文,如果要逐个检查引用,必然消耗时间。
另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的时间间隔中进行,这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就像被暂停在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,也就是在分析过程中用户线程还在工作。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。
但是目前主流的Java虚拟机都是准确式GC(准确式GC是指就是让JVM知道内存中某个位置数据的类型是什么),所以在执行系统停顿下来之后,并不需要一个不漏的检查执行上下文和全局的引用位置,虚拟机是有办法得知哪些地方存放的是对象的引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。
安全点
在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但可能导致引用关系变化的指令非常多,如果为每一条指令都生成OopMap,那将会需要大量的额外空间,这样GC的空间成本会变的很高。
实际上,HotSpot也的确没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。SafePoint的选定既不能太少,以致让GC等待时间太久,也不能设置的太频繁以至于增大运行时负荷。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。
对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。
而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。
安全区域
使用安全点似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。
安全区域是指在一段代码片段之中,你用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。
当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。
垃圾收集器
Serial收集器
Serial是一个单线程的收集器,这表示其Serial只会使用一个CPU或者是一条收集线程进行垃圾回收的工作,同时需要注意的是它在进行回收工作是会停掉所有的其他工作线程(Stop the World),知道它的回收工作结束。
Serial虽然存在上面的问题,但是这并不表示它是一个无用的收集器,反而到目前为止Serial收集器在Client模式下被用在新生代的收集(64位虚拟机默认支持Server模式,并且无法切换;32位虚拟机可在Client和Server之间切换。正常情况下,Server模式启动较慢,但启动后性能远高于Client模式)。但是实际上使用也不多了。。。
Serial收集器的优点在于:在单CPU环境中,由于Serial由于没有线程的开销,专心做垃圾回收自然能获得极高的回收效率。
ParNew收集器
ParNew实际上就是一个多线程版的Serial收集器,除了多线程进行垃圾回收外其他都和Serial基本一致。
ParNew在很多运行于Server模式下的虚拟机中被用于新生代的首选。最大的原因在于目前为止只有其能CMS(Concurrent Mark Sweep)收集器配合使用。
并发与并行
- 并发:简单来讲是指一个处理器在同一时间间隔处理多个任务。放到垃圾回收这里指垃圾回收线程和用户工作线程同时进行工作。
- 并行:并行是指多个处理器在同一时刻处理多个任务。放到垃圾回收这里指多个垃圾回收线程同时工作,但是用户工作线程处于等待状态。
Parallel Scavenge收集器
Parallel Scavenge是一个新生代的收集器,同时它是一个并行的多线程的收集器,其使用复制算法。Parallel Scavenge的目标是达到一个可控制的吞吐量(throughput)。
吞吐量:CPU用于运行用户代码的时间和CPU总共运行时间的比值
吞吐量越高表示停顿时间短,程序响应速度快,CPU利用率越高。
Parallel Scavenge提供了几个用于精确控制吞吐量的参数:
- -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,该参数的值运行为一个大于0的毫秒数,虚拟机会尽量将垃圾回收花费的时间控制在设定的值之下。但是并不代表将每次垃圾回收的时间减小就一定是有利的,因为回收时间的降低是以降低吞吐量和新生代空间为代价的。为了保证每次垃圾回收的时间减小,就需要降低每一次垃圾回收的区域,所以需要减小新生代的大小。但在降低新生代大小的同时也增加了垃圾回收的次数。所以在设置该值是不能盲目的追求小的回收时间,而应该根据项目实际情况进行设置。
- -XX:GCTimeRatio:直接设置吞吐量大小。该值代表吞吐量比率,所以其应该是一个大于0并且小于100的数,这里还要求其为整数。
- -XX:UseAdaptiveSizePolicy:用于确定是否开启GC自适应的调整策略,如果打开该策略,就不需要手工指定新生代的大小(-Xmn)、Eden区和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代年龄(-XX:PretentureSizeThreshold)等参数。虚拟机会监控当前系统运行状态收集性能监控信息,自动的调整系统中垃圾回收停顿时间或者最大的吞吐量。该方式适合不太清楚如何设置上面那些参数的同学。
Serial Old
Serial Old是Serial的老年代版本,它也是一个单线程收集器,使用“标记-整理”算法。它的作用主要是两个:一个是搭配Parallel Scavenge收集器使用;另外就是当CMS收集器发生Concurrency Mode Failure时作为备用收集器。
Parallel Old
同Serial Old一样,Parallel Old是Parallel Scavenge的老年代版本。在注重吞吐量和CPU资源敏感的地方都可以优先考虑Parallel Old可以和Parallel Scavenge一起搭配使用。
CMS收集器
CMS(Concurrency Mark Sweep)是一个以获取最短回收停顿时间为目标的收集器,允许垃圾回收线程和用户工作线程同时运行。其使用“标记-清除”算法。目前来说例如淘宝等大型互联网企业都希望请求响应时间能尽量短,并且垃圾回收的停顿时间也尽量短,这种情况就可以使用CMS收集器。
CMS的“标记-清除”算法分为多个步骤:
- 初始标记:初始标记是用于标记直接与GC Roots关联的对象,不需要遍历下去,所需的时间很短。这一过程会发生STW(Stop the World)。
- 并发标记:并发标记就是遍历所有与GC Roots直接或者间接关联的对象。
- 重新标记:前面说过CMS允许垃圾回收线程和用户工作线程同时运行,所以这一过程是为了标记在前面标记过程中发生变动的对象。这一过程会发生STW(Stop the World)。
- 并发清除:清除掉那些没有被标记的对象。
其中并发标记和并发清除过程耗费时间最长,但是这两个阶段都可以并发进行,所以对用户的影响也不会太大。
虽然CMS确实是一款很不错的垃圾收集器,但是其也还有几个缺点:
- CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
- CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。同时为了保证在垃圾回收的同时用户线程也可以正常工作,所以不可能对整个区域进行回收,需要预留一部分区域给用户线程,如果在垃圾回收阶段,预留的垃圾回收区域不足,就可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
- CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
G1收集器
G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:
- 并行与并发:G1收集器能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器。从局部上来看是基于“复制”算法实现的。
- 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
在G1中Heap被分成一块块大小相等的region,Region的大小可以通过参数-XX:G1HeapRegionSize
设定,取值范围从1M到32M,且是2的指数。如果不指定,那么G1会根据Heap大小自动决定。保留新生代和老年代的概念,但它们不需要物理上的隔离。每块region都会被打唯一的分代标志(eden,survivor,old),代表一个分代类型的region可以是不连续的。eden regions构成Eden空间,survivor regions构成Survivor空间,old regions构成了old 空间。通过命令行参数-XX:NewRatio=n
来配置新生代与老年代的比例,n为整数,默认为2,即比例为2:1;-XX:SurvivorRatio=n
可以配置Eden与Survivor的比例,默认为8。
G1收集器进行回收大致可分为以下几个阶段:
- 初始标记:同CMS功能基本一致初始标记是用于标记直接与GC Roots关联的对象,不需要遍历下去,所需的时间很短。这一过程会发生STW(Stop the World)。
- 并发标记:并发标记就是遍历所有与GC Roots直接或者间接关联的对象。遍历整个堆寻找活跃对象,这个发生在应用运行时,这个阶段可以被年轻代垃圾回收打断。
- 重新标记:这一过程是为了标记在前面标记过程中发生变动的对象,和CMS的重新标记过程功能上基本保持一致。但是G1使用一个叫作snapshot-at-the-beginning(SATB)的比CMS收集器的更快的算法。
- 筛选回收:进行垃圾回收,G1保留了YGC并加上了一种全新的MIXGC用于收集老年代。G1中没有Full GC,G1中的Full GC是采用serial old的Full GC。
Young GC
当Eden空间不足时就会触发YGC。在G1中YGC也是采用复制存活对象到survivor空间,对于对象的存活年龄满足晋升条件时,把对象移到老年代。
在对新生代进行垃圾回收时,需要判断哪些对象能够会被回收。这里判断的方法也是采用可达性分析,标记与GC Roots直接或间接关联的对象。在CMS中使用了Card Table的结构,里面记录了老年代对象到新生代引用。G1也是使用这个思路,定义了一个新的数据结构:Remembered Set。在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。在进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
Full GC
full gc是指对包括新生代、老年代和方法区(元空间)等地区进行垃圾回收。
full gc的触发包括以下几种情况:
- 老年代空间不足
- 新生代对象晋升到老年代时,老年代剩余空间低于新生代晋升为老年代的速率,会触发老年代回收
- minor gc之后,survior区内存不足,将存活对象放入老年代,老年代也不足,触发Full GC。本质上还是老年代内存不足。
- System.gc().
理解GC日志
这里介绍一些打印出的gc日志的信息:
为了触发gc写一段代码,实际上也可以直接使用System.gc()
:
public class Test {
public static void main(String[] args) {
byte[] bytes1 = new byte[1024 * 1024];
byte[] bytes2 = new byte[1024 * 1024];
byte[] bytes3 = new byte[1024 * 1024];
byte[] bytes4 = new byte[1024 * 1024];
byte[] bytes5 = new byte[1024 * 1024];
}
public static void test(){
test();
}
}
要在控制台打印gc信息需要我们手动的配一些参数:
- -XX:+PrintGCDetails 输出GC的详细日志
- -XX:+PrintGCTimeStamps/PrintGCDateStamps 输出GC的时间戳
- -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- -Xloggc:../logs/gc.log 日志文件的输出路径
我这里使用Idea,直接在VM args配置即可:
现在运行上面的程序即可在控制台获得gc信息:
2019-01-24T20:08:25.811+0800: [GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.872+0800: [GC (Allocation Failure) [PSYoungGen: 1504K->488K(1536K)] 1624K->780K(5632K), 0.0016239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.879+0800: [GC (Allocation Failure) [PSYoungGen: 653K->504K(1536K)] 4017K->3940K(5632K), 0.0009844 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.880+0800: [GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 3940K->3948K(5632K), 0.0006796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.881+0800: [Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 3444K->3832K(4096K)] 3948K->3832K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0076471 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2019-01-24T20:08:25.888+0800: [GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 3832K->3832K(5632K), 0.0003390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.889+0800: [Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Test.main(Test.java:17)
[PSYoungGen: 0K->0K(1536K)] [ParOldGen: 3832K->3814K(4096K)] 3832K->3814K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0065960 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 1536K, used 65K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 6% used [0x00000000ffe00000,0x00000000ffe104d8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 4096K, used 3814K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 93% used [0x00000000ffa00000,0x00000000ffdb9a60,0x00000000ffe00000)
Metaspace used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 377K, capacity 388K, committed 512K, reserved 1048576K
上面的gc信息取一条分析:
[GC/Full GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
最前面的GC/FullGC表示gc类型,GC表示新生代gc(Minor GC),Full GC表示对新生代和老年代一起收集。
[PSYoungGen: 1019K->488K(1536K)]这个表示GC前该内存区域已使用容量-->GC后该内存区域已使用容量,后面圆括号里面的1536K为该内存区域的总容量。
紧跟着后面的1019K->608K(5632K),表示GC前Java堆已使用容量->GC后Java堆已使用容量,后面圆括号里面的5632K为Java堆总容量。
[Times: user=0.00 sys=0.00, real=0.00 secs]分别表示用户消耗的CPU时间,内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间,CPU时间和墙钟时间的差别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时。因为这里是测试在几乎一开始就发生了gc,并且设置的堆栈容量都较小,所以看不出时间。
PSYoungGen和ParOldGen分别代表新生代和老年代所使用的垃圾收集器。PSYoungGen表示Parallel Scavenge收集器,ParOldGen表示Parallel Old。要查看当前jvm使用那种收集器可以使用-XX:+PrintCommandLineFlags
,命令行下运行即可。
java -XX:PrintCommandLineFlags -version
-XX:InitialHeapSize=132485376 -XX:MaxHeapSize=2119766016 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
其中的-XX:+UseParallelGC表示使用Parallel Scavenge+Serial Old的组合,但是上面是Parallel Scavenge+parallel old的组合,这是为什么???
GC中的参数
这里有一篇不错的文章总结gc中的参数,比较详细:GC
内存分配与回收策略
对象内存的分配,一般是在堆上进行分配,但是随着JIT技术的发展,部分对象直接在栈上进行内存分配。
在前面的分代收集算法小节处,已经描述了jvm中的分代,将堆分为新生代和老年代。在描述内存分配前,我们先来了解下不同的GC类型:
- Minor GC:当Eden区可分配的内存不足以创建对象时就会触发一次Minor GC,Minor GC发生在新生代,由于新生代中大多数对象都是使用过后就不需要所以Minor GC的触发非常频繁。在Minor GC中存活下来的对象会被移到Survivor中,如果Survivor内存不够就直接移动到老年代。
- full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC)。或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。本内容来源于R大回答。
对象分配
在大多数情况下,对象的内存分配都优先在Eden中进行分配,当Eden区可分配的内存不足以创建对象时就会触发一次Minor GC。将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域。如Minor
GC时survivor空间不够,对象提前进入老年代,老年代空间不够时就进行Full GC。大对象直接进入老年代,避免在Eden区和Survivor区之间产生大量的内存复制,虚拟机提供了一个-XX:PretureSizeThreshold
参数,令大于这个值得对象直接进入老年代,但是该参数支队Serial和ParNew收集器有效。 此
外大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间。
这里大对象主要是指那种需要大量连续内存的java对象,比如大数组或者特别长的字符串等。
对象晋级
年龄阈值:虚拟机为每个对象定义了一个对象年龄(Age)计数器, 经第一次Minor GC后
仍然存活,被移动到Survivor空间中, 并将年龄设为1。以后对象在Survivor区中每熬
过一次Minor GC年龄就+1。 当增加到一定程度(默认
15),将会晋升到老年代(晋级的年龄可以通过-XX:MaxTenuringThreshold
进行设置)。
提前晋升: 动态年龄判定,如果在Survivor空间中相同年龄所有对象大小的总和大
于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代,而无
须等到晋升年龄。
空间分配担保
在前面说垃圾收集算法时关于复制对象有说过可能会存在存活下来的对象无法被survivor容纳,这时就需要老年代容纳无法被survivor容纳的对象。而如果老年代也没有足够的空间来存放这些对象的话就会触发一次Full GC。