龙空技术网

五万字长篇-java虚拟机看这篇文章就够了(中)

闪亮旅游 102

前言:

而今兄弟们对“c语言编写虚拟机程序代码”大约比较注意,看官们都想要剖析一些“c语言编写虚拟机程序代码”的相关文章。那么小编在网上搜集了一些对于“c语言编写虚拟机程序代码””的相关知识,希望大家能喜欢,咱们快快来学习一下吧!

java虚拟机

垃圾回收算法

上一节讨论了垃圾回收的基本概念,应该已经知道什么是垃圾回收和为什么要进行自动化的垃圾回收了。本节将进一步步讨论实现垃圾回收的方法,主要内容是理解Java垃圾回收机制的理论基础,将主要讨论:引用计数法、标记压缩法、标记清除法、复制算法和分代、分区的思想。

垃圾回收的思想

垃圾回收的基本思想是考察每一个对象的可触及性,即从根节点开始是否可以访问到这个对象,如果可以,则说明当前对象正在被使用,如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。

引用计数法

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。引用计数器的实现也非常简单,只需要为每个对象配备一 个整型的计数器即可。

但是,引用计数器有两个非常严重的问题:

(1)无法处理循环引用的情况。因此,在Java的垃圾回收器中,没有使用这种算法。

(2)引用计算器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

一个简单的循环引用问题描述如下:有对象A和对象B, 对象A中含有对象B 的引用,对象B中含有对象A的引用。此时,对象A和B的引用计数器都不为0。但是,在系统中,却不存在任何第3个对象引用了A或B。也就是说,A和B是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起内存泄漏。

如图所示,不可达的对象出现循环引用,它的引用计数器均不为0。

标记清除法

标记清除算法是现代垃圾回收算法的思想基础。标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记清除算法可能产生的最大问题是空间碎片。

如图所示,使用标记清除算法对一块连续的内存空间进行回收。从根节点开始(这里

显示了2个根),所有的有引用关系的对象均被标记为存活对象(箭头表示引用)。从根节点起,不可达的对象均为垃圾对象。在标记操作完成后,系统回收所有不可达的空间。

缺点:回收后的空间是不连续的。在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续内存空间的工作效率要低于连续的空间。因此,这也是该算法的最大缺点。

复制算法

复制算法的核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

优点:

如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量就会相对较少。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。又由于对象是在垃圾回收过程中,统一被复制到新的内存空间中的,因此,可确保回收后的内存空间是没有碎片的。

缺点:

虽然有以上两大优点,但是,复制算法的代价却是将系统内存折半,因此,单纯的复制算法也很难让人接受。

如图所示,A.B两块相同的内存空间,A在进行垃圾回收时,将存活对象复制到B中,

B中的空间在复制后保持连续。复制完成后,清空A。并将空间B设置为当前使用空间。

在Java的新生代串行垃圾回收器中,使用了复制算法的思想。新生代分为eden空间、from空间和to空间3个部分。其中from和to空间可以视为用于复制的两块大小相同、地位相等、且可进行角色互换的空间块。from和to空间也称为survivor空间,即幸存者空间,用于存放未被回收的对象。如图所示。

在垃圾回收时,eden空间中的存活对象会被复制到未使用的survivor空间中(假设是to),

正在使用的survivor空间(假设是from)中的年轻对象也会被复制到to空间中(大对象,或者老年对象会直接进入老年代,如果to空间已满,则对象也会直接进入老年代)。此时,eden 空间和from空间中的剩余对象就是垃圾对象,可以直接清空,to空间则存放此次回收后的存活对象。这种改进的复制算法,既保证了空间的连续性,又避免了大量的内存空间浪费。如图所示,显示了复制算法的实际回收过程。当所有存活对象都复制到survivor区后(图中为to),简单地清空eden区和备用的survivor区(图中为from)即可。

注意:复制算法比较适用于新生代。因为在新生代,垃圾对象通常会多于存活对象。复制算法的效果会比较好。

标记压缩法

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生。

但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。

标记压缩算法是一种老年代的回收算法。它在标记清除算法的基础上做了一些优化。和标记清除算法一样,标记压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不只是简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比较高。如图所示,在通过根节点标记出所有可达对象后,沿虚线进行对象移动,将所有的可达对象都移动到一一端,并保持它们之间的引用关系,最后,清理边界外的空间,即

可完成回收工作。

标记压缩算法的最终效果等同于标记清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记清除压缩(MarkSweepCompact)算法。

分代算法

前文中介绍了复制、标记清除、标记压缩等垃圾回收算法。在所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。因此,根据垃圾回收对象的特性,使用合适的算法回收,才是明智的选择。

分代算法就是基于这种思想,它将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。

一般来说,Java 虛拟机会将所有的新建对象都放入称为新生代的内存区域,新生代的特点是对象朝生夕灭,大约90%的新建对象会被很快回收,因此,新生代比较适合使用复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老年代的内存空间。在老年代中,几乎所有的对象都是经过几次垃圾回收后依然得以存活的。因此,可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。在极端情况下,老年代对象的存活率可以达到100%。如果依然使用复制算法回收老年代,将需要复制大量对象。再加上老年代的回收性价比也要低于新生代,因此这种做法是不可取的。

根据分代的思想,可以对老年代的回收使用与新生代不同的标记压缩或标记清除算法,以提高

垃圾回收效率。如图 所示,显示了这种分代回收的思想。

对于新生代和老年代来说,通常,新生代回收的频率很高,但是每次回收的耗时都很短,

而老年代回收的频率比较低,但是会消耗更多的时间。为了支持高频率的新生代回收,虚拟机可能使用一种叫作卡表的数据结构。卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用。

这样在新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对 象的引用关系,而可以先扫描卡表,只有当卡表的标记位为1时,才需要扫描给定区域的老年代对象,而卡表位为0的所在区域的老年代对象,一定不含有新生代对象的引用。如图所示,卡表中每一位表示老年代4KB的空间,卡表记录为0的老年代区域没有任何对象指向新生代,只有卡表位为1的区域才有对象包含新生代引用,因此在新生代GC时,只需要扫描卡表位为1所在的老年代空间。使用这种方式,可以大大加快新生代的回收速度。

