jvm笔记2--垃圾收集器与内存分配策略

垃圾收集器与内存分配策略

Java运行时,内存的各个部分中,程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭:栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。因此这几个区域不需要过多考虑回收的问题,因为线程结束时,内存自然就跟着回收了。

Java堆和方法区不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的。

1对象死亡

1.引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

Java虚拟机里面没有选用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题,如:

public class ReferenceCountingGC {

	public Object instance = null;
	// 占点内存,方便观察内存回收
	private byte[] test = new byte[2 * 1024 * 1024];

	/**
	 * VMArgs: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
	 * -XX:+PrintGCDetails
	 *
	 * @param args
	 */
	public static void main(String[] args) {
		ReferenceCountingGC objA = new ReferenceCountingGC();
		ReferenceCountingGC objB = new ReferenceCountingGC();
		// 对象内相互引用
		objA.instance = objB;
		objB.instance = objA;

		objA = null;
		objB = null;
		System.gc();
	}
}
[Full GC (System) [Tenured: 0K->152K(13696K), 0.0097331 secs] <strong>4340K->152K</strong>(19840K), [Perm : 368K->368K(12288K)], 0.0097823 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
 def new generation   total 6144K, used 110K [0x33090000, 0x33730000, 0x33730000)
  eden space 5504K,   2% used [0x33090000, 0x330ab8b8, 0x335f0000)
  from space 640K,   0% used [0x335f0000, 0x335f0000, 0x33690000)
  to   space 640K,   0% used [0x33690000, 0x33690000, 0x33730000)
 tenured generation   total 13696K, used 152K [0x33730000, 0x34490000, 0x34490000)
   the space 13696K,   1% used [0x33730000, 0x33756270, 0x33756400, 0x34490000)
 compacting perm gen  total 12288K, used 368K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec3c0, 0x344ec400, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)

由结果可以看出,虚拟机并没有因为这两个对象互相引用就不回收它们,也就说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

2.可达性分析算法

Java语言的主流实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。基本思路就是通过一系列的称为"GC Roots"的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明这个对象不可用即可回收的对象。

Java中可以作为GC Roots的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

3.引用

定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。

Java将引用分为4种:强度一次逐渐减弱

    • 强引用(Strong Reference):
类似"Object  obj = new Object()"这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

  • 软引用(Soft Reference):
描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果回收后还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用(Weak Reference):
描述非必需对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾回收时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用(Phantom Reference):
也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

4.对象死亡

在可达性分析算法中不可达的对象,只是处于"缓刑"阶段。真正确定一个对象死亡,至少要经历两次标记过程:对象在进行可达性分析后发现没有与GC Roots相连接的引用链,将会被第一次标记。然后进行一次筛选,条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行"。

如果这个对象被判定为有必要执行finalize()方法,这个对象就会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的,低优先级的Finalizer线程去执行(由虚拟机触发),但并不一定会等待它运行结束。原因就是:如果对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,只要在finalize()中重新与引用链上的任何一个对象建立关联即可。否则稍后GC将对F-Queue中的对象进行第二次小规模的标记。如果对象这时候还没有逃脱,基本上就会被回收了。

public class FinalizeEscapeGC {
	public static FinalizeEscapeGC SAVEHOOK = null;

	public void isAlive() {
		System.out.println("alive!");
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("finalize method executed!");
		FinalizeEscapeGC.SAVEHOOK = this;// 在结束的时候将当前对象重新与引用链挂钩,可阻止对象被回收
	}

	public static void main(String[] args) throws InterruptedException {
		SAVEHOOK = new FinalizeEscapeGC();
		SAVEHOOK = null;// 引用制空,告诉GC对象可回收
		System.gc();
		// finalize方法执行优先级较低,虚暂停一会等其执行
		Thread.sleep(500);
		if (SAVEHOOK != null) {
			SAVEHOOK.isAlive();
		} else {
			System.out.println("died!");
		}

		// 第二次自救(不会再执行finalize()方法),自救失败
		SAVEHOOK = null;// 引用制空,告诉GC对象可回收
		System.gc();
		// finalize方法执行优先级较低,虚暂停一会等其执行
		Thread.sleep(500);
		if (SAVEHOOK != null) {
			SAVEHOOK.isAlive();
		} else {
			System.out.println("died!");
		}
	}
}

代码中两次进行对象回收,第一次对象成功逃脱,第二次则失败被回收。这是因为任何一个对象的finalize()方法都只会被系统自动调用一次。

完全不建议这么使用!

5.方法区回收(永久代)

在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

永久代的垃圾回收主要回收 废弃常量无用的类。如:一个字符串"abc"已经进入了常量池中,但是当前系统中没有任何一个String对象引用常量池中的"abc"常量,也没有其他地方引用了这个字面量,就可以被回收。常量池中的其他类(接口),方法,字段的符号引用也与此类似。

无用的类回收条件要苛刻许多:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

虚拟机对满足上述3个条件的无用类可以进行回收,但不是必然回收。

是否对类进行回收,HotSpot提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class及-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看类加载和卸载信息

在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景中需要虚拟机具备类卸载的功能。

2.垃圾收集算法

1.标记-清除算法

分标记和清除两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

不足:

  • 效率问题:标记和清除两个过程的效率都不高
  • 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致以后分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收。

2.复制算法

主要用来回收新生代。

解决效率问题。将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这块内存用完了,就将还存活的对象复制到另一块上面,然后将已使用过的内存空间一次清理掉。每次都是对整个半区进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。代价就是将内存缩小为了原来的一半。

实际引用中并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor大小比例是8:1。即每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被"浪费"。
当保存对象的Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)。

如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

3.标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。老年代一般不能直接选用这种算法。

根据老年代的特点,提出了"标记-整理"(Mark-Compact)算法,标记过程不变,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4.分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用"分代收集",根据对象存活周期的不同将内存划分为几块。一般将Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法 。新生代就选用 复制算法,老年代就使用"标记-清除"或"标记-整理"算法进行回收。

3.hotspot的算法实现

1.枚举根节点

以GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(常量或类静态属性)与执行上下文(栈帧中的本地变量表)中。

可达性分析为了确保分析结果的准确性,分析工作必须在一个能确保一致性的快照中进行。这就导致GC进行时必须停顿所有Java执行线程。

如何快速获取上下文和全局引用信息

目前的主流Java虚拟机使用的都是准确式GC,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机有办法直接得知哪些地方存放这对象引用。

在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内各个偏移量上的各种类型的数据计算出来了,在JIT编译过程总,也会在特定位置记录下栈和寄存器中哪些位置是引用。因此GC在扫描时就可以直接得知这些信息了。

2.安全点

在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举。但问题是如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。

实际上HotSpot并没有为每条指令都生成OopMap,而只是在特定位置记录这些信息,称为安全点(Safepoint)。程序执行时并非在所有地方都能停下来开始GC,只有在到达安全点时才能暂停。Safepoint选定既不能太少以致于让GC等待时间太长,也不能太频繁而过分增大运行时的负荷。

安全点的选定:以程序"是否具有让程序长时间执行的特征"为标准进行选定。

“长时间执行”的指令一般是指令序列复用,如:方法调用,循环跳转,异常跳转。具有这些功能的指令才会产生Safepoint。

如何在GC发生时让所有线程都跑到最近的安全点上再停顿下来?

1.抢先式中断(Preemptive Suspension)

GC发生时,先将所有线程中断,如果线程中断的地方不在安全点,就恢复线程,让其执行到安全点。(几乎没有虚拟机使用)

2.主动式中断(Voluntary Suspension)

当GC需要中断线程的时候,不直接对线程操作,只是简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志和安全点是重合的,另外再加上创建对象需要分配内存的地方。

3.安全区域(Safe Region)

Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。

如果程序不在执行的时候怎么处理?

如果线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求(执行到安全地方中断挂起),JVM也不太可能等待线程重新被分配CPU时间。

安全区域:指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,我们也可以把SafeRegion看成Safepoint的扩展。

在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那么,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,线程就继续执行,否则就必须等待直到收到可以安全离开Safe Region的信号为止。

4.垃圾收集器

Java内存回收的具体实现。HosSpot虚拟机实现:

1.serial收集器

Serial是最基本,发展历史最悠久的收集器(新生代收集器)。是一个单线程的收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop The World)。这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的。

优点:简单高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

2.parnew收集器

就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。如:控制参数(-XX:SurvivorRatio,-XX:PretenureSizeThreshold,-XX:HandlePromotionFailure等),收集算法,Stop The World,对象分配规则,回收策略等。

ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。有一个与性能无关但很重要的原因是,除了Serial收集器外,只有它能与CMS收集器配合工作。

ParNew收集器也是使用 -XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可以通过 -XX:ParallelGCThreads参数来限制垃圾收集的线程数。

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

3.parallel scavenge收集器

新生代收集器,使用复制算法,并行的多线程收集器。

Parallel  Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。

吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,垃圾收集1分钟,吞吐量就是99%.

高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

-XX:MaxGCPauseMillis:控制最大垃圾回收停顿的时间。值为大于0的毫秒数

收集器将尽可能地保证内存回收花费的时间不超过设定值。并不是参数越小就能使垃圾回收更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。停顿时间缩短反而增加垃圾回收的频率。

-XX:GCTimeRatio:设置吞吐量大小。值为大于0小于100的整数,就是垃圾回收时间占总时间的比率(吞吐量的倒数)。

如果参数设置为19,允许的最大GC时间就占总时间的5%(即,1/(1+19)),默认是99,允许最大1%(1/(1+99))的垃圾回收时间

Parallel Scavenge收集器也称为"吞吐量优先"收集器

-XX:+UseAdaptiveSizePolicy:这是一个开关参数,不需要手工指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRation),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,称为GC自适应的调节策略(GC
Ergonomics)。

如果手工优化存在困难时,可以使用自适应调节策略。只需设置基本参数(如-Xmx),然后使用MaxGCPauseMillis或GCTimeRatio参数给虚拟机设立一个优化目标,具体细节参数调节工作交给虚拟机。

4.serial old收集器

是Serial收集器的老年代版本,同样是一个单线程收集器,使用"标记-整理"算法。

5.parallel old收集器

是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法。

在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Savenge加Parallel Old收集器。

6.cms收集器(Concurrent Mark Sweep)

老年代收集器,以获取最短回收停顿时间为目标的收集器。适合重视服务响应速度,希望停顿时间短的应用。是基于“标记-清除”算法实现的。

整体运作过程分一下4步:

  • 初始标记(CMS initial mark)
"Stop The World",标记下GC Roots能直接关联到的对象。

  • 并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程

  • 重新标记(CMS remark)
"Stop The World",修正并发标记期间因用户程序继续运作而导致标记产生变动的哪一部分对象的标记记录,这个阶段停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

  • 并发清除(CMS concurrent  sweep)

由于整个过程中耗时最长的并发标记和并发清除过程都可以和用户线程一起工作,所以总体上说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

缺点:

  • CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感,在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,即CPU在4个以上时,并发回收线程不少于25%的资源。
  • 浮动垃圾(Floating Garbage):CMS并发清理阶段,用户线程新产生的垃圾,CMS无法立刻处理,留待下一次GC时再清理。
CMS收集器由于无法处理浮动垃圾,可能出现"Concurrent Mode Faile"失败而导致另一次Full GC的产生。由于垃圾回收时用户线程还在运行,那么就还需要预留内存空间给用户线程使用,因此CMS不能像其他收集器那样等到老年代几乎完全被填满了再回收。

如果CMS运行期间预留的内存无法满足程序需要,就会出现"Concurrent Mode Failure"失败。

JDK1.6中,老年代使用92%后会启动回收(-XX:CMSInitiatingOccupancyFraction 参数进行设置)。

  • CMS基于“标记--清除”算法实现,回收结束会产生大量内存碎片,碎片过多时,会给大对象分配照成影响。现象:老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
-XX:UseCMSCompactAtFullCollection 开关参数(默认开启),在CMS收集器顶不住要FullGC时开启内存碎片合并整理过程,由于内存整理过程无法并发,停顿时间就会变长

-XX:CMSFullGCsBeforeCompaction,设置每隔多少次Full GC,进行一次压缩(默认为0,表示每次都进行整理)。

7.g1收集器(Garbage-First)

G1是一款面向服务端应用的垃圾收集器,特点如下:

  • 并行与并发:G1能充分多CPU,多核环境下的硬件优势,使用多CPU来缩短Stop-The-World停顿时间。
  • 分代收集:G1可以独立管理整个GC堆,能够采用不同方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的回收效果。
  • 空间整合:G1从整体看是基于"标记--整理",从局部(两个Region之间)上来看,是基于“复制”算法实现的。从而能够保证G1运行期间不会产生内存碎片。
  • 可预测的停顿:G1相对于CMS的另一大优势,G1建立了可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

将整个Java堆划分为多个大小相等的区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离了,都是Region(不一定连续)的集合。

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(Garbage-First名称由来)。这种方式保证了G1收集器在有限的时间内可以获取尽可能高的回收效率。

问题:

一个对象分配在某个Region中,并非只能被本Region中的对象引用,而是可以与整个Java堆任意的对象发生引用。那么在做可达性判断对象是否存活的时候,就得扫描整个Java堆。

解决:

Region之间的对象引用以及其他收集器新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都对应一个Remembered Set。当虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(分代例子中就是检查是否对象跨代引用了),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remember
Set之中。当进行回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

G1操作步骤:

  • 初始标记(Initial Marking)
标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,需要停顿线程,耗时短。

  • 并发标记(Concurrent Marking)
从GC Root开始对堆中对象进行可达性分析,找出存活的对象,耗时较长,可与用户程序并发执行。

  • 最终标记(Final Marking)
修正在并发标记期间用户进程继续运作导致的引用变动,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,然后将Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行

  • 筛选回收(Live Data Counting and Evacuation)
对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划

8.gc日志

每种收集器的日志形式都由自身的实现所决定,格式都可以不一样。但是虚拟机将各个收集器的日志到维持了一定的共性。

9.垃圾收集器参数总结

  • UseSerialGC  : 虚拟机运行在Client模式下的默认值,打开此开关后,Serial+Serial Old的收集器组合进行内存回收。
  • UseParNewGC : 打开此开关后,使用ParNew+Serial Old 的收集器组合进行内存回收。
  • UseConcMarkSweepGC  :使用ParNew+CMS+Serial Old的收集器组合进行内存回收。Serial Old作为CMS出现Concurrent Mode Failure失败后的备用收集器。
  • UseParallelGC  : 虚拟机运行在Server模式下的默认值,使用Parallel Scavenge + Serial Old (PS MarkSweep)的组合进行回收。
  • UseParallelOldGC  : 使用Parallel Scavenge+Parallel Old的收集器组合进行内存回收
  • SurvivorRation  : 新生代中Eden区域与Survivor区域的容量比值,默认8
  • PretenureSizeThreshold  : 直接晋升到老年代的对象大小,设置后,大于这个参数的对象将直接在老年代分配。
  • MaxTenuringThreshold  : 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC(频繁发生在新生代的GC)后,年龄就+1,当超过这个参数时就进入老年代。
  • UseAdaptiveSizePolicy  : 动态调整Java堆中各个区域的大小以及进入老年代的年龄
  • HandlePromotionFailure  : 是否允许分配担保失败--老年代剩余空间无法满足 Eden和Survivor区对象存活太多的情况。
  • ParallelGCThreads  : 设置并行GC时进行内存回收的线程数。
  • GCTimeRatio  : GC时间占总时间的比率,默认99,即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效。
  • MaxGCPauseMillis  : 设置GC最大停顿时间,仅在使用Parallel Scavenge收集器时生效
  • CMSInitiatingOccupancyFraction  : 设置CMS收集器在老年代空间被是使用多少后触发垃圾回收。默认68%。仅在使用CMS收集器时有效
  • UseCMSCompactAtFullCollection  : 设置CMS收集器在完成垃圾回收后是否进行内存整理
  • CMSFullGCsBeforeCompaction  : CMS收集器在进行多少次回收后进行一次内存碎片整理。

5.内存分配与回收策略

Java技术体系所提倡的自动内存管理主要解决两个问题:给对象分配内存和回收分配给对象的内存。

对象内存分配主要是在堆上分配,主要分配在新生代的Eden区,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况可能会直接分配在老年代中,分配规则不是固定的。具体取决于使用的哪种垃圾回收器组合,和虚拟机中与内存相关的参数设置。

1.对象优先在eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

通过 -XX:+PrintGCDetails 打印回收日期,进行查看。

	/**
	 * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
	 * -XX:SurvivorRatio=8 新生代大小 Eden与Survivor区大小比
	 */
	public static void testAllocation() {
		byte[] allocation1, allocation2, allocation3, allocation4;
		allocation1 = new byte[2 * _1MB];
		allocation2 = new byte[2 * _1MB];
		allocation3 = new byte[2 * _1MB];
		allocation4 = new byte[4 * _1MB];// 出现一次Minor GC
	}

GC日志:

[GC [DefNew: <strong>6487K->152K(9216K)</strong>, 0.0066114 secs]<strong> 6487K->6296K(19456K)</strong>, 0.0066516 secs] [Times: user=0.00 sys=0.02, real=0.01 secs]
Heap
 def new generation   total 9216K, used 4412K [0x33090000, 0x33a90000, 0x33a90000)
 <strong> eden space 8192K,</strong>  52% used [0x33090000, 0x334b8fd8, 0x33890000)
<strong>  from space 1024K,</strong>  14% used [0x33990000, 0x339b6270, 0x33a90000)
 <strong> to   space 1024K, </strong>  0% used [0x33890000, 0x33890000, 0x33990000)
 tenured generation   total 10240K, used 6144K [0x33a90000, 0x34490000, 0x34490000)
   <strong>the space 10240K</strong>,  60% used [0x33a90000, 0x34090030, 0x34090200, 0x34490000)
 compacting perm gen  total 12288K, used 369K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec460, 0x344ec600, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)

从上面信息可以看出:Eden区是  8192K(8M),from space(survivor1) 1024k(1M),to space(survivor2)1024k。

在声明前三个变量(allocation1,allocation2,allocation3)时,占用Eden区6M内存,在声明allocation4时,剩余的空间不足4MB内存,所以发生了Minor GC。GC期间发现3个2MB大小的对象无法放入Survivor空间(只有1MB大小),只好通过分配担保机制转移到老年代(可以看到 DefNew:6487K -> 152K(9216K)的GC日志信息)。总占用内存8487K ->6296K(19456K)几乎没减,因为allocation1,allocation2,allocation3变量都还存活。

GC结束后,给allocation4变量分配内存,在后面的堆内存快照中可以看出:Eden区使用了 4412K即4M,老年代10240k(10M)使用了6144k(4M)。

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,回收速度也比较快
  • 老年代GC(Major GC/Full GC):指发生在老年大的GC,出现了Major GC,经常会伴随至少一次的Minor GC(非绝对,在Parallel Scavenge回收策略中有直接进行Major GC的策略选择过程)。Major GC 的速度比 Minor GC慢10倍以上。

2.大对象直接进入老年代

大对象指,需要大量连续内存空间的Java对象,如字符串及数组(例子中的byte[]数组)。经常出现大对象容易导致内存还有不少空间就得提前触发垃圾回收来获取足够的连续空间来进行分配。

-XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。避免Eden区及两个Survivor区之间发生大量的内存复制。

	/**
	 * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * 			<span style="white-space:pre">		</span>新生代大小 		Eden与Survivor区大小比
	 *  -XX:PretenureSizeThreshold=3145728
	 */
	public static void testPretenureSizeThreshold(){
		byte[] allocation;
		allocation = new byte[4*_1MB]; //直接分配在老年代中
	}

日志:

Heap
 def new generation   total 9216K, used 507K [0x33090000, 0x33a90000, 0x33a90000)
  eden space 8192K,   6% used [0x33090000, 0x3310eee0, 0x33890000)
  from space 1024K,   0% used [0x33890000, 0x33890000, 0x33990000)
  to   space 1024K,   0% used [0x33990000, 0x33990000, 0x33a90000)
 tenured generation   total 10240K, used 4096K [0x33a90000, 0x34490000, 0x34490000)
   the space 10240K,  40% used [0x33a90000, 0x33e90010, 0x33e90200, 0x34490000)
 compacting perm gen  total 12288K, used 369K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec550, 0x344ec600, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)

Eden区几乎没有使用,老年代直接使用了4M空间,因为参数的原因,3M以上的对象都会直接在老年代中进行分配。

3.长期存活的对象将进入老年代

虚拟机采用分代回收的思想来管理内存,那么就得判断对象是放在新生代还是老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。

如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间,并且对象年龄设为1。此后对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。

-XX:MaxTenuringThreshold   设置年龄阀值。

	/**
	 * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * 		    -XX:MaxTenuringThreshold=1
	 * 			-XX:+PrintTenuringDistribution
	 */
	public static void testTenuringThreshold(){
		byte[] allocation1,allocation2,allocation3;
		allocation1 = new byte[_1MB / 4];
		//什么时候进入老年代取决于XX:MaxTenuringThreshold设置
		allocation2= new byte[4*_1MB];
		allocation3 = new byte[4*_1MB];
		allocation3 = null;
		allocation3 = new byte[4*_1MB];
	}

日志:

4.动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

	/**
	 * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * 		    -XX:MaxTenuringThreshold=15
	 * 			-XX:+PrintTenuringDistribution
	 */
	public static void testTenuringThreshold2(){
		// allocation1 + allocation2 > Survivor/2
		byte[] allocation1 = new byte[_1MB / 4];
		byte[] allocation2 = new byte[_1MB / 4];

		byte[] allocation3 = new byte[_1MB * 4];
		allocation3 =null;
		allocation3 = new byte[_1MB * 4];
		byte[] allocation4 = new byte[_1MB * 4];

	}

日志:

5.空间分配担保

在Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置是否允许担保失败。如果允许,就继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full
GC。

风险:

新生代使用复制收集算法,当Minor GC后仍然存活大量对象,Survivor无法容纳时,就需要老年代进行分配担保,让对象直接进入老年代。

前提条件: 老年代本身有剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以需要取之前每次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值,就会导致担保失败(Handle Promotion Failure)。虽然担保失败会绕大圈,但大部分情况下还是会开启HandlePromotionFailure,避免频繁Full  GC。

	/**
	 * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * 		   -XX:-HandlePromotionFailure   不开启
	 * 		   -XX:+HandlePromotionFailure   开启
	 */
	public static void testHandlePromotion(){
		byte[] allocation1,allocation2,allocation3,allocation4,allocation5,allocation6,allocation7;
		allocation1 = new byte[2*_1MB];
		allocation2 = new byte[2*_1MB];
		allocation3 = new byte[2*_1MB];
		allocation1 = null;
		allocation4 = new byte[2*_1MB];
		allocation5 = new byte[2*_1MB];
		allocation6 = new byte[2*_1MB];
		allocation4 = null;
		allocation5 = null;
		allocation6 = null;
		allocation7 = new byte[2*_1MB];
	}

不开启分配担保,日志:

