JVM7u6阅读与思考《前3章》
https://docs.oracle.com/javase/specs/index.html
《深入理解Java虚拟机:JVM高级特性和最佳实践》Java第二版中文pdf版本
《深入理解Java虚拟机:JVM高级特性和最佳实践》Java第三版中文pdf版本
JDK:Java语言程序设计、Java虚拟机、JavaAPI类库这三部分统称为JDK,JDK是支持Java开发的最小环境。
JRE:将JavaAPI中的Java SE API子集以及Java虚拟机这两个部分统称为JRE,JRE是支持Java程序允许的标准环境。
Java Card:支持java小程序运行在小内存设备上的平台。
Java虚拟机是怎么运行起来的?运行起来干了什么? pass
在计算机操作系统章节中给出过一张明确的程序启动运行图,在java虚拟机没有运行的时候,是存储在磁盘中的一个可执行的文件,当启动java虚拟机时就相当于双击了这个可执行的文件,将该文件加载到内存中,解析执行,而这个可执行的文件开始在内存中开辟一系列运行时数据区,以准备执行的java程序,在执行java程序之前先经过编译,然后解析执行,在执行程序的过程中,对于当前的线程有自己独有的程序计数器、虚拟机栈、本地方法栈。方法区和堆是多个线程共有的区域。
每一个线程都有自己的一块程序计数器区域,这是一块比较小的内存空间。程序计数器的作用就是记录:当前线程执行到哪一行了。
因为Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。所以每个线程都要有一个单独记录自己执行到哪一行的程序计数器。
程序计数器只是一个简单的提供计数的功能,所以它压根不会发生内存溢出。
注意虚拟机栈的描述:是Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。虚拟机栈的生命周期与线程相同。
何为栈帧?栈帧里的局部变量表、操作栈、动态链接又是什么? nopass
局部变量存放了编译期可知的各种基本数据类型(8种)、对象引用类型。局部变量表所需的内存空间在编译期间完成分配(注意说的是 局部变量 的 引用,String x = "123",存放的是x,这里是栈空间),当进入方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
也就是说:基本数据类型的存储空间的大小是在局部变量表里指定的,引用类型只是在局部变量表里存放引用类型的地址。
从图2.4中看Java虚拟机栈的图可以看到,java虚拟机栈是用来存放栈帧的,一个栈帧的大小肯定和局部变量等有关系,那如果有无线深度调用的话,那这个java虚拟机栈里肯定会存放超级多的栈帧!这个java虚拟机的栈不可能无限的的动态开辟空间去存放这些栈帧,总有个界限吧?一但超过了界限怎么办? pass
【引用Java虚拟机】:在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展(当前大部分的Java虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemeryError异常。
也就是说除了超出固定长度的虚拟机栈抛出StackOverflowError异常外,即使虚拟机可以无限的扩展虚拟机栈深度,也会出现无法申请到足够的内存而抛出OutOfMemerError异常。
在Sun HotSpot虚拟机中直接就把本地方法栈和虚拟机栈合二为一。
通俗的讲Native Method就是一个java调用的非java代码实现的接口(JNI)。
native是用做java和其他语言(大部分是c/c++)运行协作时使用的,native修饰的方法不是java实现的。
native的意思是本地的,这个本地指的就是操作系统,对于不同的操作系统,java跨平台实现某个方法调的native肯定也都是不一样的,java有调用的权限,不必自己在重复实现了。
发挥的作用类型,本地方法栈里也是存储栈帧结构?既然是本地方法,不要迷糊了,也就是java方法调用了其他语言写的方法,既然是其他语言比如c++/c实现的,那这个方法栈难道是跑的操作系统里执行本地方法开辟方法栈空间???不不不,肯定不是这样的。既然是调用本地方法,那经过编译后的.class文件该是什么样子的? pass
使用javah将class文件转化为c的头文件.h,创建.c源文件实现native方法(.c源文件要包含.h头文件),生成dll文件
运行java程序,System.loadLibrary()加载dll,然后即可调用native函数
可以理解,虽然是本地方法,但是在java中依旧声明了它,既然声明了它,在代码中调用它的时候就会在本地方法栈中创建栈帧,只不过这个栈帧里的结构相当简单,局部变量表应该是需要的,需要存放形参,方法出口信息可能含有返回类型应该也是要有的,其他的用到用不到暂时未知。所以本地方法栈也会和虚拟机栈一样,也会抛出StackOverflowError、OutOfMemberError异常。
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配(那除了几乎之外的对象呢?栈上分配、标量替换优化技术将会导致一些微妙的变化发生)。
java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。堆在实现时既可以实现成固定大小的,也可以是可扩展的(主流的虚拟机都是按可扩展实现的)。
当堆上创建的对象实例无限多,堆上没有内存空间足以完成新实例的分配,并且申请不到可扩展的内存空间时就会抛出OutOfMemberError异常。
方法区和Java堆一样,都是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
方法区和java堆一样不需要连续的内存和选择固定大小或者可扩展外,还可以选择不实现垃圾收集。java堆是垃圾收集的主要区域,而方法区的垃圾收集行为是比较少出现的,所以方法区在垃圾收集中常被称为“永久代”。也并不是说永久代这里面的内存就永远不回收了,这个区域的内存回收目标主要是针对 常量池 的回收和 堆类型 的卸载,一般来说这个区域的回收“成绩”比较难以令人满意。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
按数据类型可分为基本数据类型和引用数据类型变量,按声明位置又可分为成员变量和局部变量,在方法体外,类内声明的变量称成员变量;在方法体内声明的称局部变量。
静态变量表示该变量属于整个类的,使用static修饰,其修饰的方法称静态方法,其修饰的类称静态类,修饰的块称静态方法块。
成员变量:是实例变量,每创建一个实例,JVM就会为实例变量分配一次内存,实例变量位于堆中,其生命周期为实例的生命周期。
静态成员变量:是类变量,在内存中只有一份,在JVM加载类的过程中为静态变量分配内存,静态变量位于方法区,被类的所有实例共享。可直接通过类名访问,其生命周期为类的生命周期。
// 代码段2.1 final 类型 常量名 = 值 // 例如 final String LOGO = "JVM";
常量是一种特殊的变量,它的值被设定后,在程序运行过程中不允许改变。
1.字符常量是用单引号括起来的单个字符。char c = 'c';
2.Java中还可以使用转义字符'\'来将其后的字符转变为特殊字符型常量。例如:char c = '\n';
3.直接使用Unicode值来表示字符型常量:'\uXXXX',其中XXXX代表一个十六进制数,如'\u000a'表示\n
1.调用它的构造方法完成,String a = new String("a");。
如果是按照构造方法创建String对象,如String s1 = new String("abc");String s2 = new String("abc");其中s1和s2分别占用独立的内存空间,利用==比较的话,比较的是内存地址,得false.。由于程序中经常出现大量String对象值相同得情况,造成内存空间冗余。为有效利用内存,Java预留得一块特殊内存区域,称为字符串常量池。当编译器遇到一个String常量时,先检查字符串常量池是否存在相同得String常量,存在把该常量得引用指向字符串常量池中的String常量。
运行时常量池(Runtime Constant Pool)是方法区的一部分,如上图Non Heap里的Interned Strings所在的位置。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table)【Class常量池】,用于存放编译期生成的各种 字面量和符号引用 ,这部分内容将在类加载后存放到方法区的运行时常量池中。
字面量就是比如int a = 1;这个1就是字面量,又比如String a = "abc",abc就是字面量。
在java中,一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类要引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以将符号org.simple.Tool翻译为Tool类的实际内存地址(把符号引用替换成内存地址),即直接引用地址。
注意:运行时常量池,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
参见常量池.note
运行时常量池是方法区的一部分,在运行时不断的往池中写数据,池总有写满的时候,当常量池无法申请到内存时会抛出OutOfMemoryError异常。
我们上面罗列的都属于运行时数据区的内容,但直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且有可能导致OutOfMemoryErro异常出现。
在JDK4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为它避免了在Java堆和Native堆中来回复制数据。
上面明确的说了直接内存不是虚拟机规范中定义的区域,它并属不于java。直接内存是属于操作系统直接管辖的内存区域。
虽然直接内存不受Java的管理,但是java在文件读取等操作的时候会去操作这一块内存区域,当操作的这一块内存区域不足以在继续操作下去的时候,Java会抛出OutOfMemoryError异常。
从接触Java以来,自己就潜移默化的把大学里C和C++的概念带到了java里,只知道,哦,java也像C和C++一样使用指针访问对象,再后来做毕设的时候接触了MATLAB语言,印象中它是使用句柄操作对象的,对于这二者我并不透彻。
// 代码段2.2 Object obj = new Object()
对于以上代码,Object obj会在Java虚拟机栈作为一个reference类型数据出现。而new Object会在堆中形成一块存储了Object类型所有实例数据值的结构化内存。在java虚拟机规范中,reference只规定了一个指向对象的引用,并没有定义一种这个引用应该通过哪种方式去定位,以及访问到Java堆中对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,目前主流的访问方式有两种:句柄和指针。
如上图,通过句柄访问的,Java堆将会划出一小块内存空间作为句柄池,每个句柄包含两个部分,指向对象实例数据的指针和对象类型数据(即Class类)的指针,而reference指向的是句柄池里该句柄的地址。
如果是通过指针访问的,在图中可见,对象实例存储了到对象类型数据的指针,reference指向的是该对象的地址。
如上图,使用指针的方式就是节省了存储句柄池的空间消耗,还节省了指针定位时间的开销(HotSpot使用的是指针访问方式)。
使用句柄访问方式的最大好处就是reference中存储是句柄的地址,并不是对象指针地址,当对象被移动时只会改变句柄中实例数据指针,而reference不需要更改(在垃圾收集时移动对象是非常普遍的行为)。
// 代码段2.3 -Xms20M -Xmx20M -Xmn10M //Xms:设置堆的最小值,Xmx:设置堆的最大值。 // 当Xms和Xmx参数设置为一样即可避免堆自动扩展 // -Xmn10M:设置年轻代大小为10M,整个堆大小=年轻代大小+年老代大小+持久代大小 -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError // -verbose:gc //在控制台输出GC情况 // -XX:+PrintGCDetails将在控制台输出详细的GC信息 // -XX:SurvivorRatio=8 定义了年轻代中Eden区域和Survivor区 // 域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占年轻代的8/10, // From幸存区和To幸存区各占年轻代的1/10 // -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出时Dump出 // 当前的内存堆转储快照以便事后进行分析(可以使用JProfile直接打开进行分析)。
堆内存的大小。为了测试堆内存OOM异常,我们只需要不断的创建对象,而且不能让这个对象被JVM回收,JVM一般会在内存快满时,进行一次内存回收,我们先来演示一下,在IDEA中集成JProfiler插件,并在本地安装JProfiler后即可进行内存分析:
// 代码段2.4 package outofmemboryerror; /** * VM options:-Xms1M -Xmx1M -XX:+PrintGCDetails */ public class HeapOutOfMemoryError { static class Ombect { Ombect(String s) { } } public static void main(String[] args) { Ombect o = null; while (true) { o = new Ombect("演示JVM内存回收"); } } }
在项目不断运行过程中,Memory开始的瞬间涨满,垃圾收集立即被拉起开始进行回收,达到平衡后,接着二者此涨彼涨的进行小范围波动,以防止内存溢出。出现以上情况的原因是JVM不断的在Eden区创建对象,但在创建下一个对象后,上一个对象没有reference指向它,所以在下一次垃圾回收时,它将被回收。为了不让新创建的对象被垃圾回收,我们将新创建的对象丢进List中,再次查看:
// 代码段2.5 public static void main(String[] args) { List<Ombect> list = new ArrayList<>(); while (true) { list.add(new Ombect("演示JVM内存回收")); } }
与上图对比可见,Threads刚启动没多久就挂了,已经没有线程在运行了,而GC也出现一次凸峰然后就消失了,memory涨到高峰,被GC进行了一波收集后继续持续,因为内存已经溢出了,程序都挂了,Memory被涨满,没有GC在活动,所以会呈现上图那样。看一下程序运行后台打印的信息:
// 代码段2.6 Heap PSYoungGen total 1024K, used 29K [0x00000000ffe80000, 0x0000000100000000, 0x0000000100000000) eden space 512K, 5% used [0x00000000ffe80000,0x00000000ffe87708,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 71% used [0x00000000fff00000,0x00000000fff5b9b8,0x00000000fff80000) ParOldGen total 512K, used 253K [0x00000000ffe00000, 0x00000000ffe80000, 0x00000000ffe80000) object space 512K, 49% used [0x00000000ffe00000,0x00000000ffe3f470,0x00000000ffe80000) Metaspace used 3473K, capacity 4496K, committed 4864K, reserved 1056768K class space used 379K, capacity 388K, committed 512K, reserved 1048576K Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at outofmemboryerror.HeapOutOfMemoryError.main(HeapOutOfMemoryError.java:20)
异常信息说的很清楚,Java heap space发生了OutOfMemoryError。
在HotSpot实现中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。
要想让虚拟机栈溢出,首先你要指定虚拟机栈里存的是什么?是Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧放入虚拟机栈。而虚拟机栈又会出现两种异常:
StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出。
OutOfMemoryError:如果虚拟机在扩展栈时无法申请到足够的内存空间,抛出。
// 代码段2.7 package outofmemoryerror; public class StackOverFlowErrorTest { private int num = 0; /** * 一个没有出口的递归 */ public void m() { num++; m(); } public static void main(String[] args) { StackOverFlowErrorTest oom = new StackOverFlowErrorTest(); try { oom.m(); } catch (Throwable e) { System.out.println(oom.num); } } } // run once output:34623 // run twice output:29079
即使同样的代码,单线程运行多次的结果不一样,多线程下也都不同。我把上面的代码改一改,为了让栈帧更小,我把num变成类变量,打印出来的num会比之前更大一些。意思是虚拟机栈允许的最大深度不是固定的?当我们在m方法内增加一些参数和返回值,你会发现虚拟机栈的深度在越来越小,现在来看,我们的问题中“深度”应该是指多少兆,在JProfile中我也难以察觉任何和Stack相关的信息。或许这个参数还和我们虚拟机主机的配置有关,如果真的在问深度,那只能估算一般在1w-2w个栈帧,这个深度和栈帧的大小有很大关系。
测试了SOF异常之后,我们怎么测试让虚拟机栈抛出OOM异常?
// 代码段2.8 package outofmemoryerror; public class JavaVMStackOOM { private void dontStop() { while (true) { } } public void stackLeakByThread() { while (true) { Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
在研究常量池的时候就说过了,运行时常量池是动态的,我们可以使用String#intern方法动态的往运行时常量池中添加数据,使它抛出OOM【oom()在JDK6.0/7.0环境下抛出OOM,在JDK8.0下不抛出】
// 代码段2.9 package outofmemoryerror; import java.util.ArrayList; /** * -XX:PermSize=2M -XX:MaxPermSize=2M */ public class RuntimeConstantPoolOOM { /** * jdk6运行时常量池存储的是String对象,7存的是引用,8运行时常量池不在堆中 */ public static void oom() { ArrayList<String> list = new ArrayList(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } public static void main(String[] args) { oom(); } }
// 代码段2.10 E:\jdk\jdk1.6.0_45\bin\java.exe -XX:PermSize=2M -XX:MaxPermSize=2M "-javaagent:E:\IDEA\IntelliJ IDEA Community Edition 2020.2\lib\idea_rt.jar=58729:E:\IDEA\IntelliJ IDEA Community Edition 2020.2\bin" -Dfile.encoding=UTF-8 -classpath E:\jdk\jdk1.6.0_45\jre\lib\charsets.jar;E:\jdk\jdk1.6.0_45\jre\lib\deploy.jar;E:\jdk\jdk1.6.0_45\jre\lib\ext\dnsns.jar;E:\jdk\jdk1.6.0_45\jre\lib\ext\localedata.jar;E:\jdk\jdk1.6.0_45\jre\lib\ext\sunjce_provider.jar;E:\jdk\jdk1.6.0_45\jre\lib\ext\sunmscapi.jar;E:\jdk\jdk1.6.0_45\jre\lib\javaws.jar;E:\jdk\jdk1.6.0_45\jre\lib\jce.jar;E:\jdk\jdk1.6.0_45\jre\lib\jsse.jar;E:\jdk\jdk1.6.0_45\jre\lib\management-agent.jar;E:\jdk\jdk1.6.0_45\jre\lib\plugin.jar;E:\jdk\jdk1.6.0_45\jre\lib\resources.jar;E:\jdk\jdk1.6.0_45\jre\lib\rt.jar;E:\0_PROJECT\workspace\JVM\target\classes;E:\mvn\response\cglib\cglib\2.2.2\cglib-2.2.2.jar;E:\mvn\response\asm\asm\3.3.1\asm-3.3.1.jar outofmemoryerror.RuntimeConstantPoolOOM Error occurred during initialization of VM java.lang.OutOfMemoryError: PermGen space at sun.nio.cs.ext.GBK.newEncoder(GBK.java:36) at java.lang.StringCoding$StringEncoder.<init>(StringCoding.java:215) at java.lang.StringCoding$StringEncoder.<init>(StringCoding.java:207) at java.lang.StringCoding.encode(StringCoding.java:266) at java.lang.String.getBytes(String.java:946) at java.lang.ClassLoader$NativeLibrary.load(Native Method) at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1807) at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1724) at java.lang.Runtime.loadLibrary0(Runtime.java:823) at java.lang.System.loadLibrary(System.java:1028) at java.lang.System.initializeSystemClass(System.java:1086)
jdk6存在永久代,字符串常量池、运行时常量池、静态变量都是在永久代中;
jdk7存在永久代,字符串常量池和静态变量都移动到了堆当中,运行时常量池还是在永久代中;
jdk8不存在永久代,实现形式是元空间,字符串常量池和静态变量仍然在堆中,运行时常量池、类型信息、常量、字符、方法被移到到了元空间。
元空间与永久代类似,本质区别是元空间并不占用虚拟机内存,而是使用本地内存,由于本地内存一般都是比较大的,所以方法区就没有那么容易报OOM。
方法区存到是编译时类的信息,如果要让方法区抛出OOM异常,那我们需要不断的创建动态类,撑爆方法区,如下使用了GCLib类直接操作字节码运行时,生成了大量的动态类
// 代码段2.11 package outofmemoryerror; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * -XX:PermSize=2M -XX:MaxPermSize=2M */ public class JavaMethodAreaOOM { static class OOMOBject { } /** * jdk6,7还存在方法区,8已经不存在方法区,所以6,7会抛出OOM */ public static void oom() { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMOBject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invoke(objects, null); } }); enhancer.create(); } } public static void main(String[] args) { oom(); } }
// 代码段2.12 E:\jdk\jdk1.6.0_45\bin\java.exe -XX:PermSize=2M -XX:MaxPermSize=2M "-javaagent:E:\IDEA\IntelliJ IDEA Community Edition 2020.2\lib\idea_rt.jar=58776:E:\IDEA\IntelliJ IDEA Community Edition 2020.2\bin" -Dfile.encoding=UTF-8 -classpath E:\jdk\jdk1.6.0_45\jre\lib\charsets.jar;E:\jdk\jdk1.6.0_45\jre\lib\deploy.jar;E:\jdk\jdk1.6.0_45\jre\lib\ext\dnsns.jar;E:\jdk\jdk1.6.0_45\jre\lib\ext\localedata.jar;E:\jdk\jdk1.6.0_45\jre\lib\ext\sunjce_provider.jar;E:\jdk\jdk1.6.0_45\jre\lib\ext\sunmscapi.jar;E:\jdk\jdk1.6.0_45\jre\lib\javaws.jar;E:\jdk\jdk1.6.0_45\jre\lib\jce.jar;E:\jdk\jdk1.6.0_45\jre\lib\jsse.jar;E:\jdk\jdk1.6.0_45\jre\lib\management-agent.jar;E:\jdk\jdk1.6.0_45\jre\lib\plugin.jar;E:\jdk\jdk1.6.0_45\jre\lib\resources.jar;E:\jdk\jdk1.6.0_45\jre\lib\rt.jar;E:\0_PROJECT\workspace\JVM\target\classes;E:\mvn\response\cglib\cglib\2.2.2\cglib-2.2.2.jar;E:\mvn\response\asm\asm\3.3.1\asm-3.3.1.jar outofmemoryerror.JavaMethodAreaOOM Error occurred during initialization of VM java.lang.OutOfMemoryError: PermGen space at sun.nio.cs.ext.GBK.newEncoder(GBK.java:36) at java.lang.StringCoding$StringEncoder.<init>(StringCoding.java:215) at java.lang.StringCoding$StringEncoder.<init>(StringCoding.java:207) at java.lang.StringCoding.encode(StringCoding.java:266) at java.lang.String.getBytes(String.java:946) at java.lang.ClassLoader$NativeLibrary.load(Native Method) at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1807) at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1724) at java.lang.Runtime.loadLibrary0(Runtime.java:823) at java.lang.System.loadLibrary(System.java:1028) at java.lang.System.initializeSystemClass(System.java:1086)
在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除了使用GCLib字节码增强外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类,常用于Eclipse重新动态编译修改后的类而无需重启eclipse)等。
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样。
// 代码段2.13 package outofmemoryerror; import sun.misc.Unsafe; import java.lang.reflect.Field; public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws IllegalAccessException { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } }
// 代码段2.14 E:\jdk\jdk1.7.0_80\bin\java.exe -Xmx20M -XX:MaxDirectMemorySize=10M "-javaagent:E:\IDEA\IntelliJ IDEA Community Edition 2020.2\lib\idea_rt.jar=50415:E:\IDEA\IntelliJ IDEA Community Edition 2020.2\bin" -Dfile.encoding=UTF-8 -classpath E:\jdk\jdk1.7.0_80\jre\lib\charsets.jar;E:\jdk\jdk1.7.0_80\jre\lib\deploy.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\access-bridge-64.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\dnsns.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\jaccess.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\localedata.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\sunec.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\sunjce_provider.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\sunmscapi.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\zipfs.jar;E:\jdk\jdk1.7.0_80\jre\lib\javaws.jar;E:\jdk\jdk1.7.0_80\jre\lib\jce.jar;E:\jdk\jdk1.7.0_80\jre\lib\jfr.jar;E:\jdk\jdk1.7.0_80\jre\lib\jfxrt.jar;E:\jdk\jdk1.7.0_80\jre\lib\jsse.jar;E:\jdk\jdk1.7.0_80\jre\lib\management-agent.jar;E:\jdk\jdk1.7.0_80\jre\lib\plugin.jar;E:\jdk\jdk1.7.0_80\jre\lib\resources.jar;E:\jdk\jdk1.7.0_80\jre\lib\rt.jar;E:\0_PROJECT\workspace\JVM\target\classes;E:\mvn\response\cglib\cglib\2.2.2\cglib-2.2.2.jar;E:\mvn\response\asm\asm\3.3.1\asm-3.3.1.jar outofmemoryerror.DirectMemoryOOM Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at outofmemoryerror.DirectMemoryOOM.main(DirectMemoryOOM.java:15)
如上代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例并运行内存分配。因为,使用DirtecByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过即使得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。
对于线程私有内存程序计数器、本地方法栈、虚拟机栈,这三块内存随着线程生而生亡而亡,栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作。每一个栈帧中分配对象内存基本上在类结构确定下来时就是已知的,因此这几个区域的内存分配和回收都具备确定性,所以这几个区域内不需要过多考虑内存回收的问题,因为方法结束或线程结束时,内存就自然跟随着回收了。
而Java堆和方法区则不同,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象有哪些还“存活”着,哪些已经“死去”(即不可能在被任何途径使用的对象)。JVM是怎么判判定对象存活以及死亡的?
引用计数判断对象是否存活的算法是这样的:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能在被使用的。
客观的评价引用计数算法实现简单,判断效率也高,在大部分情况下它都是一个不错的算法,但是Java语言没有选用引用计数算法判断对象是否存活,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。
// 代码段3.1 package gc; /** * 引用计数,引用环问题 * VM options:-XX:+PrintGCDetails -XX:+UseSerialGC * -XX:+UseSerialGC表示使用串行垃圾收集器 */ public class ReferenceCountingGC { private Object instance = null; private static final int _1MB = 1024 * 1024; // 让对象创建的过程中在堆中创建点内存 private byte[] byteSize = new byte[2 * _1MB]; public static void main(String[] args) { ReferenceCountingGC referenceCountingGC1 = new ReferenceCountingGC(); ReferenceCountingGC referenceCountingGC2 = new ReferenceCountingGC(); referenceCountingGC1.instance = referenceCountingGC2; referenceCountingGC2.instance = referenceCountingGC1; System.gc(); } }
// 代码段3.2 D:\service\jdk\jdk1.7.0_80\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=54377:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.7.0_80\jre\lib\charsets.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\deploy.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\access-bridge-64.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\jaccess.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunec.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\zipfs.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\javaws.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jce.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfr.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfxrt.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jsse.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\management-agent.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\plugin.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\resources.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor [Full GC[Tenured: 0K->524K(86272K), 0.0070771 secs] 2069K->524K(125056K), [Perm : 2932K->2932K(21248K)], 0.0071671 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] Heap def new generation total 38912K, used 1731K [0x000000077c800000, 0x000000077f230000, 0x00000007a6a00000) eden space 34624K, 5% used [0x000000077c800000, 0x000000077c9b0d98, 0x000000077e9d0000) from space 4288K, 0% used [0x000000077e9d0000, 0x000000077e9d0000, 0x000000077ee00000) to space 4288K, 0% used [0x000000077ee00000, 0x000000077ee00000, 0x000000077f230000) tenured generation total 86272K, used 524K [0x00000007a6a00000, 0x00000007abe40000, 0x00000007fae00000) the space 86272K, 0% used [0x00000007a6a00000, 0x00000007a6a832e0, 0x00000007a6a83400, 0x00000007abe40000) compacting perm gen total 21248K, used 2971K [0x00000007fae00000, 0x00000007fc2c0000, 0x0000000800000000) the space 21248K, 13% used [0x00000007fae00000, 0x00000007fb0e6d38, 0x00000007fb0e6e00, 0x00000007fc2c0000) No shared spaces configured.
GC、Full GC、PSYoungGen、ParOldGen、PSPerGen这些是什么?
参见GC基础.note。
从日志信息中,我们可以清楚的看到调用System.gc会触发一次full gc,内存从2069k->524k,意味着虚拟机并没有因为这两个对象相互引用就不回收它们,这也侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。
Java是使用根搜索算法(GC Roots Tracing)判定对象是否存活的。这个算法的基本思路就是通过一系列的名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在上图中object5引用object6和object7但没有GC Roots对象作为起点,所以它们被判定为是可回收的对象。而object1~0bject4都可达GC Roots所有它们不会判定为可回收对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
Java虚拟机内部的引用,如常驻异常对象,系统类加载器,基本数据类型对应的Class对象
虽然这两个对象之间构成了相互引用环,但是在栈中依旧存在着这两个对象的reference,它们到GC Roots不应该是可达的吗?其实图3.2只是程序执行在main方法时的场景而已,在学习Java虚拟机栈的时候,我们就说过,Java虚拟机栈是Java方法执行的内存模型,在方法被执行时会在虚拟机栈创建一个栈帧,这个栈帧的本地变量表里存放我们的reference,但是当方法执行完之后,栈帧就被弹出了,所在的内存空间也被回收了,所以当main方法执行完之后,heap区的两个对象对于GC Roots来说就是不可达的,在下一次垃圾回收时,这两个对象都会被回收。
应用程序中的任何活动对象都不会引用对象A,可以把对象A理解为孤岛。
隔离岛是一组相互引用的对象,但是应用程序中的任何活动对象都不会引用它们。如上图3.1中Object5、Object6、Object7就是隔离岛。
到底什么是引用?引用的传统定义是:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。引用在JDK2之后又被分为强引用、软引用、弱引用、虚引用4种,这4种引用强度逐渐减弱。
强引用:即上面传统引用的定义,例如Object obj = new Object(),这个obj就是强引用,只要有强引用关系存在,垃圾收集器就不会回收被引用的对象。
软引用:描述一些还有用,但非必须的对象。只有在即将发生OOM异常前,会把这些对象列进回收范围中进行第二次回收,如果这次回收还没有足够的内存,才会抛出OOM异常。
弱引用:也是用来描述哪些非必须对象,比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否够用,都会回收弱引用关联的对象。
虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
// 代码段3.3 package gc; import java.util.ArrayList; /** * -Xms3M -Xmx3M -XX:+PrintGCDetails -XX:+UseParallelGC */ public class ReferenceTest { private Object instance = null; private static final int _1MB = 1024 * 1024; // 让对象创建的过程中在堆中创建点内存 private byte[] byteSize = new byte[2 * _1MB]; public byte[] getByteSize() { return this.byteSize; } public static void main(String[] args) { ArrayList<ReferenceTest> list = new ArrayList<ReferenceTest>(); list.add(new ReferenceTest()); list.add(new ReferenceTest()); list.add(new ReferenceTest()); Object o = new Object(); System.out.println(o.toString()); } } # GC信息 E:\jdk\jdk1.7.0_80\bin\java.exe -Xms3M -Xmx3M -XX:+PrintGCDetails -XX:+UseParallelGC "-javaagent:E:\IDEA\IntelliJ IDEA Community Edition 2020.2\lib\idea_rt.jar=64044:E:\IDEA\IntelliJ IDEA Community Edition 2020.2\bin" -Dfile.encoding=UTF-8 -classpath E:\jdk\jdk1.7.0_80\jre\lib\charsets.jar;E:\jdk\jdk1.7.0_80\jre\lib\deploy.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\access-bridge-64.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\dnsns.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\jaccess.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\localedata.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\sunec.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\sunjce_provider.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\sunmscapi.jar;E:\jdk\jdk1.7.0_80\jre\lib\ext\zipfs.jar;E:\jdk\jdk1.7.0_80\jre\lib\javaws.jar;E:\jdk\jdk1.7.0_80\jre\lib\jce.jar;E:\jdk\jdk1.7.0_80\jre\lib\jfr.jar;E:\jdk\jdk1.7.0_80\jre\lib\jfxrt.jar;E:\jdk\jdk1.7.0_80\jre\lib\jsse.jar;E:\jdk\jdk1.7.0_80\jre\lib\management-agent.jar;E:\jdk\jdk1.7.0_80\jre\lib\plugin.jar;E:\jdk\jdk1.7.0_80\jre\lib\resources.jar;E:\jdk\jdk1.7.0_80\jre\lib\rt.jar;E:\0_PROJECT\workspace\JVM\target\classes;E:\mvn\response\cglib\cglib\2.2.2\cglib-2.2.2.jar;E:\mvn\response\asm\asm\3.3.1\asm-3.3.1.jar gc.ReferenceTest [GC [PSYoungGen: 1413K->504K(2560K)] 5509K->4672K(7680K), 0.0023678 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [PSYoungGen: 504K->496K(2560K)] 4672K->4744K(7680K), 0.0014550 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 4248K->4632K(5120K)] 4744K->4632K(7680K) [PSPermGen: 3116K->3115K(21504K)], 0.0155193 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] [GC [PSYoungGen: 0K->0K(2560K)] 4632K->4632K(7680K), 0.0004165 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 4632K->4616K(5120K)] 4632K->4616K(7680K) [PSPermGen: 3115K->3115K(21504K)], 0.0092424 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] Heap PSYoungGen total 2560K, used 144K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000) eden space 2048K, 7% used [0x00000000ffd00000,0x00000000ffd24250,0x00000000fff00000) from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) ParOldGen total 5120K, used 4616K [0x00000000ff800000, 0x00000000ffd00000, 0x00000000ffd00000) object space 5120K, 90% used [0x00000000ff800000,0x00000000ffc820c0,0x00000000ffd00000) PSPermGen total 21504K, used 3199K [0x00000000fa600000, 0x00000000fbb00000, 0x00000000ff800000) object space 21504K, 14% used [0x00000000fa600000,0x00000000fa91fdd8,0x00000000fbb00000) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at gc.ReferenceTest.<init>(ReferenceTest.java:15) at gc.ReferenceTest.main(ReferenceTest.java:25)
在代码段3.3中可以清楚的看到,在程序运行到25行抛出了OOM异常,在查看GC信息一共发生了3次次要GC和两次主要GC,而每次GC的回收量极其少,还达不到一个对象的大小,当运行到24行时,堆内存即将满,假设集合里的对象属于软引用,那在此刻即将发生OOM时肯定会回收内存,但是代码段信息里看成并没有发生,也没有回收集合引用的对象,所以集合里的对象引用属于强引用。
在根搜索算法中不可达的对象,也并非是“非死不可”的,此刻它们处于“缓刑”阶段。真正的一个对象死亡至少需要经历两次标记过程:
第一次:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,则该对象会被第一次标记并且进行一次筛选。这个筛选是干什么呢?如果这个对象没有覆盖finalize方法或该对象的finalize方法已经被虚拟机调用过,就不在执行finalize方法。否则就判定这个对象有必要执行finalize方法。筛选就是判断哪些对象有必要执行finalize,哪些对象没必要。
第二次:如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要做finzlize()中拯救自己,只需重新与引用链上任何一个对象建立关联即可,那它就会在第二次标记时移除“即将回收”的集合;如果这时候还没逃脱,那基本上它就真的要被回收了。
// 代码段3.4 package gc; public class TwiceMarkTest { public static TwiceMarkTest SAVE_HOOKA = null; public void isAlive() { System.out.println("yes,i am still alive "); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); TwiceMarkTest.SAVE_HOOKA = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOKA = new TwiceMarkTest(); SAVE_HOOKA = null; System.gc(); Thread.sleep(500); if (SAVE_HOOKA != null) { SAVE_HOOKA.isAlive(); } else { System.out.println("no, i am dead A"); } } }
如上代码重写了finalize方法,所以这个对象有必要执行finalize方法,在即将进行第二次标记前,先执行了finalize方法,该对象重新和GC Roots链接上,所以它逃生了。
疑问:没有必要执行finalize方法的对象也需要经历第二次标记?
对于上面说的,只有在这个对象被判定有必要执行finalize方法时才会进行第二次标记,如果没有呢?还需要第二次标记吗?
在问这个问题的时候,我又多问了一句,在什么情况下才会没必要执行finalize方法,这个有两种可能,第一该对象没重写finalize方法,第二该对象的finalize方法已经被虚拟机调用过。对于第二种情况肯定会至少被二次标记,但对于第一种呢?如果说一个对象被回收前必须经过两次标记,那对于第一种来说如果判定它没必要执行finalize()方法,此刻进行二次标记(这或许是合理的解释),然后丢进“即将回收”的集合。如果有必要执行finalize方法,收集器会对F-Queue中对象进行第二次标记,然后丢进“即将回收”的集合。
回收废弃常量和堆内对象的回收类似。以常量池中字面量的回收为例,例如String str = "abc";当没有任何地方引用了这个常量,如果在这个时候发生了内存回收,而且必要的话,这个常量会被回收。之前在谈常量的时候说了,字面量会在堆内存创建一个实例,把引用驻留在运行时常量池,而常量池里恰恰又是GC Roots对象之一,那估计也就只有程序终止时常量才会回收。
回收无用类的条件很苛刻,需要满足下面3个条件才"可以"被回收:
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
// 控制是否对类进行回收,加了该参数表示true -Xnoclassgc // 可以使用如下指令查看类的加载和卸载信息 -verbose:class [-XX:+TraceClassLoading] [-XX:+TraceClassUnLoading] // -XX:+TraceClassLoading可以在Product版虚拟机中使用,-XX:+TraceClassUnLoading // 需要在fastdebug版虚拟机才能使用
上面介绍的引用计数算法和根搜索算法是判定对象是否存活的算法,它们并不是垃圾收集算法!
标记-清除算法是最基础的收集算法,后续的算法都是基于这个思路并对其缺点改进而得到的。它的算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集操作。
以上图为例分析标记-清除算法,上图中包括循环引用的对象(Object5、Object6、Object7组成的隔离岛)和孤岛(Object9)。
标记清除算法从GC根开始遍历所有对象引用,并将找到的每个对象标记为活动。(对象1、2、3、4、8)
回收未被标记对象占用的所有堆内存。它们被简单地标记为空闲,基本上清除了未使用的对象。(对象9)
如果两个或多个对象相互引用,但没有与任何跟链接的对象引用,则它们(隔离岛中的对象)也被清除。(对象5、6、7)
为了解决标记-清除算法的效率问题,一种称为“复制”的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。
目前商业的虚拟机都采用了复制算法来回收新生代,只不过不是按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor,HotSport默认Eden和Survivor的大小比例是8:1,也就是说新生代可用空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被浪费的。具体使用方式参见:GC基础.note
有一点需要说明的是在GC基础笔记中在survivor区域不断增长生命的对象中,当这些对象达到一定的阈值就会被分配到老年代,但是如果出现了survivor区域空间不足时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)。这个分配担保和单纯的对象达到阈值进入老年代不是一个概念。分配担保意思是当survivor区域不足以分配空间时,这些对象先进入老年代。
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。如果是按照50%的空间进行复制收集,如果不想浪费另外50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,对于新生代来说可以使用这种收集算法,但对于老年代来说的话没有人作为老年代的分配担保,所以老年代一般不能直接选用复制收集算法。
对于老年代的垃圾回收,根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法。标记过程仍然和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法(Generational Collection)并不是一种收集算法,只是一种垃圾收集的思想,只是根据对象的存活周期的不同将内存划分为几块。如我们所见的一般划分为新生代和老年代,根据各个年代的特点采用最适合的收集算法。在新生代,大量的对象的存活时间很短,只有少量的存活,那就使用复制算法。而老年代因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记 - 清除”或“标记 - 整理”算法来进行回收。
垃圾收集算法只是内存回收的方法论,垃圾收集器则是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现没有任何规定,不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。如下图HotSport虚拟机的垃圾收集器多达7种,每一种都有自己特定的应用场景,没有哪一种是万能的。
如上图所示,如果两个收集器之间存在连线,就说明它们可以搭配使用。
Serial+CMS、和ParNew+Serial Old在JDK 8时被声明为废弃,并在JDK 9中完全取消了对这些组合的支持。
Serial收集器根据名称可知是一个单线程的收集器,它是历史最悠久的,曾经是虚拟机新生代收集的唯一选择【JDK1.3.1之前】。它的“单线程”的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(即“Stop The World”),直到它收集结束。“Stop The World”这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说难以接受。
从JDK1.3开始,一直到现在最新的JDK13,HotSport虚拟机开发团队一直不停的为消除或者降低用户线程因垃圾收集而导致停顿做努力,从Serial到Parallel,再到CMS和GI收集器,一个个越来越构思精巧,优秀的垃圾收集器不断涌现,用户线程的停顿时间在持续缩短,但是仍然没有办法彻底消除。
不断有更好的垃圾收集器出现,为什么还保留Serial收集器呢?它究竟有何亮点?迄今为止,Serial收集器依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,它优于其他垃圾收集器的地方就是简单而高效,对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅指新生代使用内存),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁的发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。
对比ParNew和Serial收集器图【在本文研究的JDk时,ParNew/Serial还是可以搭配的】,ParNew收集器有多个GC线程,而Serial只有一个。ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器,还有一个重要原因是目前只有它能与CMS(Concurrent Mark Sweep)收集器配合工作。注意CMS的第一个单词Concurrent,在对比ParNew的第一个单词Parallel,一个是并发、一个是并行。
【如果某个系统支持两个或者多个动作同时存在,那么这个系统就是一个并发系统。如果两个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。在并发程序中可以拥有两个或者多个线程,这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入换出内存。这些线程是同时存在的。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。
并发不一定同时执行,也可以交替执行,但执行并发的线程都要存活着才叫并发。
并行一定是并发,且线程是同时执行的。】摘自知乎。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发(Concurrent):指用户线程与垃圾收集器同时执行(但不一定是并行的,可能会交替执行),用户程序继续运行,而垃圾收集程序运行与另一个CPU上。
细细的品,为啥并行是垃圾收集线程之间并行,而不能是垃圾收集线程和用户线程并行呢?这样并行的话,相对于并发中用户线程和垃圾收集线程采用并发方式增加线程相互切换的资源消耗不更好吗?话说回来,上面垃圾收集器所指定并行并发关系对象有三个及其以上(垃圾收集线程A,垃圾收集线程B,用户线程),在并发里只是说了用户线程和垃圾收集器之间是并发,并没有说垃圾收集器线程之间的工作方式是并发还是并行。在回想Stop The World事件,指的是在进行垃圾收集时,停止一切用户线程,那这样来看用垃圾收集线程并行方式实现的并行垃圾收集器并不能解决Stop The World事件,它的思想理念只是为了加快垃圾收集的速度,减少Stop The World的时间。而并发垃圾收集器好像虽能减少每次Stop The World事件时间,但拉长了整个垃圾收集的时间。如果能并发垃圾收集了,我想也没必要再让垃圾收集器线程之间进行并行了,垃圾收集本来就作为一个较低优先级的线程,在已经能解决Stop The World事件的情况下,应尽量减少分配更多的线程(资源)给它做并行了。
在图3.10上看出CMS老年代垃圾收集器只能和新生代ParNew垃圾收集器配合使用,即ParNew收集器是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。
ParNew收集器是并行垃圾收集器,若在单CPU的环境中的,若要使用并行,必须要多核,怎么办?ParNew收集器会通过超线程技术实现两个CPU环境,在其中运行,即使是这样都不能百分百保证超越Serial收集器,所以单核环境下还是使用Serial收集器吧。如果是多核的话,ParNew收集器优于Serial收集器,多核环境下,默认开启的线程数和CPU的数量相同,当然我们也可以使用-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
Scavenge的意思是清道夫,由图3.10知它也是一个新生代收集器,且是并行垃圾收集器【JDK1.4.0出现】。它也是使用复制算法的收集器,这个收集器和ParNew、CMS不同点是关注点不同,上面说了ParNew和CMS关注点是Stop The World的暂停时间问题,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
如果虚拟机完成某个任务,用户代码加上垃圾收集器总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合需要与用户交互或者保证服务响应质量的程序,良好的响应速度能提升用户体验,如web服务。【ParNew、CMS】;
而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。【Parallel Scavenge】
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数及直接设置吞吐量大小的-XX:GCTimeRatio参数。
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。注意GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,你设置的很小垃圾收集速度虽然会变得更快,垃圾收集的频率在增加,那样的话你的吞吐量也会下降。
GCTimeRatio参数的值应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。???吞吐量的倒数?这里描述的怕是有些问题,官网解释如下:吞吐量的目标是根据进行垃圾收集所花费的时间与在垃圾收集之外所花费的时间(称为应用程序时间)来衡量的(如上公式)。该目标由命令行选项-XX:GCTimeRatio=<N>指定,该选项将垃圾回收时间与应用程序时间的比率关系设置为1 / ( 1 + <N> ),得到的值即为垃圾收集时间占总时间(垃圾收集时间+应用程序时间)的比值。例如,-XX:GCTimeRatio = 19,根据公式得:垃圾回收时间占总时间的 5%。默认GCTimeRatio=99,即垃圾回收时间占总时间1%。
还有一个重要的参数-XX:+UseAdaptiveSizePolicy参数,表示开启GC自适应的调节策略。当开启时无需在手工设定-Xmn、-XX:SurvivorRatio、-XX:PretrnureSizeThreshold(晋升老年代对象年龄)等细节参数了,虚拟机会根据运行情况搜集性能监控信息,动态调整这些参数以提高合适的停顿时间或最大吞吐量。你只需要设置好-Xmx,再设置给虚拟机一个优化目标(设置上MaxGCpauseMillis和GCTimeRatio参数),然后设置上该参数即可。
Seial Old是Serial收集器的老年代版本,单线程收集器,使用“标记-整理”算法【JDK1.3】。它和Serial收集器一样也是被Client模式下的虚拟机使用。如果是在Server模式下,它主要还有两大用途:一个是与Parallel Scavgeng收集器搭配使用,另一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用,详细看图3.10。Serial Old收集器工作过程如下:
Parallel Old是Parallel Scavenge收集器的老年代版本,看名字我本来还以为它是ParNew收集器的老年代版本呢,这名字起的容易让人混【JDK1.6出现】。Parallel是并行的即使用多线程,它是使用多线程和“标记 - 整理”算法,看图3.10可知,这个收集器实在JDK1.6才出现的,在这之前如果新生代选用Parallel Scavenge收集器在没有Parallel Old收集器的情况下只能选用Serial Old收集器,Serial Old收集器是单线程的,性能比较底下,搭配Parallel Scavenge在吞吐量未必能在在整个应用上获得吞吐量最大化效果。所以知道这个收集器出现后Parallel Scavenge收集器搭配Parallel Old收集器才算是比较名副其实的“吞吐量优先”收集器。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务器上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,CMS非常符合这一类应用的需求。
Mark Sweep即CMS收集器是基于“标记 - 清除”算法实现的。
从上图可以看出,它经历了四个步骤:初始标记-》并发标记-》重新标记-》并发清理。其中初始标记和重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象(也就是遍历深度只有1,如下图GC Roots到a),速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行(如下图a-b-c);而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清除删掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。虽然不是每个过程都是并发的,但是那些过程耗时都比较短,整体可看作是并发的。
并发虽降低了Stop The World的时间,但却带来了CPU资源的消耗,在并发阶段,虽不会导致用户线程停顿,但是会因为占用一部分线程而导致应用程序变慢,总吞吐量会降低。
CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序的运行自然还有新的垃圾不断产生,这一部分垃圾出现在标记之后,所以CMS无法在本次收集中回收它们,只好留待下一次GC时在将它们回收,这一部分垃圾就称为浮动垃圾。由于用户线程还在运行,那肯定是要预留一部分空间给用户线程产生垃圾的,在默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活,如果在应用中老年代增长不是太快,可以适当调高-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数以获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,停顿时间会很长。如果-XX:CMSInitiatingOccupancyFraction设置太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
CMS作为一款“标记-清除”算法实现的收集器,该算法的缺点是收集结束时会产生大量空间碎片。空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费附送一个碎片整理过程,内存整理的过程是无法并发的。也就是说整个CMS垃圾收集过程会变长。虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。
G1(Carbage First)收集器是当前收集器技术发展的最前沿成果,从JDK6开始试用,JDK7正式发布。G1收集器与CMS收集器相比有两个显著的改进:一是G1收集器是基于“标记-整理”算法实现的收集器,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。二是它可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗的垃圾收集上的时间不得超过N毫秒。
G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或者老年代,而G1将整个Java堆划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage Frst名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC,我们以Serial/Serial Old收集器验证内存分配和垃圾收集策略。
我们可以使用虚拟机提供的参数-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前各区域的分配情况。
/** * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * 测试1:-XX:+UseSerialGC =>Serial+Serial Old * 测试2:-XX:+UseParNewGC =>ParNew+Serial Old */ public class GCMonitor { private static final int _1MB = 1024 * 1024; public static void testAllocation() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[3 * _1MB]; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { GCMonitor.testAllocation(); } }
D:\service\jdk\jdk1.7.0_80\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=54377:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.7.0_80\jre\lib\charsets.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\deploy.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\access-bridge-64.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\jaccess.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunec.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\zipfs.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\javaws.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jce.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfr.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfxrt.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jsse.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\management-agent.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\plugin.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\resources.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor [GC[DefNew: 7652K->519K(9216K), 0.0043070 secs] 7652K->6663K(19456K), 0.0043552 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4865K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 53% used [0x00000000f9a00000, 0x00000000f9e3e630, 0x00000000fa200000) from space 1024K, 50% used [0x00000000fa300000, 0x00000000fa381fb8, 0x00000000fa400000) to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000) tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000) compacting perm gen total 21248K, used 2943K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 13% used [0x00000000fae00000, 0x00000000fb0dfce0, 0x00000000fb0dfe00, 0x00000000fc2c0000) No shared spaces configur7652ed.
测试2:-XX:UseParNewGC,ParNew/Serial Old收集组合和Serial/Serial Old类似:
D:\service\jdk\jdk1.7.0_80\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=54377:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.7.0_80\jre\lib\charsets.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\deploy.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\access-bridge-64.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\jaccess.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunec.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\zipfs.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\javaws.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jce.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfr.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfxrt.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jsse.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\management-agent.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\plugin.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\resources.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor [GC[ParNew: 7652K->560K(9216K), 0.0025610 secs] 7652K->6704K(19456K), 0.0025971 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap par new generation total 9216K, used 4905K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 53% used [0x00000000f9a00000, 0x00000000f9e3e5f0, 0x00000000fa200000) from space 1024K, 54% used [0x00000000fa300000, 0x00000000fa38c040, 0x00000000fa400000) to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000) tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000) compacting perm gen total 21248K, used 2936K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 13% used [0x00000000fae00000, 0x00000000fb0de290, 0x00000000fb0de400, 0x00000000fc2c0000) No shared spaces configured.
如以上代码段的testAllocation()方法中,尝试分配3个2MB大小和1个4MB大小的对象,在运行时通过-Xms20M、-Xmx20M和-Xmn10M这三个参数限制Java堆大小为20MB,且不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8比1,从输出的结果也能清晰地看到:“eden space 8192k、from space 1024k、to space 1024k”的信息,新生代总可用空间为9216k(即Eden区8192k + 一个Survovor区的总容量1024k)。
执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代7652KB变成了519KB,而总内存占用量由7652KB变成了6663KB减少的并不多(因为allocation1、2、3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法进入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。
这次GC结束后,4MB的allocation4对象被顺利分配在Eden中。因此程序执行完的结果是Eden占用4MB(被allocation4占用),Survivor空闲,老年代被占用6MB(被allocation1、2、3占用)。通过GC日志可以证实这一点。
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次ideMinor GC(但非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
大对象是指:需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组(如上面代码例子中byte数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(写程序时应当避免“朝生夕灭”的“短命大对象”),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
虚拟机提供了-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝。
/** * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * -XX:+UseSerialGC =>Serial+Serial Old */ public class GCMonitor { private static final int _1MB = 1024 * 1024; /** * 测试:大对象直接进入老年代 * -XX:PretenureSizeThreshold=3145728 大于3MB的对象直接进入老年代中分配 */ public static void testPretenureSizeThreshold() { byte[] allocation4; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { GCMonitor.testPretenureSizeThreshold(); } }
D:\service\jdk\jdk1.7.0_80\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=54377:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.7.0_80\jre\lib\charsets.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\deploy.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\access-bridge-64.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\jaccess.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunec.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\zipfs.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\javaws.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jce.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfr.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfxrt.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jsse.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\management-agent.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\plugin.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\resources.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor Heap def new generation total 9216K, used 1672K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 20% used [0x00000000f9a00000, 0x00000000f9ba2238, 0x00000000fa200000) from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000) to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000) tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000) compacting perm gen total 21248K, used 2945K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 13% used [0x00000000fae00000, 0x00000000fb0e0a30, 0x00000000fb0e0c00, 0x00000000fc2c0000) No shared spaces configured.
如上测试,当我们设置byte数组为4MB时,老年代占用40%,当设置为6MB时,老年代占用60%,enden space一直为20%。也就是说新生代并没有被byte数组占用,4MB的allocation4直接就分配在老年代中,这是因为我们把PretenureSizeThreashold设置为了3MB,超过3MB的对象直接进入老年代。
注意:PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。
虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应当放在老年代中。虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过-XX:MaxTrnuringThreshold来设置。
/** * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * -XX:+UseSerialGC =>Serial+Serial Old */ public class GCMonitor { private static final int _1MB = 1024 * 1024; /** * -XX:MaxTenuringThreshold=1 | 15 * -XX:+PrintTenuringDistribution */ public static void testTenuringThreshold() { // 什么时候进入老年代取决于MaxTenuringThreshold的设置 byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; } public static void main(String[] args) { GCMonitor.testTenuringThreshold(); } }
D:\service\jdk\jdk1.7.0_80\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=54377:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.7.0_80\jre\lib\charsets.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\deploy.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\access-bridge-64.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\jaccess.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunec.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\zipfs.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\javaws.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jce.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfr.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfxrt.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jsse.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\management-agent.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\plugin.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\resources.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor [GC[DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 793792 bytes, 793792 total : 5860K->775K(9216K), 0.0029176 secs] 5860K->4871K(19456K), 0.0029666 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC[DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 224 bytes, 224 total : 5039K->0K(9216K), 0.0011978 secs] 9135K->4868K(19456K), 0.0012212 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e227b0, 0x00000000fa200000) from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa2000e0, 0x00000000fa300000) to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000) tenured generation total 10240K, used 4868K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 47% used [0x00000000fa400000, 0x00000000fa8c1288, 0x00000000fa8c1400, 0x00000000fae00000) compacting perm gen total 21248K, used 2936K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 13% used [0x00000000fae00000, 0x00000000fb0de298, 0x00000000fb0de400, 0x00000000fc2c0000) No shared spaces configured.
D:\service\jdk\jdk1.7.0_80\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=54377:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.7.0_80\jre\lib\charsets.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\deploy.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\access-bridge-64.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\jaccess.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunec.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\ext\zipfs.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\javaws.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jce.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfr.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jfxrt.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\jsse.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\management-agent.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\plugin.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\resources.jar;D:\service\jdk\jdk1.7.0_80\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor [GC[DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 794248 bytes, 794248 total : 5860K->775K(9216K), 0.0030067 secs] 5860K->4871K(19456K), 0.0030526 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC[DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 336 bytes, 336 total : 5040K->0K(9216K), 0.0012910 secs] 9136K->4869K(19456K), 0.0013166 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e22800, 0x00000000fa200000) from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200150, 0x00000000fa300000) to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000) tenured generation total 10240K, used 4868K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 47% used [0x00000000fa400000, 0x00000000fa8c1348, 0x00000000fa8c1400, 0x00000000fae00000) compacting perm gen total 21248K, used 2946K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 13% used [0x00000000fae00000, 0x00000000fb0e0b50, 0x00000000fb0e0c00, 0x00000000fc2c0000) No shared spaces configured.
在JDK1.7.0_80上测试,当MaxTrnuringThreshold=15得到的结果比不像书中使用那般,不同的虚拟机实现的垃圾收集方式有很大差别,具体原因暂时不明白。考虑到可能是不同版本导致的影响,我们切换为jdk1.6.0.45【没找到书中作者使用的7u6版本,只好用早一点的版本替代了】,再次尝试:
D:\service\jdk\jdk1.6.0_45\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=58127:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.6.0_45\jre\lib\charsets.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\deploy.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\javaws.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\jce.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\jsse.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\management-agent.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\plugin.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\resources.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 453200 bytes, 453200 total : 5188K->442K(9216K), 0.0031539 secs] 5188K->4538K(19456K), 0.0031893 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 232 bytes, 232 total : 4950K->0K(9216K), 0.0006420 secs] 9046K->4538K(19456K), 0.0006698 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e22848, 0x00000000fa200000) from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa2000e8, 0x00000000fa300000) to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000) tenured generation total 10240K, used 4538K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 44% used [0x00000000fa400000, 0x00000000fa86e950, 0x00000000fa86ea00, 0x00000000fae00000) compacting perm gen total 21248K, used 3458K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 16% used [0x00000000fae00000, 0x00000000fb1609e0, 0x00000000fb160a00, 0x00000000fc2c0000) No shared spaces configured.
D:\service\jdk\jdk1.6.0_45\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=58145:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.6.0_45\jre\lib\charsets.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\deploy.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\javaws.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\jce.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\jsse.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\management-agent.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\plugin.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\resources.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 453296 bytes, 453296 total : 5188K->442K(9216K), 0.0025873 secs] 5188K->4538K(19456K), 0.0026184 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 208 bytes, 208 total - age 2: 452736 bytes, 452944 total : 4950K->442K(9216K), 0.0004593 secs] 9046K->4538K(19456K), 0.0004752 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4676K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e22840, 0x00000000fa200000) from space 1024K, 43% used [0x00000000fa200000, 0x00000000fa26e950, 0x00000000fa300000) to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000) tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000) compacting perm gen total 21248K, used 3469K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 16% used [0x00000000fae00000, 0x00000000fb165c10, 0x00000000fb165e00, 0x00000000fc2c0000) No shared spaces configured.
当我们把MaxTrnuringThreshold设置为1时,allocation1对象需要256KB的内存空间,Survivor空间可以容纳,当allocation2进入Eden时此时Eden空间内有allocation1和allocation2,Eden区域还没满,并没有触发Minor GC,当allocation3创建时,发现Eden剩余的空间不足以容纳allocation1、2、3,所以触发Minor GC,在GC过程中发现Survivor区域只有1M,不足以容纳allocation2,所以将allocationn2担保到老年代,此刻Eden空间只有allocation3,Survivor1空间有allocation1且年龄为1,allocation2在老年代。当执行到最后一行时allocation4要进入Eden区域,由于allocation3此刻存在与Eden区域,且占用了4MB,Eden总共8MB,即使Eden里还剩余4MB,但着4MB如果不是连续的内存空间依旧无法直接分配给allocation4,怎么办?再次触发Minor GC,此刻发现allocation3以及没有任何引用指向它,所以它直接被回收了并没有进入Survivor2,Survivor1里的依旧存活的且年龄不超过阈值的也将进入Survivor2,但是allocation1的年龄已经为1了,所以它不能进入Survivor2,最终将它移自老年代并清空Survivor1。所以最后的结果是:Eden存在allocation4,老年代存在allocation1和allocation2。
当我们把MaxTrnuringThreshold设置为15时,Survivor1里存活的allocation1经过第二次Minor GC时年龄不超过15,所以并不会进入老年代,而是进入Survivor2。所以最终结果时Eden区域存在allocation4,Survivor存在allocation1,allocation2存在于老年代。
在上面一章节我们遇到了当JDK1.7.0_18时,设置MaxTrnuingThreshold并不起作用的问题,在这一章节看能否解决疑惑。
为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTrnuingThreshold才晋升老年代,如果在Survivor空间中相同年龄对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
再次回头看我们上一章节JDK1.7.0_18时的那个文件,如果按照本章节解释的话,在allocation4分配时,allocation1并没有超过Survivor空间的一半,所以解释不通啊,动态年龄判断并不足以解决上一章节遇到的疑惑。
/** * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * 测试1:-XX:+UseSerialGC =>Serial+Serial Old * 测试2:-XX:+UseParNewGC =>ParNew+Serial Old */ public class GCMonitor { private static final int _1MB = 1024 * 1024; /** * -XX:MaxTenuringThreshold=15 * -XX:+PrintTenuringDistribution */ public static void testTenuringThreshold2() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { GCMonitor.testTenuringThreshold2(); } }
D:\service\jdk\jdk1.6.0_45\bin\java.exe -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution -XX:+UseSerialGC -javaagent:D:\soft\developerIDEA\IntelliJ\lib\idea_rt.jar=58650:D:\soft\developerIDEA\IntelliJ\bin -Dfile.encoding=UTF-8 -classpath D:\service\jdk\jdk1.6.0_45\jre\lib\charsets.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\deploy.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\dnsns.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\localedata.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\sunjce_provider.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\ext\sunmscapi.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\javaws.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\jce.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\jsse.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\management-agent.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\plugin.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\resources.jar;D:\service\jdk\jdk1.6.0_45\jre\lib\rt.jar;D:\workspace\java\JVM\out\production\JVM GCMonitor [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 715456 bytes, 715456 total : 5444K->698K(9216K), 0.0025423 secs] 5444K->4794K(19456K), 0.0025708 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 208 bytes, 208 total : 5206K->0K(9216K), 0.0007338 secs] 9302K->4794K(19456K), 0.0007689 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e227f0, 0x00000000fa200000) from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa2000d0, 0x00000000fa300000) to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000) tenured generation total 10240K, used 4794K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 46% used [0x00000000fa400000, 0x00000000fa8ae8a0, 0x00000000fa8aea00, 0x00000000fae00000) compacting perm gen total 21248K, used 3463K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 16% used [0x00000000fae00000, 0x00000000fb161ff8, 0x00000000fb162000, 0x00000000fc2c0000) No shared spaces configured. Process finished with exit code 0
对于上一章节MaxTrnuingThreshold=15的测试可发现,在本次测试中Survivor区域的使用率为0%,也就是说并没有被占用,究其原因,直到allocation4第一次进入Eden时,allocation1和allocation的Age为1,但是Eden不足以分配allocation4,发生Minor GC,allocation3进入老年代,allocation1和allocation2所占总空间为512KB为Survivor空间的一半,此刻年龄大于或等于1的对象可直接进入老年代,所以allocation1和allocation2对象直接进入了老年代。
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为一次Full GC。
这里的每次晋升到老年代的平均大小是几个意思?为什么取平均大小而不是每一次的总大小呢?
关于这个问题,我查看了虚拟机第三版关于本章节的描述:在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。
前面说新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时(最极端的就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来,在实际完成内存回收之前是无法明确知道的,所以只好取之前每次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,假如某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
在JDK 6 Update 24之后,-XX:HandlePromotionFailure参数已经被废弃,在JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。我本机的JDK6和JDK7都演示不了书上的效果,所以截图验证: