快速入门
JVM位置
JVM运行在操作系统上,和硬件没有交互关系。
JVM结构图
方法区:存储已被虚拟机加载的类元数据信息(元空间)
堆:存放对象实例,几乎所有的对象实例都在这里分配内存
虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息
程序计数器:当前线程所执行的字节码的行号指示器
本地方法栈:本地方法栈则是为虚拟机使用到的Native方法服务。
类加载器ClassLoader
负责加载class文件,文件的入口。
- 启动类加载器(Bootstrap):c++编写的(在java中无法获取到这个类加载器,被这个类加载器加载的类生成的对象,getClassLoader()的返回值是null),负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class!
- 扩展类加载器(Extension): java编写的,负责加载$JAVA_HOME中jre/lib/**ext/*.jar 里所有的class!
- 应用类加载器(AppClassLoader):java编写的,负责加载classpath中指定的jar包以及目录中class
- 用户自定义加载类:是Java.lang.ClassLoader的子类,用户可以定制类的加载方式。
前三种类加载器都是虚拟机自带的加载器。
工作过程
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
双亲委派模型
类加载器这种工作模式,其实就是双亲委派模型。简单来说:如果一个类加载器受到了类加载的请求,它首先不会自己区尝试加载这个类,而是把请求委托给父加载器区完成,依次向上。
好处:可以防止内存中出现多份同样的字节码(安全性角度)。
比如加载位于rt.jar保重的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
沙箱安全机制
不允许用户重写扩展类或启动类加载器中的类!
执行引擎Execution Engine
作用:解释命令,交给操作系统运行!
本地接口Native Interface
方法有Native修饰,这个方法是由C,C++语言编写的。
本地方法栈Native Method Stack
执行引擎发现有本地方法时,加载本地方法库。
PC寄存器
用来存储指向下一条指令的地址。
是线程私有的。
执行引擎在执行的时候,读取的PC寄存器的指令地址,可以忽略不计(分配的内存很小)。
方法区Method Area
方法区是被所有线程共享的。也就是共享区间。
存储数据:
静态变量+常量+类信息(构造方法/接口定义)+运行时常量池
实例变量存在堆内存中,和方法区无关。
栈Stack
介绍
栈也叫栈内存,主要管理的是Java程序的运行,是在线程创建时创建的,它的生命期是跟随线程的生命期。对于栈来说不存在垃圾回收问题,只要线程一结束,栈也就没了。属于是线程私有的。
八种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
存储内容
栈中的数据都是以栈帧的格式存在的。
栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数的数据集。
栈帧中主要保存三类数据:
- 本地变量:输入参数和输出参数以及方法内的变量。
- 栈操作:记录出栈、入栈的操作。
- 栈帧数据:包括类文件、方法等。
运行原理
其实就是栈的特点,“先进后出”。
每调用一个方法就产生一个栈帧,然后被压入到栈中的顶部,顶部栈就是当前的方法,该方法执行完毕后就会自动将这个栈帧出栈。
在调用递归时,可能由于不正确的while(参数)导致栈溢出,会报Exception in thread "main" java.lang.StackOverflowError错误。
堆Heap
堆、栈、方法区的关系
HotSpot是使用指针的方式来访问对象:
- Java堆中会存放访问类元数据的地址
- reference存储的就是对象的地址
JVM种类
- Sun公司的HotSpot
- BEA公司的JRockit
- IBM公司的J9 VM
堆体系
在JAVA7之前:
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、长变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存逻辑上分为三部分:
- 新生区(年轻代)
- 养老去(老年代)
- 永久区(持久代、永久代)
新生区
一个对象在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为:伊甸区,幸存者区。
所有对象都在伊甸区被new出来。
幸存区有两个:From区和To区。
当伊甸区的空间用完的时候,程序又要创建新对象,JVM就会对伊甸区进行垃圾回收,将其中不再被其他对象引用的对象销毁,剩余的对象就会被移动到幸存区(From区)。这个过程叫做Minor GC。
详细过程
- 伊甸区,from区复制到to区,年龄+1,首先,当伊甸区满的时候会触发第一次GC,把还活着的对象拷贝到幸存者from区,当伊甸区再次触发GC的时候扫描伊甸区和幸存from区,对这两个区域进行垃圾回收,经过这次回收后依然存活的对象,就直接复制到To区(如果有对象的年龄已经达到了老年的标准,就复制到老年代区),同时把这些对象的年龄+1
- 清空伊甸区,幸存from区,然后清空伊甸区和from区中的对象
- To和from互换,原to区成为下一次GC时的From区,部分对象会在from和to中来回复制,这样交换15次(这个数值是由JVM参数MaxTenuringThreshold决定的,默认是15)。如果还活着,那就存到老年代里。
- 大对象特殊情况。如果分配的新队像比较大,伊甸区放不下,但是老年区可以放下的时候,对象会直接被分配到老年代了。
MinorGC的过程:复制 -> 清空 -> 互换
老年代
这里存放的都是经过多次GC依然存在的对象(默认是15次),老年代的对象都比较稳定,不会频繁的GC。
如果养老区也满了,这个时候会产生MajorGC(FullGC),来清理养老区的内存。
如果养老区执行了FullGC之后发现依然无法进行新对象的保存,就会产生异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明是Java虚拟机的堆内存不够。
OOM原因分析
java.lang.OutOfMemoryError: Java heap space异常
- Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整
- 代码中创建了大量的大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
永久代
现在不再有永久代了,取代它的是元空间。
就是一个常驻内存区域。
存什么?
JDK自带的Class、Interface的元数据,也就是说它存储的是运行环境必须的类信息。
被装在进这个区域的数据是不会被GC回收。
直到关闭JVM才会释放此区域所占的内存。
OOM原因分析
如果出现的是java.lang.OutOfMemoryError: PermGen space异常。
说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。
比如说:在一个Tomcat下部署了他多的应用。或者大量的动态反射生成的类不断被加载。导致Perm区(永久代)被占满。
特别说明
jdk1.6以及之前:有永久代。1.6时常量池在方法区。
jdk1.7:有永久代,但是已经逐步失去永久代。1.7时常量池在堆。
jdk1.8以及以后:没有永久代了,1.8的常量池在堆里。
永久代和元空间(方法区)
对于HotSpot型虚拟机。严格本质上两者是不同的,或者说是使用永久代来实现方法区而已。永久代是方法区的一个实现。
实际上,方法区和堆一样,是共享区间。
虽然JVM规范将方法区描述为堆的一个逻辑部分,但它还有一个别名叫做Non-Heap,目的就是要和堆分开。
永久代使用的是JVM的堆内存,但是java8以后的元空间(方法区)并不在虚拟机中而是使用本机物理内存。
所以默认情况下,元空间(方法区)的大小只受本地内存限制,不过JVM也提供了参数来控制它的使用。
堆的参数优化
以JDK1.8+HotSpot为例
jdk1.7
jdk1.8
常用JVM参数
| 参数 | 备注 |
|---|---|
| -Xms | 初始堆大小。只要启动,就占用的堆大小,默认是内存的1/64 |
| -Xmx | 最大堆大小。默认是内存的1/4 |
| -Xmn | 新生区堆大小 |
| -XX:+PrintGCDetails | 输出详细的GC处理日志 |
怎么查看jvm堆的默认值
Runtime.getRuntime().maxMemory() // 堆的最大值,默认是内存的1/4
Runtime.getRuntime().totalMemory() // 堆的当前总大小,默认是内存的1/64
如何设置JVM参数
程序运行时,可以给该程序设置jvm参数,不同的工具设置方式不同。
如果是命令行运行:
java -Xmx50m -Xms10m HeapDemo
idea运行时设置方式如下:
查看堆内存详情
public class Demo2 {
public static void main(String[] args) {
System.out.print("最大堆大小:");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================================================");
byte[] b = null;
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
}
}
执行前配置参数:
-Xmx50m -Xms30m -XX:+PrintGCDetails
新生代和老年代的堆大小之和是
Runtime.getRuntime().totalMemory()
GC演示
public class HeapDemo {
public static void main(String args[]) {
System.out.println("=====================Begin=========================");
System.out.print("最大堆大小:Xmx=");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================First Allocated===================");
byte[] b1 = new byte[5 * 1024 * 1024];
System.out.println("5MB array allocated");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("=================Second Allocated===================");
byte[] b2 = new byte[10 * 1024 * 1024];
System.out.println("10MB array allocated");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("=====================OOM=========================");
System.out.println("OOM!!!");
System.gc();
byte[] b3 = new byte[40 * 1024 * 1024];
}
}
将JVM参数设置成最大堆内存100M,当前堆内存10M:
-Xmx100m -Xms10m -XX:+PrintGCDetails
再次运行,可以看到minor GC和full GC日志
OOM演示
把上面案例中的jvm参数改成最大堆内存设置成50M,当前堆内存设置成10M,执行测试: -Xmx50m -Xms10m
=====================Begin=========================
剩余堆大小:free mem=8.186859130859375M
当前堆大小:total mem=9.5M
=================First Allocated=====================
5MB array allocated
剩余堆大小:free mem=3.1868438720703125M
当前堆大小:total mem=9.5M
================Second Allocated====================
10MB array allocated
剩余堆大小:free mem=3.68682861328125M
当前堆大小:total mem=20.0M
=====================OOM=========================
OOM!!!
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.dyy.demo.HeapDemo.main(HeapDemo.java:40)
实际开发中可以使用MAT工具来定位这种错误信息。
MAT工具
安装方式:eclipse的插件市场中下载
使用
运行参数:-Xmx30m -Xms10m -XX:+HeapDumpOnOutOfMemoryError
重新刷新项目,可以看到dump文件,后缀为.hprof.
可以直接打开
如果是idea
run->Edit Configurations...
然后找到运行参数
-Xmx50m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\tmp
这个-XX:HeapDumpPath参数是指生成dump文件的路径。
这个dump文件也可以通过jdk自带的解读工具,这个工具在%JAVA_PATH%/bin目录下,名为jvisualvm.exe,然后大考这个文件,通过文件->装入->选择要打开的文件即可。
常用命令行
| 作用 | 命令 |
|---|---|
| 查看java进程 | jps -l |
| 查看某个java进程所有参数 | jinfo 进程号 |
| 查看某个java进程总结性垃圾回收统计 | jstat -gc 进程号 |
JVM结构总结
GC垃圾回收
问题
- JVM内存模型以及分区,需要详细到每个区放什么
- 堆里面的分区:Eden,Survival from to,老年代,他们各自的特点。
- GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点。
- Minor GC和Full GC分别在什么时候发生。
JVM垃圾判定算法
对象是否已死?
引用计数法
引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:
简单高效,现在的objective-c、python等用的就是这种算法。
缺点:
- 引用和去引用伴随着加减性能,影响性能。
- 很难处理循环引用,相互引用的两个对象无法释放。
目前主流的Java虚拟机都摒弃了这种算法
可达性分析算法(根搜索算法)
这个算法的基本思想就是通过一系列的称为GC Roots的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,说明这个对象是不可用的。
在Java语言中,可以作为GC Roots的对象包括以下:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中类静态属性引用的对象。
- 方法区中的常量引用的对象。
- 本地方法栈中JNI(Native方法)的引用对象
真正标记对象为可回收状态需要至少标记两次
第一次标记:不再GC Roots链中,标记为可回收对象。
第二次标记:判断当前对象是否实现了finalize()方法,如果没有实现则直接判定这个对象可以回收,如果实现了就会先放入一个队列中。并由虚拟机建立一个低优先级的程序去执行他,随后就会进行第二次小规模标记,这次被标记的对象就会真正被回收了。
//t1 可以叫 虚拟机栈中引用的对象
GCRootsDemo t1 = new GCRootsDemo();
//方法区中的类静态属性引用的对象
private static GCRootsDemo2 t2 = new GCRootsDemo2();
//方法去常量引用的对象
private static final GCRootsDemo3 t3 = new GCRootsDemo3();
四种引用
我们平时只会用到强引用和软引用。
强引用:
类似于Object obj = new Object();只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:
SoftReference类实现弱引用。在系统要发生内存溢出异常之前,才会将这些对象列进回收范围之内进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。软引用可用来实现内存敏感的高速缓存。
弱引用:
WeakReference类实现弱引用。对象只能生存到下一次垃圾回收之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
虚引用:
PhantomReference类实现虚引用。无法通过虚引用获取另一个对象的实例,为对象设置一个虚引用关联的唯一目的就是能在这个对象被收集器回收时受到一个系统通知。
GC回收算法
stop-the-world:
stop-the-world意味着由于要执行GC而停止了应用程序的执行,并且这种情况会在任何一种GC算法中发生。
当stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。
所以GC优化在多数情况下指的是减少stop-the-world发生的时间,从而使系统具有高吞吐,低停顿的特点。
复制算法(copying)
这个算法将内存分成两部分,然后每次只用其中一部分,这部分内存满了之后,就将这部分中所有存活对象复制到另一部分内存中,然后把之前的内存清空。按照这种方法循环下去。
优点:
- 实现简单
- 不产生内存碎片
缺点:
- 浪费了一般的内存空间
- 如果对象存活率非常高,那么重置地址这个过程非常浪费时间,所以老年代一般不能直接用这种算法。
在年轻代中使用的使Minor GC,这个GC算法使用的就是复制算法
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。
标记清除(Mark-Sweep)
"标记-清除"算法是几种GC算法中最基础的算法,是因为后续的收集算法都是根据这种算法改进而来的。
算法分为两个阶段:
- 标记需要回收的对象,使用的标记算法是可达性分析算法。
- 回收被标记的对象
缺点:
- 效率低(两次遍历)
- 空间问题(标记清除后产生大量不连续的碎片,JVM需要维护一个内存的空闲列表,在分配数组对象的时候,也不好找连续的内存空间)
标记压缩,也称标记整理(Mark-Compact)
第一阶段,将所有对象标记为存活或死亡状态。
第二节点,将所有存活对象向同一端移动,然后清除边界以外的内存。
优点:
标记整理算法不仅可以弥补标记/清除算法中,内存区域分散的缺点,也消除了复制算法中,内存减半的高额代价。
缺点:
当存活的对象过多时,整理阶段会执行较多的复制操作,导致算法效率降低。
老年代一般是由标记清除或者是标记清除和标记整理的混合实现。
分代收集算法(Generational-Collection)
内存效率:复制算法>标记清除>标记整理(此处只是简短对比时间复杂度,实际情况不一定如此)
内存整齐度:复制算法=标记整理>标记清除
内存利用率:标记整理=标记清除>复制算法
没有最优算法,只有最合适的算法。
年轻代(Young Gen)
年轻代特点是区域相对老年代较小,对像存活率低。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenure Gen)
老年代的特点是区域较大,对象存活率高。
这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
如何查看当前jdk的收集器:
cmd 输入命令: java -XX:+PrintCommandLineFlags -version
使用的jdk1.8显示的是-XX:+UseParallelGC
说明:
新生代收集器:Parallel Scavenge(复制算法)
老年代收集器:Parallel Old(标记压缩)
Serial/Serial Old收集器
串行收集器。只使用一个线程去回收。可能会产生较长时间的停顿。
新生代老年代用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集过程中会stop-the-world。
他还有对应老年代的版本:Serial Old
参数控制: -XX:+UseSerialGC 串行收集器
ParNew收集器
实际上就是Serial收集器的多线程版本。
新生代并行,老年代串行。
新生代复制算法,老年代标记压缩。
参数控制:
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量
Parallel/Parallel Old收集器
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;
新生代复制算法、老年代标记压缩。
参数控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供
参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行
CMS收集器
以获取最短回收停顿时间为目标。
目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
CMS收集器的内存回收过程是与用户线程一起并发地执行。
是老年代收集器。新生代收集器使用ParNew。
优点:并发收集、低停顿
缺点:产生大量空间碎片,并发阶段会降低吞吐量
参数控制:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
CMS是一种预处理垃圾回收器,它不能等到old内存用尽前,完成回收操作,否则会导致并发回收失败。
G1收集器
HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:
- 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
- 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
前面的垃圾收集器,收集范围都是整个新生代或者老年代,而G1不再这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大的差别,它将真个Java堆划分成多个大小独立的相等的独立区域(Region),虽然还保留新生代和捞钱代的概念,但是新生代和老年代不再是物理隔阂了,他们都是一部分(可以不连续)Region的集合。
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
1、初始标记(Initial Making)
2、并发标记(Concurrent Marking)
3、最终标记(Final Marking)
4、筛选回收(Live Data Counting and Evacuation)
看上去跟CMS收集器的运作过程有几分相似,不过确实也这样。初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。而最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。
垃圾回收器比较
如果两个收集器之前存在连线,说明他们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
整堆收集器:G1
垃圾回收器选择策略:
客户端程序:Serial + Serial Old
吞吐率优先的服务端程序(比如:计算密集型):Parallel Scavenge + Parallel Old;
相应时间优先的服务端程序:ParNew + CMS
G1收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确的控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。