[GC [DefNew: 6487K->152K(9216K), 0.0043054 secs] 6487K->4248K(19456K), 0.0043431 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC [DefNew:<strong> 6382K->6382K(9216K)</strong>, 0.0000188 secs][Tenured: 4096K->4248K(10240K), 0.0085509 secs] 10478K->4248K(19456K), [Perm : 370K->370K(12288K)], 0.0086249 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
//Full GC
Heap
 def new generation   total 9216K, used 2211K [0x33090000, 0x33a90000, 0x33a90000)
  eden space 8192K,  27% used [0x33090000, 0x332b8fd8, 0x33890000)
  from space 1024K,   0% used [0x33990000, 0x33990000, 0x33a90000)
  to   space 1024K,   0% used [0x33890000, 0x33890000, 0x33990000)
 tenured generation   total 10240K, used 4248K [0x33a90000, 0x34490000, 0x34490000)
   the space 10240K,  41% used [0x33a90000, 0x33eb6200, 0x33eb6200, 0x34490000)
 compacting perm gen  total 12288K, used 370K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec8c8, 0x344eca00, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)

开启分配担保,日志:

[GC [DefNew: 6487K->152K(9216K), 0.0043572 secs] 6487K->4248K(19456K), 0.0043931 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew<strong>: 6382K->152K(9216K)</strong>, 0.0006780 secs] 10478K->4248K(19456K), 0.0007148 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //Minor GC
Heap
 def new generation   total 9216K, used 2364K [0x33090000, 0x33a90000, 0x33a90000)
  eden space 8192K,  27% used [0x33090000, 0x332b8fd8, 0x33890000)
  from space 1024K,  14% used [0x33890000, 0x338b61e0, 0x33990000)
  to   space 1024K,   0% used [0x33990000, 0x33990000, 0x33a90000)
 tenured generation   total 10240K, used 4096K [0x33a90000, 0x34490000, 0x34490000)
   the space 10240K,  40% used [0x33a90000, 0x33e90020, 0x33e90200, 0x34490000)
 compacting perm gen  total 12288K, used 370K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec8c8, 0x344eca00, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)

JDK6 Update 24之后,只要老年代连续空间大于新生代对象总大小或历次晋升平均大小,就会进行Minor GC,否则进行Full GC。

时间: 2024-10-12 21:06:31

jvm笔记2--垃圾收集器与内存分配策略的相关文章

jvm系列 (二) ---垃圾收集器与内存分配策略

回顾 上文介绍了jvm的内存区域以及介绍了内存的溢出情况. jvm区域分为5个,线程独有:虚拟机栈,本地方法栈,程序计数器.线程共享:方法区,堆 两种溢出:栈溢出(StackOverflowError),OutOfMemoryError(OOM) 为什么学习垃圾收集 看起来jvm好像一切帮你做好,但是当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这种自动化的技术进行监控和调节. 根据实际应用需求,选择最优的收集方式才能更高的性能. 垃圾收集的区域 虚拟机栈,本地方法栈,程序计数器是线程私

Java虚拟机垃圾收集器与内存分配策略

Java虚拟机垃圾收集器与内存分配策略 概述 那些内存需要回收,什么时候回收,如何回收是GC需要完成的3件事情. 程序计数器,虚拟机栈与本地方法栈这三个区域都是线程私有的,内存的分配与回收都具有确定性,内存随着方法结束或者线程结束就回收了. java堆与方法区在运行期才知道创建那些对象,这部分内存分配是动态的,本章笔记中分配与回收的内存指的就是:java堆与方法区. 判断对象已经死了 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它,计数器+1;引用失败,计数器-1.计数器为0则改判

垃圾收集器以及内存分配策略