分区算法

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间,如图所示。每一个小区间都独立使用,独立回收。这种算法的好处是可以

控制一次回收多少个小区间。

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,从而产生的停顿也越长,有关GC产生的停顿后面会讲。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

谁是真正的垃圾对象复活

所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。但事实上,一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,需要给出一个对象可触及性状态的定义,并规定在什么状态下,才可以安全地回收对象。

简单来说,可触及性可以包含以下3种状态。

●可触及的:从根节点开始,可以到达这个对象。

●可复活的:对象的所有引用都被释放,但是对象有可能在finalize()函数中复活。

●不可触及的:对象的finalize()函数被调用,并且没有复活,那么就会进入不可触及状态,

不可触及的对象不可能被复活,因为finalize()函数只会被调用一次。

以上3种状态中,只有在对象不可触及时才可以被回收。

案例:finalize()方法复活对象

public class CanReliveObj {

public static CanReliveObj obj;

@Override

protected void finalize() throws Throwable {

super.finalize();

System.out.println("CanRelive0bj finalize called");

obj = this;

}

@Override

public String toString() {

return "I am CanRelive0bj";

}

public static void main(String[] args) throws InterruptedException {

obj = new CanReliveObj();

obj = null;

System.gc();

Thread.sleep(1000);

if (obj == null) {

System.out.println("obj是null");

} else {

System.out.println("obj可用");

System.out.println("第2次gc");

obj = null;

System.gc();

Thread.sleep(1000);

if (obj == null) {

System.out.println("obj是null");

} else {

System.out.println("obj可用");

}

}

}

}

运行结果

代码分析

可以看到,在代码第20行将obj设置为null后,进行GC,结果发现obj对象被复活了。

第28行,再次释放对象引用并运行GC,对象才真正地被回收。这是因为第一次GC时,在finalize()函数调用之前,虽然系统中的引用已经被清除,但是作为实例方法finalize()对象的this 引用依然会被传入方法内部,如果引用外泄,对象就会复活,此时,对象又变为可触及状态。而finalize()函数只会被调用一次,因此,第2次清除对象时,对象就再无机会复活,因此就会被回收。

注意: finalize()函数是一 个非常不推荐的模式,再次不推荐读者使用finalize()函数释放

资源。

第一,因为finalize()函数有可能发生引用外泄,在无意中复活对象;

第二,由于finalize()是被系统调用的,调用时间是不明确的,因此不是一个好的资源

释放方案。

引用和可触及性的强度

在Java中提供了4个级别的引用:强引用、软引用、弱引用和虚引用。除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。如图所示,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。其中FinalReference意味“最终”引用,它用以实现对象的finalize()方法, 后面会说。

强引用

强引用就是程序中一般使用的引用类型,强引用的对象是可触及的,不会被回收。相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虛可触及的,在一定条件下,都是可以被回收的。

示例:

StringBuffer str = new StringBuffer("hello world");

假设以上代码是在函数体内运行的,那么局部变量str将被分配在栈上,而对象StringBuffer实例被分配在堆上。局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是StringBuffer实例的强引用,如图所示。

再加一条语句:

StringBuffer str1 = str;

那么,str所指向的对象也将被str1所指向,同时在局部变量表上会分配空间存放strl变量,如图所示。此时,该StringBuffer实例就有两个引用。对引用的“=="操作用于表示两操作数所指向的堆空间地址是否相同。

如本例中的两个引用,都是强引用,强引用具备以下特点:

●强引用可以直接访问目标对象。

●强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿拋出OOM异常,也不会回收强引用所指向对象。

●强引用可能导致内存泄漏。

软引用-可被回收

软引用是比强引用弱一点的引用类型。一个对象只持有软引用,那么当堆空间不足时,就会被回收。软引用使用java.lang.ref.SoftReference类实现。

下面示例演示了软引用会在系统堆内存不足时被回收。

public class SoftRef {

public static class User {

public int id;

public String name;

public User(int id, String name) {

this.id = id;

this.name = name;

}

@Override

public String toString() {

return " [id=" + String.valueOf(id) + ", name=" + name + "]";

}

}

public static void main(String[] args) {

User u = new User(1, "geym");

SoftReference<User> userSoftRef = new SoftReference<User>(u);

u = null;

System.out.println(userSoftRef.get());

System.gc();

System.out.println("After GC:");

System.out.println(userSoftRef.get());

byte[] b = new byte[1024 * 935 * 7];

System.gc();

System.out.println(userSoftRef.get());

}

}

代码分析

上述代码申明了一个User类,

在代码第22行,建立了User类的实例,这里的u变量为强引用。代码第23行,通过强引用u建立软引用。

代码第24行,去除强引用。

代码第25行从软引用中重新获得强引用对象。

代码第26行进行一次垃圾回收,

代码第28 行,在垃圾回收之后,在此获得软引用中的对象。

代码第29行,分配一块较大的内存,让系统认为内存资源紧张,

代码第30行进行一次GC (实际上,这个是多余的,因为在分配大数据时,系统会自动进行GC,这里只是为了更清楚地说明问题),

代码第31行再次从软引用中获取数据。

使用参数-Xmx10m运行上述代码,得到:

因此,从该示例中可以得到结论: GC未必会回收软引用的对象,但是,当内存资源紧张时,软引用对象会被回收,所以软引用对象不会引起内存溢出。

弱引用

弱引用是一种比软引用较弱的引用类型。在系统GC时,只要发现弱引用,不管系统堆空间使用情况如何,都会将对象进行回收。但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。

看下面的例子

public class WeekRef {

public static class User {

public int id;

public String name;

public User(int id, String name) {

this.id = id;

this.name = name;

}

@Override

public String toString() {

return " [id=" + String.valueOf(id) + ", name=" + name + "]";

}

}

public static void main(String[] args) {

User u = new User(1, "geym");

WeakReference<User> userWeakRef = new WeakReference<User>(u);

u = null;

System.out.println(userWeakRef.get());

System.gc();

//不管当前内存空间足够与否,都会回收它的内存

System.out.println("After GC:");

System.out.println(userWeakRef.get());

}

}

上述

代码第24行,构造了弱引用。

代码25行,去除了强引用。

代码第26行从弱引用中重新获取对象。

第27行进行GC,第30行重新尝试从弱引用中获取对象。

运行上述代码,输出为:

注意:软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当

系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源

充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

虚引用

虚引用是所有引用类型中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

public class TraceCanReliveObj {

public static TraceCanReliveObj obj;

static ReferenceQueue<TraceCanReliveObj> phantomQueue = null;

public static class CheckRefQueue extends Thread {

@Override

public void run() {

while (true) {

if (phantomQueue != null) {

PhantomReference<TraceCanReliveObj> objt = null;

try {

objt = (PhantomReference<TraceCanReliveObj>) phantomQueue.remove();

} catch (InterruptedException e) {

e.printStackTrace();

}

if (objt != null) {

System.out.println("TraceCanReliveObj is delelte by GC");

}

}

}

}

}

@Override

protected void finalize() throws Throwable {

super.finalize();

System.out.println("CanRelive0bj finalize called");

obj = this;

}

@Override

public String toString() {

return "I am CanRelive0bj";

}

public static void main(String[] args) throws InterruptedException {

Thread t = new CheckRefQueue();

t.setDaemon(true);

t.start();

phantomQueue = new ReferenceQueue<TraceCanReliveObj>();

obj = new TraceCanReliveObj();

PhantomReference<TraceCanReliveObj> phantomRef = new PhantomReference<TraceCanReliveObj>(obj, phantomQueue);

obj = null;

System.gc();

Thread.sleep(1000);

if (obj == null) {

System.out.println("obj是null");

} else {

System.out.println("obj可用");

}

System.out.println("第2次gc");

obj = null;

System.gc();

Thread.sleep(1000);

if (obj == null) {

System.out.println("obj是null");

} else {

System.out.println("obj可用");

}

}

}

上述代码中TraceCanReliveObj 对象是一个在finalize()函数中可复活的对象。

第47行,构造了TraceCanReliveObj对象的虚引用,并指定了引用队列。

第48行将强引用去除。

第49行第一次进行GC,由于对象可复活,GC无法回收该对象.

第58行进行第2次GC, 由于finalize()只会被调用一次,因此第2次GC会回收对象,同时其引用队列应该也会捕获到对象的回收。

垃圾回收的停顿

垃圾回收器的任务是识别和回收垃圾对象进行内存清理。为了让垃圾回收器可以正常且高效地执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程的执行,只有这样,系统中才不会有新的垃圾产生,同时停顿保证了系统状态在某一个瞬间的一致性, 也有益于垃圾回收器更好地标记垃圾对象。因此,在垃圾回收时,都会产生应用程序的停顿。停顿产生时,整个应用程序会被卡死,没有任何响应,因此这个停顿也叫做“Stop-The-World” (STW)。

public class StopWorldTest {

public static class MyThread extends Thread {

HashMap map = new HashMap();

@Override

public void run() {

try {

while (true) {

if (map.size() * 512 / 1024 / 1024 >= 900) {

map.clear();

System.out.println("clean map");

}

byte[] b1;

for (int i = 0; i < 100; i++) {

b1 = new byte[512];

map.put(System.nanoTime(), b1);

}

//Thread.sleep(1);

}

} catch (Exception e) {

}

}

public static class PrintThread extends Thread {

public static final long starttime = System.currentTimeMillis();

@Override

public void run() {

try {

while (true) {

long t = System.currentTimeMillis() - starttime;

System.out.println(t / 1000 + "." + t % 1000);

Thread.sleep(100);

}

} catch (Exception e) {

}

}

public static void main(String args[]) {

MyThread t = new MyThread();

PrintThread p = new PrintThread();

t.start();

p.start();

}

}

}

}

上述代码中,开启两个线程,PrintThread 负责每0.1秒在控制台上进行一 次时间戳的输出,MyThread则不停地消耗内存资源,以引起GC。代码第17行,在内存消耗大于900MB时,清空内存,防止内存溢出。

使用参数执行

-Xmx1g -Xms1g -Xmn812k -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails

应用程序的意外停顿都可以在GC日志中找到对应的线索给予解释。这也间接证明了GC对于应用程序的影响。

从这个例子中可以看到,新生代GC进行比较频繁,但每一次GC耗时较短,老年代GC

发生次数较少,但每一次所消耗的时间较长。这种现象和虚拟机参考设置有关。下面通过修改虚拟机参数改变这种现象。

用下面参数来执行

-Xmx1g -Xms1g -Xmn900m -XX:SurvivorRatio=1 -XX: +UseSerialGC -Xloggc:gc. log

-XX: +PrintGCDetails

此参数设置了一个较大的新生代(900MB),并将from、to区域和eden区域设置各300MB。

同时,修改上述代码第8行为(考虑一下为何需要做这个修改) :