垃圾回收 垃圾回收的三个问题: 哪些内存需要回收? 什么时候回收? 如何回收? 1.哪些对象需要回收? 判断对象是否存活的办法: 引用计数算法:给对象中添加一个引用计数器,有一个地方引用就+1,引用失效就-1.只要计数器为0则对象已死. 优点:简单易实现: 缺点:无法解决对象之间相互引用的问题.(JVM也因为此种原因没有使用它) 根搜索算法: 通过选取出一个GC Roots对象,已它作为起始点,如果对象不可达,则对象已死. GC Roots对象: 虚拟机栈中引用的对象 方法区中类静态属性引用的对

垃圾收集器与内存分配策略(三)之HotSpot的算法实现

垃圾收集器与内存分配策略(三)--HotSpot的算法实现 Java JVM 垃圾回收 在HotSpot虚拟机上实现这些算法时,必须对算法的执行效率有着严格的考量,才能保证虚拟机高效地运行. 1. 枚举根节点 采用可达性分析从GC Roots节点中找引用链为例 存在的缺点: 1.在前面找出还存活对象时,采用可达性分析从GC Roots节点中找引用链时,可作为GC Roots的节点主要在全局性的引用(方法区的常量或类静态属性引用)与执行上下文(虚拟机栈栈帧中的本地变量表或本地方法栈中的Native

垃圾收集器与内存分配策略(五)之垃圾日志与常见参数

垃圾收集器与内存分配策略(五)--垃圾日志与常见参数 理解GC日志 每个收集器的日志格式都可以不一样,但各个每个收集器的日志都维持一定的共性.如下面二段日志: 33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs] 100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->

垃圾收集器与内存分配策略(二)之垃圾收集算法

垃圾收集器与内存分配策略(二)--垃圾收集算法 Java JVM 垃圾回收 简单了解算法的思想 1. 标记-清除算法 标记-清除算法分为标记和清除二个阶段:首先标记出需要回收的对象(详见上一节的可达性分析找出存活对象),在标记完成后统一回收所有被标记的对象. 缺点: 1.标记和清除二个过程的效率都不高 2.空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作. 2. 复制算法 复制算

深入理解java虚拟机-----&gt;垃圾收集器与内存分配策略(下)

1.  前言 内存分配与回收策略 JVM堆的结构分析(新生代.老年代.永久代) 对象优先在Eden分配 大对象直接进入老年代 长期存活的对象将进入老年代 动态对象年龄判定 空间分配担保  2.  垃圾收集器与内存分配策略 Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决两个问题: 给对象分配内存; 回收分配给对象的内存. 对象的内存分配,往大方向上讲就是在堆上的分配,对象主要分配在新生代的Eden区上.少数也可能分配在老年代,取决于哪一种垃圾收集器组合,还有虚拟机中的相关内存的参

第三章 垃圾收集器和内存分配策略

第三章 垃圾收集器和内存分配策略 对象已死吗 引用计算方法 可达性分析算法 通过一些列的GC roots 对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC roots 没有任何引用链的则证明对象不可用的 虚拟机栈中的引用的对象 方法区中类静态属性引用的对象 方法去区中常量引用的对象 本地方法栈中JNI引用的对象 生存还是死亡 一次筛选,筛选是否有必要执行 finalize()方法 没有覆盖或者finalize()已经被调用过  视为没必要执行 放入一个F-Qu

垃圾收集器与内存分配策略(二)

垃圾收集算法简介 1.标记-清除算法       标记-清除算法主要分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一进行回收.对象的标记过程在垃圾收集器与内存分配策略(一)中已经介绍过. 存在的问题:一是效率问题,标记和清除的效率都不高:二是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的内存而不得不提前触发另一次垃圾收集动作. 2.复制算法       复制算法:它将内存按照容量划分为大小

垃圾收集器与内存分配策略(四)之垃圾收集器

垃圾收集器与内存分配策略(四)--垃圾收集器 收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现. 垃圾收集器介绍 在垃圾收集器的层面上对并行与并发的解释: 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户现场仍处于等待状态. 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能会交替执行),用户程序仍在继续执行,而垃圾收集程序运行于另一个CPU上. 对于不同的厂商,不同的版本的虚拟机都可能有很大的差别.此处讨论的是jdk1.7之后的