if (map.size() * 512 / 1024 / 1024 >= 550) {

16.643: [GC (Allocation Failure) 16.650: [DefNew (promotion failed) : 478630K->468622K(614400K), 0.3180471 secs]16.968: [Tenured: 126975K->126975K(126976K), 0.2966344 secs] 478630K->330798K(741376K), [Metaspace: 3473K->3473K(1056768K)], 0.6221686 secs] [Times: user=0.59 sys=0.03, real=0.62 secs]

可以看到,在增大新生代空间后,新生代GC次数明显减少,但是每次耗时增加,这里显示的6次新生代GC合计耗时775ms。

垃圾回收器

问题1:垃圾回收算法和垃圾回收器有什么关系?

垃圾回收算法是垃圾回收的方法论,垃圾收集器是垃圾回收算法的具体实现

问题2:为什么有这么多种垃圾回收器?

Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,

因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都有可能有很大差别

目前为止,还没有完美的收集器出现,Java的应用场景很多,没有万能的收集器能解决所有应用场景,只是针对具体应用选择最合适的收集器,进行分代收集

问题3:如何查看默认的垃圾回收器?

java -XX:+PrintCommandLineFlags -version

代码后结果的参数提前说明:

DefNew:Defalut New Generation,默认新生代串行

Tenured:Old,老年代老年代的串行

ParNew:Parallel New Generation,在新生代用并行回收

PSYoungGen:Parallel Scavenge

ParOldGen:Parallel Old Generation,在老年代用并行回收

串行回收器新生代串行

串行收集器是是JDK中最基本的垃圾回收器之一。

串行回收器主要有两个特点:

第一,它仅仅使用单线程进行垃圾回收。

第二,它是独占式的垃圾回收。

在串行收集器进行垃圾回收时,Java应用程序中的线程都需要暂停,等待垃圾回收的完成。如图所示,在串行回收器运行时,应用程序中的所有线程都停止工作,进行等待。这种现象称之为“Stop-The-World”。它将造成非常糟糕的用户体验,在实时性要求较高的应用场景中,这种现象往往是不能被接受的。

新生代串行处理器使用复制算法,实现相对简单、逻辑处理特别高效、且没有线程切换的开销。在诸如单CPU处理器等硬件平台不是特别优越的场合,它的性能表现可以超过并行回收器和并发回收器。

使用-XX:+UseSerialGC参数可以指定使用新生代串行收集器和老年代串行收集器。

当虚拟机在Client模式下运行时,它是默认的垃圾收集器。

示例代码:

public class GCDemo {

public static void main(String[] args) {

System.out.println("=====GCDemo,Hello====");

try {

String str = "GCDemo";

while (true){

str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);

str.intern();

}

}catch (Throwable e){

e.printStackTrace();

}

}

}

结论:DefNew+Tenured

[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]

1.“GC”中接下来的“[DefNew”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

2、后面方括号内部的“310K->194K(2368K)”、“2242K->0K(2368K)”,指的是该区域已使用的容量->GC后该内存区域已使用的容量(该内存区总容量)。方括号外面的“310K->194K(7680K)”、“2242K->2241K(7680K)”则指的是GC前Java堆已使用的容量->GC后Java堆已使用的容量(Java堆总容量)。

3、再往后“0.0269163 secs”表示该内存区域GC所占用的时间,单位是秒。最后的“[Times: user=0.00 sys=0.00 real=0.03 secs]”

(1)user – 此次垃圾回收, 垃圾收集线程消耗的所有CPU时间(Total CPU time)

(2)sys – 操作系统调用(OS call) 以及等待系统事件的时间(waiting for system event)

(3)real – 应用程序暂停的时间(Clock time). 由于串行垃圾收集器(Serial Garbage Collector)只会使用单个线程, 所以 real time 等于 user 以及 system time 的总和.

优点:

简单而高效对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率

缺点:

收集期间需要暂停所有应用线程,用户体验不好

应用场景:

Java虚拟机运行在Client模式下默认的新生代垃圾收集器

对应JVM参数是:-XX:+UseSerialGC

当我们使用此参数开启Serial,老年代默认会开启Serial Old

即开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合

表示新生代和老年代都会使用串行回收收集器

新生代使用复制算法,老年代使用标记压缩算法

老年代串行

老年代串行收集器使用的是标记压缩算法。由于老年代垃圾回收通常会使用比新生代回收更长的时间,因此,在堆空间较大的应用程序中,一旦老年代串行收集器启动,应用程序很可能会因此停顿较长的时间。虽然如此,老年代串行回收器可以和多种新生代回收器配合使用,

若要启用老年代串行回收器,可以尝试使用以下参数。

●-XX:+UseSerialGC: 新生代、老年代都使用串行回收器。

●-XX:+UseParNewGC: 新生代使用ParNew回收器,老年代使用串行收集器。

●-XX:+UseParallelGC: 新生代使用ParallelGC回收器,老年代使用串行收集器。

一次老年代串行回收器的工作输出日志类似如下信息:

25.299: [Full GC (Allocation Failure) 25.300: [Tenured: 126975K->84388K(126976K), 0.1865275 secs] 741375K->84388K(741376K), [Metaspace: 3476K->3476K(1056768K)], 0.1866490 secs] [Times: user=0.19 sys=0.00, real=0.18 secs]

新生代并行回收器ParNew

ParNew回收器是一个工作在新生代的垃圾收集器。它只是简单地将串行回收器多线程化,它的回收策略、算法以及参数和新生代串行回收器一样。 ParNew 回收器的工作示意图如图所示。ParNew回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,因此,在并发能力比较强的CPU上,它产生的停顿时间要短于串行回收器,而在单CPU或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,由于多线程的压力,它的实际表现很可能比串行回收器差。

开启ParNew回收器可以使用以下参数。

●-XX:+UseParNewGC:新生代使用ParNew回收器,老年代使用串行回收器。

●-XX:+UseConcMarkSweepGC: 新生代使用ParNew回收器,老年代使用CMS。

ParNew回收器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定。一般,最好与CPU数量相当,避免过多的线程数,影响垃圾收集性能。在默认情况下,当CPU数量小于8个时,ParallelGCThreads 的值等于CPU数量,当CPU数量大于8个时,ParallelGCThreads 的值等于3+((5*CPU_ Count)/8)。

演示代码如上个例子:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseParNewGC

一次ParNew回收器的日志输出信息如下:

0.834: [GC 0.834: [ParNew: 13184K->1600K(14784K) ,0.0092203 secs] 13184K- >

1921K (63936K),0.0093401 secs] [Times: user=0.00 sys=0.00, real=0.00 secs ]

可以看到,这个输出和新生代串行收集器几乎是一样的,只有回收器标识符不同。

应用场景:

最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样

它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器

对应JVM参数是:-XX:+UseParNewGC

启用ParNewGC收集器,只影响新生代的收集,不影响老年代

开启后会使用:ParNew(Young区用)+Serial Old(Old区用)的收集器组合

新生代使用复制算法,老年代使用标记压缩算法

ParallelGC新生代并行垃圾回收器

新生代ParallelGC回收器也是使用复制算法的收集器。从表面上看,它和ParNew回收器一样,都是多线程、独占式的收集器。但是, ParallelGC回收器有个重要的特点:它非常关注系统的吞吐量

新生代ParallelGC回收器可以使用以下参数启用。

●-XX:+UseParalleIGC: 新生代使用ParallelGC回收器,老年代使用ParallelOldGC。

●-XX:+UseParallelOldGC: 新生代使用ParallelGC 回收器,老年代使用ParallelOldGC

回收器。

对应JVM参数是:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活,即配置其中一个,另一个会自动连带激活)

ParallelGC回收器提供了两个重要的参数用于控制系统的吞吐量。

Parallel重点关注的是:可控制的吞吐量

吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)

即比如程序运行100分钟,垃圾收集时间位1分钟,吞吐量为99%

高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务

比如你在前台下个单,它停顿来算,交互性差,而对于科学计算,它自己在后台计算,停顿一会我们也不知道,但是它高效利用了CPU

●-X:MaxGCPauseMillis: 设置最大垃圾收集停顿时间。它的值是一个大于0的整数。

ParallelGC在工作时,会调整Java堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMillis以内。如果希望减少停顿时间,而把这个值设得很小,为了

达到预期的停顿时间,虚拟机可能会使用一个较小的堆(一个小堆比一个大堆回收快),

而这将导致垃圾回收变得很频繁,从而增加了垃圾回收总时间,降低了吞吐量。

●-XX:GCTimeRatio: 设置吞吐量大小。它的值是一个0到100之间的整数。假设

GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。比如

GCTimeRatio等于19 (默认值),则系统用于垃圾收集的时间不超过1/(1+19)=5%。默

认情况下,它的取值是99,即不超过1/(1+99)=1%的时间用于垃圾收集。

除此以外,ParallelGC回收器与ParNew回收器另一个不同之处在于它还支持一种自适应的

GC调节策略。使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。在这种模式下,新生代的大小、eden和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虛拟机的最大堆、目标吞吐量( GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。

演示代码如上个例子:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseParallelGC

结论:PSYoungGen+ParOldGen

ParallelOldGC老年代垃圾回收器

老年代ParallelOldGC回收器也是一种多线程并发的收集器。和新生代ParallelGC回收器一样,它也是一种关注吞吐量的收集器。并且和ParallelGC新生代回收器搭配使用。

ParallelOldGC回收器使用标记压缩算法,图显示了老年代

ParallelOldGC回收器的工作模式。

使用-XX:+UseParallelOldGC可以在新生代使用ParallelGC 回收器,老年代使用

ParallelOldGC回收器。

这是一对非常关注吞吐量的垃圾回收器组合。在对吞吐量敏感的系统中,可以考虑使用。参数-XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。

ParallelOldGC回收器的工作日志如下:

1.500: [Full GC [PSYoungGen: 2682K->0K(19136K)] [ParOldGen: 28035K->30437K(43712K) ] 3071 7K-> 304 37K (62848K) [PSPermGen: 10943K-> 10928K (32768K) ],0.2902791secs] [Times: user=1.44 sys=0.03, real=0.30 secs]

它显示了新生代、老年代以及永久区在回收前后的情况,以及FullGC所消耗的时间。

CMS垃圾回收器CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器

与ParallelGC 和ParallelOldGC 不同,CMS回收器主要关注于系统停顿时间。

CMS工作时,主要步骤有:

初始标记、并发标记、预清理、重新标记、并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而预清理、并发标记、并发清除和并发重置是可以和用户线程一起执行的。因此,从整体上说,CSM收集不是独占式的,它可以在应用程序运行过程中进行垃圾回收。

CMS参数设置

-XX:+UseConcMarkSweepGC CMS 启用CMS回收器,是多线程回收器,设置合理

的工作线程数量也对系统性能有重要的影响。

CMS默认启动的并发线程数是(ParallelGCThreads+3)/4)。

并发线程数量也可以通过-XX:ConcGCThreads或者-XX:ParallelCMSThreads参数手工设定。

当CPU资源比较紧张时,受到CMS回收器线程的影响,应用系统的性能在垃圾回收阶段可能会非常糟糕。

注意:并发是指收集器和应用线程交替执行,并行是指应用程序停止,同时由多个线

程一起执行GC。因此并行回收器不是并发的。因为并行回收器执行时,应用程

序完全挂起,不存在交替执行的步骤。

在CMS回收过程中,应用程序仍然在不停地工作,又会不断地产生垃圾。这些新生成的垃圾在当前CMS回收过程中是无法清除的。同时,因为应用程序没有中断,所以在CMS回收过程中,还应该确保应用程序有足够的内存可用。因此,CMS回收器不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一阈值时便开始进行回收,以确保应用程序在CMS工作过程中,依然有足够的空间支持应用程序运行。

这个回收阈值可以使用-XX:CMSInitiatingOccupancyFraction来指定,默认是68。即当老年代的空间使用率达到68%时,会执行一次CMS回收。如果应用程序的内存使用率增长很快,在CMS的执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失败,虚拟机将启动老年代串行收集器进行垃圾回收。如果这样,应用程序将完全中断,直到垃圾回收完成,这时,应用程序的停顿时间可能会较长。

注意:通过-XX:CMSInitiatingOccupancyFraction 可以指定当老年代空间使用率达到多

少时,进行一次CMS垃圾回收。

因此,根据应用程序的特点,可以对-XX:CMSInitiatingOccupancyFraction进行调优。如果内存增长缓慢,则可以设置个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。

CMS是一个基于标记清除算法的回收器。在本章之前的篇幅中已经提到,标记清除算法将会造成大量内存碎片,离散的可用空间无法分配较大的对象。图显示了CMS回收前后老年代的情况。

在这种情况下,即使堆内存仍然有较大的剩余空间,也可能会被迫进行一次垃圾回收,以换取一块可用的连续内存。这种现象对系统性能是相当不利的,为了解决这个问题,CMS回收器还提供了几个用于内存压缩整理的参数。

-XX:+UseCMSCompactAtFullCollection开关可以使CMS在垃圾收集完成后,进行一次内存碎片整理,内存碎片的整理不是并发进行的。

-XX:CMSFullGCsBeforeCompaction参数可以用于设定进行多少次CMS回收后,进行一次内存压缩。

CMS日志

执行上一节代码,参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC

以上信息是一次CMS收集的输出。可以看到,在CMS回收器的工作过程中,包括了初始化标记、并发标记、预清理、重新标记、并发清理和重发重置等几个重要阶段。在日志中,还可以看到CMS的耗时以及堆内存信息。

G1

G1回收器(Garbage-First) 是在JDK 1.7 中正式使用的全新的垃圾回收器,从长期目标来

看,它是为了取代CMS回收器。从分代上看,G1依然属于分代垃圾回收器,它会区分年轻代和老年代,依然有eden区和survivor区,但从堆的结构上看,它并不要求整个eden区、年轻代或者老年代都连续。它使用了分区算法。G1同时使用了全新的分区算法,其特点如下。

●并行性: G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力尽量缩短STW。

●并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因

此一般来说,不会在整个回收期间完全阻塞应用程序。

●分代GC:G1依然是一个分代收集器,但是和之前回收器不同,它同时兼顾年轻代和

老年代。对比其他回收器,它们或者工作在年轻代,或者工作在老年代,宏观上看G1之中不再区分年轻代和老年代,把内存划分成多个独立的子区域(Region)。

●空间整理: G1在回收过程中,会进行适当的对象移动,不像CMS,只是简单地标记清

理对象,在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会

有效地复制对象,减少空间碎片。

●可预见性: 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿也能得到较好的控制。

新生代GCRegion区域化垃圾收集器:

最大的好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可

区域化内存划片Region,整体变为了一些不连续的内存区域,避免了全内存区的GC操作

核心思想:将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地位某个代服务,可以按需在年轻代和老年代切换启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048=65536MB=64G内存

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器

这些Region的一部分包含新生代新生代的垃圾收集依然采用暂停所有应用线程的方式,将存或对象拷贝到老年代或Survivor空间这些Region的一部分包含老年代G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作这意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存问题的存在了在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这种巨型对象默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响,为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象如果H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储,为了能找到连续的H区,有时候不得不启动Full GC

回收步骤:

G1收集器下的Young GC针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部分晋升到Old区Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区最后Eden区收拾干净了,GC结束,用户的应用程序继续执行回收过程

G1的并发阶段和CMS有点类似,它们都是为了降低一次停顿时间,而将可以和应用程序

并发的部分单独提取出来执行。

并发标记周期可以分为以下几步。

●初始标记:

标记从根节点直接可达的对象。这个阶段会伴随一次新生代 GC,它是会产生全局停顿的,应用程序线程在这个阶段必须停止执行。

●根区域扫描:

在这个阶段,将扫描由survivor区直接可达的老年代区域,并标记这些直接可达的对象。这个过程是可以和应用程序并发执行的。但是根区域扫描不能和新生代GC同时执行(因为根区域扫描依赖survivor区的对象,而新生代GC会修改这个区域)。

●并发标记:

和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。这是一个并发的过程,并且这个过程可以被一次新生代GC打断。

●重新标记:

和CMS一样,重新标记也是会产生应用程序停顿的。由于在并发标记过程

中,应用程序依然在运行,因此标记结果可能需要进行修正,所以在此对上一次的标记

结果进行补充。

●独占清理:

这个阶段是会引起停顿的。它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集(Remebered Set)。

该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息。

●并发清理阶段:

这里会识别并清理完全空闲的区域。它是并发的清理,不会引起停顿。

[GC pause (G1 Humongous Allocation) (young) (initial-mark) (to-space exhausted), 0.0080719 secs]

[Parallel Time: 5.1 ms, GC Workers: 10]

[GC Worker Start (ms): Min: 134.2, Avg: 134.3, Max: 134.3, Diff: 0.1]

[Ext Root Scanning (ms): Min: 4.3, Avg: 4.5, Max: 4.6, Diff: 0.2, Sum: 44.9]

[Update RS (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.7]

[Processed Buffers: Min: 0, Avg: 1.1, Max: 5, Diff: 5, Sum: 11]

[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

[Object Copy (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.2]

[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.6]

[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 10]

[GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 1.1]

[GC Worker Total (ms): Min: 4.8, Avg: 4.9, Max: 4.9, Diff: 0.1, Sum: 48.6]

[GC Worker End (ms): Min: 139.1, Avg: 139.1, Max: 139.1, Diff: 0.0]

[Code Root Fixup: 0.0 ms]

[Code Root Purge: 0.0 ms]

[Clear CT: 0.7 ms]

[Other: 2.3 ms]

[Evacuation Failure: 1.2 ms]

[Choose CSet: 0.0 ms]

[Ref Proc: 0.5 ms]

[Ref Enq: 0.0 ms]

[Redirty Cards: 0.5 ms]

[Humongous Register: 0.0 ms]

[Humongous Reclaim: 0.0 ms]

[Free CSet: 0.0 ms]

[Eden: 6144.0K(6144.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 8262.9K(10.0M)->6516.9K(10.0M)]

[Times: user=0.00 sys=0.00, real=0.01 secs]

[GC concurrent-root-region-scan-start]

[GC concurrent-root-region-scan-end, 0.0000227 secs]

[GC concurrent-mark-start]

[GC concurrent-mark-end, 0.0008297 secs]

[GC remark [Finalize Marking, 0.0003497 secs] [GC ref-proc, 0.0002639 secs] [Unloading, 0.0007434 secs], 0.0017108 secs]

[Times: user=0.00 sys=0.00, real=0.00 secs]

[GC cleanup 7215K->7215K(10M), 0.0012619 secs]

[Times: user=0.00 sys=0.00, real=0.00 secs]

必要时FullGC

和CMS类似,并发收集由于让应用程序和GC线程交替工作,因此总是不能完全避免在特别繁忙的场合会出现在回收过程中内存不充足的情况。当遇到这种情况时,G1也会转入一个Full GC进行回收。

G1的参数设置

-XX:+UseG1GC可以使用标记打开G1收集器开关。

-XX:MaxGCPauseMillis它用于指定目标最大停顿时间。如果任何一次停顿超过这个设置值时,G1就会尝试调整新生代和老年代的比例、调整堆大小、调整晋升年龄等手段,试图达到预设目标。对于性能调优来说,有时候,总是鱼和熊掌不可兼得的,如果停顿时间缩短,对于新生代来说,这意味着很可能要增加新生代GC的次数,GC反而会变得更加频繁。对于老年代区域来说,为了获得更短的停顿时间,那么在混合GC收集时,一次

收集的区域数量也会变少,这样无疑增加了进行Full GC的可能性。

-XX:ParallelGCThreads,它用于设置并行回收时,GC的工作线程数量。

-XX:InitiatingHeapOccupancyPercent参数可以指定当整个堆使用率达到多少时,触发

并发标记周期的执行。默认值是45,即当整个堆占用率达到45%时,执行并发标记周期。InitiatingHeapOccupancyPercent 一旦设置,始终都不会被G1收集器修改,这意味着G1收集器不会试图改变这个值,来满足MaxGCPauseMillis的目标。如果InitiatingHeapOccupancyPercent值设置偏大,会导致并发周期迟迟得不到启动,那么引起Full GC的可能性也大大增加,反之,一个过小的InitiatingHeapOccupancyPercent 值,会使得并发周期非常频繁,大量GC线程抢占CPU,会导致应用程序的性能有所下降。

对象何时进入老年代老年对象进入老年代

那eden区中的对象何时能进入老年代呢?一般来说, 当对象的年龄达到一定的大小,就自然可以离开年轻代,进入老年代,一般可以把对 象进入老年代的事件,称为“晋升”。对象的年龄是由对象经历过的GC次数决定的。在新生代中的对象每经历一次GC,如果它没有被回收,它的年龄就加1。虚拟机提供了一个参数来控制新生代对象的最大年龄: MaxTenuringThreshold。默认情况下,这个参数为15。也就是说,在新生代的对象最多经历15次GC,就可以晋升到老年代。

-XX:MaxTenuringThreshold作用:在可以自动调节对象晋升(Promote)到老年代阈值的GC中,设置该阈值的最大值。【啥意思?当一个对象在新生代中经历过Minor GC之后对象的年龄就+1,原来0岁此时就变为1了,当又经历一次回收之后依然没有被回收则年龄就变为2了,而我们给这参数设置的最大参数为5,假如对象的年龄变为6超些我们设置的最大的这个5时,该对象就会从新生代晋升到老年代当中,注意:这里是一个极其理想的情况,但是实际它是可能自动调节的,可能年龄到了2还没达到我们设定的5也将其对象晋升到老年代了,但是最大值不可能超过我们设置的这个值的,也就是不可能对象的年龄到了6还没有被晋升】该参数的默认值是15

示例:

public class AllocEden{

public static final int _1K=1024;

public static void main (String args[]) {

for(int i=0;i<5* _1K; i++) {

byte[] b=new byte[_1K] ;

}

}

}

可以看到整个过程中没有GC发生,因此,一共分配的5MB数据都应该在堆中,从堆的日志中可以看到,eden 区占据了7MB左右的空间,from、 to 和老年代tenured均未使用。

示例:

public class MaxTenuringThreshold {

public static final int _1M = 1024 * 1024;

public static final int _1K = 1024;

public static void main(String args[]) {

Map<Integer, byte[]> map = new HashMap<Integer, byte[]>();

for (int i = 0; i < 5 * _1K; i++) {

byte[] b = new byte[_1K];

map.put(i, b);

}

for (int k = 0; k < 17; k++) {

for (int i = 0; i < 270; i++) {

byte[] g = new byte[_1M];

}

}

}

}

用下面参数运行

-Xmx1024M -Xms1024M -XX:+PrintGCDetails -XX:MaxTenuringThreshold=5 -XX:+PrintHeapAtGC

这里有着明显的差异。从新生代被移除的对象,这里晋升到了老年代(指map对象中的byte数组),这从最后一次GC的后续堆日志中可以看到。老年代已经有6592KB被使用,而新生代使用为0KB。这说明这5MB对象晋升老年代成功。MaxTenuringThreshold默认值是15

虽然有上述示例做铺垫,但仍然需要再次强调,MaxTenuringThreshold 指的是最大晋升年龄。它是对象晋升老年代的充分非必要条件。即达到该年龄,对象必然晋升,而未达到该年龄,TargetSurvivorRatio,它用于设置survivor区的目标使用率,默认为50,即如果survivor 区在GC后超过50%的使用率,那么,就很可能会使用较小的age作为晋升年龄。

大对象进入老年代

除了年龄外,对象的体积也会影响对象的晋升。试想,如果对象体积很大,新生代无论eden区或者survivor区无法容纳这个对象,自然这个对象无法存放在新生代,因此,由于体积太大,也非常有可能被直接晋升到老年代。如图所示,如果需要一个连续的6MB空间,而新生代survivor区( 只有5MB)无法接纳这样的大小,此时无论该对象年龄如何,它都会被直接晋升到老年代。

另外一个有趣的参数是PretenureSizeThreshold,它用来设置对象直接晋升到老年代的阈值,单位是字节。只要对象的大于指定值,就会绕过新生代,直接在老年代分配。这个参数只对串行回收器和ParNew有效,对于ParallelGC 无效。默认情况下该值为0,也就是不指定最大的晋升大小,一切由运行情况决定。

示例:

public class PretenureSizeThreshold {

public static final int _1K = 1024;

public static void main(String args[]) {

Map<Integer, byte[]> map = new HashMap<Integer, byte[]>();

for (int i = 0; i < 5 * _1K; i++) {

byte[] b = new byte[_1K];

map.put(i, b);

}

}

}

使用下面参数运行

-Xmx32m -Xms32m -XX:+UseSerialGC -XX:+PrintGCDetails

可以看到,所有的对象均分配在新生代,老年代的使用率为0。接着附加参数PretenureSizeThreshold, ;令PretenureSizeThreshold=1000,则大小为1024字节的byte数组理应被分配在老年代,参数如

-Xmx32m -Xms32m -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000

也许会觉得非常奇怪,因为期望的结果是至少有5MB数据被分配在老年代,但为什么作为分配主体的5MB数组看起来依然在新生代呢?似乎PretenureSizeThreshold 不起作用,但是老年代的情况却和不加PretenureSizeThreshold时有所不同,在这里,有96KB的空间被使用。导致这种现象的原因是虚拟机在为线程分配空间时,会优先使用一块叫作TLAB的区域,对于体积不大的对象,很有可能会在TLAB上先行分配,因此,就失去了在老年代分配的机会。因此,这里简单地禁用TLAB即可。使用下述参

-Xmx32m -Xms32m -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB

可以看到,在禁用TLAB后,大于1000字节的byte数组已经分配在老年代了。

方法finalize对垃圾回收器的影响

该函数允许在子类中被重载,用于在对象被回收时进行资源地释放。目前,普遍的认识是,尽量不要使用finalize()函数进行资源释放,原因主要有以下几点:

●前面曾提到,在finalize()时可能会导致对象复活;

●finalize()函数的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()将没有机会执行;

●一个糟糕的 finalize()会严重影响GC的性能。函数finalize()是由FinalizerThread 线程处理的。每一个即将被回收的并且包含有finalize()方法的对象都会在正式回收前加入FinalizerThread 的执行队列,该队列为java.lang.ref.ReferenceQueue引用队列,内部实现为链表结构,队列中每一项为 java.lang.ref.Finalizer引用对象,它本质为一个引用,如图所示,这和虚引用、弱引用等如出一辙。

Finalizer内部封装了实际的回收对象,如图所示。可以看到next、prev 为实现链表所需

,它们分别指向队列中的下一个元素和上一个元素,

finalizee是要回收的对象,比如,在后续的示例中就为LongFinalize$LF。

由于对象在回收前被Finalizer 的referent 字段进行“强引用”,并加入了FinalizerThread

的执行队列,这意味着对象又变为可达对象,因此阻止了对象的正常回收。由于在引用队列中的元素排队执行finalize()方法, 一旦出现性能问题,将导致这些垃圾对象长时间堆积在内存中,可能会导致OOM异常。

示例

public class LongFinalize {

public static class LF {

private byte[] content = new byte[512];

@Override

protected void finalize() {

try {

System.out.println(Thread.currentThread().getId());

Thread.sleep(1000);

} catch (Exception e) {

e.printStackTrace();

}

}

}

public static void main(String[] args) {

long b = System.currentTimeMillis();

for (int i = 0; i < 50000; i++) {

LF f = new LF();

}

long e = System.currentTimeMillis();

System.out.println(e - b);

}

}

-Xmx10m -Xms10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath="D:/f.dump"

运行结果

结果发生了OOM错误并在D盘下得到了堆的Dump文件。仔细阅读代码,不难发现,每次循环中产生的LF对象(占用大约512字节)都会在下一次循环中失效(因为局部变量作用域过期,对象也无其他引用),因此所有产生的LF对象都应该可以被回收。因此10MB堆空间,理论上应该完全可以满足需要,只是需要多进行几次GC而已,而这里为什么

依然会出现OOM呢?使用MAT打开得到的堆文件。

从最大对象中可以看到,目前系统中有大量的Finalizer类,这意味着FinalizerThread 执行

列可能一 直持有对象而来不及执行,因此大量的对象堆积而无法被释放,最终导致了这个OOM。使用MAT自带的“Finalizer Overview” 功能可以更好地观察系统中的Finalizer,

去掉LF类的finalize()方法,再次以相同的参数运行这段

程序。可以观察到,程序很快正常结束。由此,|可以进一步说明finalize()对GC产生的影响。

注意:一个糟糕的finalize()可能会使对象长时间被Finalizer 引用,而得不到释放,因

此这会进一步增加GC的压力。因此,finalize()应该是尽量少 地被使用。

温故而知新

1.与串行回收器相关的参数

-XX:+UseSerialGC:在新生代和老年代使用串行收集器。

●-XX:SurvivorRatio: 设置eden区大小和survivior区大小的比例。

●-XX:PretenureSizeThreshold: 设置大对象直接进入老年代的阈值。当对象的大小超过这

个值时,将直接在老年代分配。

●-XX:MaxTenuringThreshold: 设置对象进入老年代的年龄的最大值。每一次Minor GC

后,对象年龄就加1。任何大于这个年龄的对象,一定会进入老年代。

2.与并行GC相关的参数

日人己

●-XX:+UseParNewGC: 在新生代使用并行收集器。

●-XX:+UseParallelOldGC: 老年代使用并行回收收集器。

●-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通过情况下可以和CPU数量相等,

但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。

●-XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间。它的值是一个大于0的整数。收

集器在工作时,会调整Java堆大小或者其他一些参数,尽可能地把停顿时间控制在

MaxGCPauseMillis以内。

●-XX:GCTimeRatio: 设置吞吐量大小。它的值是一个0到100之间的整数。假设

GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。

●-XX:+UseAdaptiveSizePolicy:打开自适应GC策略。在这种模式下,新生代的大小、eden

和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞

吐量和停顿时间之间的平衡点。

3.与CMS回收器相关的参数

●-XX:+UseConcMarkSweepGC: 新生代使用并行收集器,老年代使用CMS+串行收集器。

●-XX:ParallelCMSThreads: 设定CMS的线程数量。

●-XX:CMSInitiatingOccupancyFraction: 设置CMS收集器在老年代空间被使用多少后触

发,默认为68%。

●-XX:+UseCMSCompactAtFullCollection: 设置CMS收集器在完成垃圾收集后是否要进

行一次内存碎片的整理。

●-XX:CMSFullGCsBeforeCompaction: 设定进行多少次CMS垃圾回收后,进行一次内存

压缩。

-XX:+CMSClassUnloadingEnabled:允许对类元数据区进行回收。

●-XX:CMSInitiatingPermOccupancyFraction: 当永久区占用率达到这- - -百分比时,启动

CMS回收(前提是-XX:+CMSClassUnloadingEnabled激活了)。

4.与G1回收器相关的参数

●-XX:+UseG1GC: 使用G1回收器。

●-XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间。

●-XX:GCPauseIntervalMillis: 设置停顿间隔时间。

五万字长篇-java虚拟机看这篇文章就够了(上)

五万字长篇-java虚拟机看这篇文章就够了(下)

Git版本控制学习总结

标签: #c语言编写虚拟机程序代码 #java放大缩小