JVM系列之:内存与垃圾回收篇(三)
xml
复制代码
##本篇内容概述:
1、执行引擎
2、StringTable
3、垃圾回收
一、执行引擎
xml
复制代码
##一、执行引擎概述
如果想让一个java程序运行起来,执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。
简单来说,JVM中的执行引擎充当了将改机语言翻译为机器语言的译者。
##二、执行引擎的工作过程
1)执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器
2)每当执行完一项操作后,PC寄存器就会更新下一条需要被执行的指令地址
3)当方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的
对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头
中的元数据指针定位到目标对象的类型信息。
xml
复制代码
##三、什么是解释器,什么是JIT编译器?
解释器:当java虚拟机启动时会根据预定义的规范对字节码采用逐行解释
的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令
执行。(解释执行)
JIT编译器:就是虚拟机将源码直接编译成和本地机器平台相关的语言。(先翻译好,放在方法区等待执行)
##四、解释器
JVM设计者初衷仅仅只是单纯地为了满足java程序实现跨平台的特性,因
此避免了采用静态编译的方式直接生成本地机器指令,从而诞生了实现解
释器在运行时采用逐行解释字节码执行程序的想法。
解释器真正意义上所承担的角色是一个运行时翻译者,将字节码文件中的
内容翻译为对应平台的本地机器指令执行。
当一条字节码指令被解释执行完成后,接着在根据PC寄存器中记录的下一
条需要被执行的字节码指令执行解释操作
##五、JIT编译器
基于解释器执行已经沦为低效的代名词,为了解决这个问题,JVM平台支持一种叫做即时编译器的技术。即使编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅提升。
##六、HotSpot为何解释器和JIT编译器共存?
首先,在程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。
编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。
所以,当JAVA虚拟机启动时,解释器可以首先发挥作用,而不必等待即时
编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随
着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获
得更高的执行效率。
同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的逃生门。
##七、热点代码及探测方式
代码是否需要启动JIT编译器将字节码直接便以为对应平台的本地机器指令则需要根据代码被调用的频率而定。哪些需要被编译为本地代码的字节码被称为热点代码。
目前HotSpot VM所采用的的热点探测方式是基于计数器的热点探测:
方法调用计数器,client模式下是1500次,server模式下是
10000次才会触发JIT编译(不是按调用绝对次数统计的,调用次数
有热度衰减)。
回边计数器,统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令成为回边。
##八、设置HotSpot模式
-Xint完全采用解释器模式执行程序
-Xcomp完全采用JIT编译器模式执行程序。如果即时编译器出现问题,解释器会介入执行。
-Xmixed采用解释器+即时编译器的混合模式共同执行程序。
二、StringTable
xml
复制代码
##一、String的基本特性
·String字符创:实例化方式
String s1 = "aaa";//字面量的定义方式
String s2 = new String("bbb");
·String是被声明为final的,不可被继承
·String实现了Serializable接口,表示字符串是支持序列化的 String实现了Comparable接口,表示String可以比较大小
·String在JDK8及之前内部定义了final char[] value用于存储字符串数据
String在JDK9改为了final byte[] value
·String代表不可变的字符序列。简称不可变性
>当对字符串重新赋值时,需要重新制定内存区域赋值,不能使用原有的value进行赋值。
>当对现有的字符串进行连接操作时,也需要重新制定内存区域赋值,不能使用原有的value进行赋值。
>当调用String的replace()方法修改指定字符或字符串是,也需要重新指定内存区域赋值,不能使用原有的value进行复制。
·通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
【字符串常量池中是不可以放相同的字符串的】
eg:
public class StringExer{
String str = "good";
char[] ch = {'t','e','s','t'};
public void change(String str,char ch[]){
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args){
StringExer ex = new String Exer();
ex.change(str,ch);
System.out.println(ex.str);//结果是good
System.out.println(ex.ch);//结果是best
}
}
##二、StringTable底层Hashtable结构的说明
·字符串常量池中是不会存储相同内容的字符串的
>String的String Pool是一个固定大小的Hashtable,默认值大小长度是
1009.如果放进String Pool的String非常多,就会造成Hash冲突严重,
从而导致链表会很长,而链表长了之后直接回造成的影响就是当调用
String.intern的性能会大幅下降
>使用-XX:StringTableSize可以设置StringTable的长度
>jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字
符串过多就会导致效率下降很快,StringTableSize设置没有要求
>jdk7中,StringTable的长度默认值为60013
>jdk8中,StringTabledSize 1009是可设置的最小值
##三、String的内存分配
·在Java中有8中基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快'更节省内存,都提供了一种常量池的概念
·常量池就类似一个Java系统级别提供的缓存。8中基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
>直接使用""声明出来的String对象会直接存储在常量池中
String info = "Hebei";
>如果不是使用""声明的String对象,可以使用String提供的intern()方法。
·JDK6及以前,字符串常量池存放在永久代
jdk7字符串常量池的位置调整到了java堆中
JDK8永久代变为了元空间,字符串常量池还在堆中
##四、String的拼接操作
·常量与常量的拼接结果在常量池,原理是编译期优化
·常量池中不会存在相同内容的常量
·拼接操作中只要其中一个是变量,结果就在堆中(不是常量池中)。
变量拼接的原理是StringBuilder(相当于新new了一个对象)
·拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式
·如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址,有的话则直接返回地址。
说明:
equals判断两个变量或者实例指向同一个内存空间的"值"是不是相同
而==是判断两个变量或者实例是不是指向同一个内存空间地址
eg:
public void test1(){
String s1 = "a"+"b"+"c";//也放在常量池中
String s2 = "abc";//放在字符串常量池中,并将此地址赋值给s2
System.out.println(s1==s2);//true
System.out.println(s1.equals(s2));//true
}
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE"+"hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3==s4);//true
System.out.println(s3==s5);//false s1为变量,结果在堆
//如果拼接符号的前后出现了变量
//相当于在堆空间中new String()
//具体的内容为拼接的结果
System.out.println(s3==s6);//false 结果在堆
System.out.println(s3==s7);//false 结果在堆
System.out.println(s5==s6);//false 结果在堆
System.out.println(s5==s7);//false 结果在堆
System.out.println(s6==s7);//false 结果在堆
//intern():判断字符串常量池中费否存在javaEEhadoop
//如果存在,则返回常量池中JavaEEHadoop的地址
//如果字符串常量池中不存在JavaEEHadoop,则在常量池中加载一份
//并返回此对象的地址
String s8 = s6.intern();
System.out.println(s3==s8);//true 结果在字符串常量池中
}
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
//先StringBuilder s = new StringBuilder()
//然后 s.append("a")
//然后 s.append("b")
//然后 s.toString() --> 约等于new String("ab")
//注:jdk5之后使用StringBuilder,jdk5之前使用StringBuffer
String s4 = s1 + s2;//
System.out.println(s3==s4);//false s4结果在堆
}
public void test4(){
final String s1 = "a";//常量
final String s2 = "b";//常量
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3==s4);//true
}
##五、String拼接和StringBuilder append的效率对比
string拼接:
for(int i=0;i<10000;i++){
s = s + "a"; //每次循环都会创建一个StringBuilder,然后再转成String }
StringBuilder append:
for(int i=0;i<10000;i++){
s.append("a");
}
结论:通过StringBuilder的append()的方式拼接字符串的效率远高于String的字符串拼接方式。
①、StringBuilder自始至终只创建了一个StringBuilder的对象
②、使用String拼接方式,每次for循环都会创建一次StringBuilder和String对象,因此内存占用也更大,GC还需要花费额外的时间。
StringBuilder在实际开发中,如果基本确定字符串的总长度不会高于highlevel的情
况下,也减一使用构造器new StringBuilder(highLevel)//指定长度,防止在拼接
的过程中StringBuilder扩容(扩容的过程中原有的StringBuilder会废弃然后重新创
建StringBuilder)。
##六、intern()的使用
如果不是""声明的String对象,可以使用String提供的intern方法:
intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放
入常量池中,然后返回地址;若存在则直接返回地址。
eg:String info = new String("i love you").intern();
也就是说,如果任意字符串上调用String.intern()方法,name其返回结果所指向的那
个实力,必须和直接以常量形式出现的字符串实力完全相拥。因此下列表达式的值必定是true:
eg: ("a"+"b"+"c").intern() == "abc";
通俗点将,intern就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快
字符串操作任务的执行速度。
注意:这个值会被存放子啊字符串常量池中。
##七、new String("ab")创建了几个对象?
String str = new String("ab")
创建了两个对象,一个new关键字在堆空间中创建的,
然后在字符串常量池中创建了一个ab
String str = new String("a")+new String("b");
创建了3个对象,
对象1:new StringBuilder(),用以拼接
对象2: new String("a")
对象3:字符串常量池中 a
对象4 new String("b")
对象5 字符串常量池中b
[stringbuilder的tostring方法是没有在字符串常量池中创建ab的]
eg:
public static void main(String[] args) {
String s1 = new String("77");//s1指向堆中的地址,且常量池中有77
s1.intern();//指向了常量池中已有的77,但没有把 这个引用赋值给s1,它是不同于s1=s1.intern()的
String s2 = new String("77").intern();//s2指向了s1在常量池中创建的77的地址
String s3 = "77";//s3也指向了s1在常量池中创建的77的地址
System.out.println(s1==s2);//false
System.out.println(s1==s3);//false
System.out.println(s2==s3);//true
String s4 = new String("1")+new String("2");//s4指向了自己在堆中的地址,且在常量池中创建了1和2,但没有创建12
s4.intern();//在常量池中创建了77,常量池中的77指向了s4在堆中的地址
String s5 = "12";//s5指向了常量池中77,相当于指向了s4的堆中的引用
System.out.println(s4==s5);//true
String s6 = new String("5")+new String("6");//s6指向了自己在堆中的地址,且在常量池中创建了5和6,但没有创建56
String s7 = "56";//在常量池中创建了56
String s8 = s6.intern();//intern由于56已在常量池中创建,因此s8指向了s7在常量池中创建的56
System.out.println(s6==s7);//false
System.out.println(s6==s8);//false
System.out.println(s7==s8);//true
}
intern()总结:
·jdk1.6:
>如果串池中有,则不会放入。返回已有的串池中的对象的地址。
>如果串池没有,则会把此对象复制一份,放入串池,并返回串池中的对象地址。
·jdk1.7之后:
>如果串池中有,则不会放入。返回已有的串池中的对象的地址。
>如果串池没有,则会把此对象的引用地址复制一份,放入串池,并返回串池中的引用地址
[String str = new String("abc");]
[以上动作常量池中会存在abc,可以拆解开理解:常量池中创建abc,在堆中new一个String("abc"),栈中会保存str,并将堆中的地址赋值给str]
三、垃圾回收
1、垃圾回收概述
xml
复制代码
##一、什么是垃圾?
·在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
##二、为什么需要GC?
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一致保留到应
用程序结束,被保留的空间无法被其它对象使用。甚至可能导致内存溢出。
##三、内存溢出和内存泄漏
·指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据
的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,
即所谓的内存溢出。
·是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,
但内存泄漏堆积后的后果就是内存溢出。
【GC的主要作用区域:方法区+堆】
【Java堆是GC的工作重点】
【频繁回收 新生代,较少回收 老年代,基本不动 元空间或永久代】
2、垃圾回收相关算法(重)
2.1、垃圾标记阶段的算法:
xml
复制代码
##1、垃圾标记阶段:对象存活判断
·在堆里存放着几乎所有的java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对
象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用
的内存空间,因此这个过程我们可以成为垃圾标记阶段。
·在JVM中如何标记一个死亡对象?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以
宣判为已经死亡。
·判断对象存活一般有两种方式:【"引用计数算法"】 和 【"可达性分析算法"】
##2、引用计数算法Refrence Counting--->HotPot没有使用
·引用计数算法,为每个对象保存一个整型的"引用计数器"属性们,用于记录对象被引用的情况。
·对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就
减1.只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
·优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
·缺点:
>需要单独的字段存储计数器,这样的做法增加了存储空间的开销
>每次赋值都需要更新计数器,伴随着加法和减法操作,增加了时间开销
>引用计数器有一个严重的问题,即 "无法处理循环引用" 的情况。这是一条致命的缺陷,导致在java的垃圾回收器中没有使用这类算法。
xml
复制代码
##3、可达性分析算法(也叫根搜索算法、追踪性垃圾收集)
·相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
·可达性分析被java c#选择。这种类型的垃圾收集也叫做"追踪性垃圾收集"
·所谓GC Roots跟集合就是一组必须活跃的引用
·基本思路:
>可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
>使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索锁走过的路径被称为引用链(Refrence Chain)
>如果目标对象没有被任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
>在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象
xml
复制代码
·在java中,GC Roots包括以下几类元素:
>虚拟机栈中的引用对象
eg:各个线程被调用的方法中使用到的参数、局部变量
>本地方法栈内JNI引用的对象
>方法区中类静态属性引用的对象
eg:Java类的引用类型静态变量
>方法区中常量引用的对象
eg:字符串常量池里的引用
>所有被同步锁synchronized持有的对象
>Java虚拟机内部的引用
eg:基本数据类型对象的class对象,一些常驻的异常对象,系统类加载器
>反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
缺点:
·如果使用可达性分析算法判断内存是否回收,需要分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证,这也是GC时必须"STW(stop the world)"的一个重要原因。
2.2、对象的finalization机制
xml
复制代码
·Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁前的自定义处理逻辑
·当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象前,总会先调用这个对象的finalize()方法
·finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放,通常在这个方法中进行一些资源
释放和清理工作,比如关闭文件、套接字和数据库连接等
·永远不要主动的调用某个对象的finalize()方法,应交给垃圾回收机制调用。
原因如下:
>在finalize()是可能会导致对象复活
>finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法没有执行几回
>一个糟糕的finalize()会严重影响GC的性能
·由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态:如果从根节点都无法访问到某个对
象,说明对象已经不再使用了。一般来说此对象需要被回收。但事实上,也并非是非死不可的,这时候它们暂时
处于缓刑阶段。一个无法触及的对象可能在某一个条件下复活自己,如果这样,那么对它的回收就是不合理的,
为此,定义虚拟机中的对象可能的三种状态如下:
>可触及的:从根节点开始,可以到达这个对象
>可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活
>不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可
能被复活,因为finalize()只能被调用一次,类似servlet的destory()。
以上三种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
##判定一个对象ObjA是否可回收的具体过程:
·如果对象ObjA到GC Roots没有引用链,则进行第一次标记
·进行筛选,判断次对象是否有必要执行finalize()方法:
>如果obja没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为没有必要执
行,obja被判定为不可触及的
>如果对象obja重写了finalize()方法,且还未执行过,那么obja会被插入到F-Queue队列中没有一个虚拟
机自动创建的、低优先级的finalizer线程触发器finalize()方法执行
>finalize()方法时对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果
obja在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,obja会被溢出即将
回收集合。之后,对象会再次出现没有引用存在的情况。这个情况下finalize方法不会被再次调用,对象会直
接编程不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次
2.3、MAT与JProfiler的GC Roots溯源
MAT下载地址 JProfiler下载地址
xml
复制代码
MAT(memory analyzer)是一款java堆内存分析器,用于查找内存泄漏以及查看内存消耗情况
[MAT是eclipse开发的,免费性能分析工具]
##获取dump文件
>方式一:命令行使用jmap
>方式二:使用JVisualVM生成
JVisualVM -> 监视 -> 右侧堆dump -> 左侧树右击保存
##MAT打开dump
##JProfiler也可以在idea中安装插件使用
2.4、垃圾清除阶段的算法
xml
复制代码
##1、垃圾清除
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存
空间,以便有足够的可用内存空间为新对象分配内存。
目前JVM中比较常见的三种垃圾清除算法是:
>标记-清除算法(mark-sweep)
>复制算法
>标记-压缩算法
##2、标记-清除算法(mark-sweep)
执行过程:
·当堆中的有效内存空间被耗尽的时候,就会停止整个程序(STW),然后进行两项工作,第一项是标记,第二项是清除。
>标记:collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的header中记录为可达对象
>清除:collector对堆内存从头到尾进行现行遍历,如果发现某个对象在其Header中没有标记为
可达对象,则将其回收(内存空间碎片化了),并将其地址记录在空闲列表中以便之后使用
缺点:
>效率不高(两次全遍历)
>在进行GC时,需要停止整个应用程序(STW),用户体验差
>这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表(记录哪个位置是空闲的可以存放对象)
注意:
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新
对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。【其实是覆盖】
##3、复制算法(copying)--->适用于新生代
核心思想:
·将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象
复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最
后完成垃圾回收。【类似堆区新生代中的Eden区和survivor0区和survivor1区】
优点:
>没有标记和清除过程,实现简单,运行高效
>复制过去以后保证空间的连续性,不会出现内存碎片问题
缺点:
>此算法可用内存空间被砍半了
>对于G1这种分拆成为大量region的gc,复制而不是移动,意味着GC需要维护region之间对象引
用关系,不管是内存占用或者时间开销也不小.(移动后 栈对堆中对象的引用地址就会发生变化需要重新设置)
应用场景:
·新生代中的对象大部分是朝生夕死的,回收的性价比高,所以现在的商业虚拟机都是用这种收集算法回收新生代。
##4、标记-压缩算法(mark-compact)--->适用于老年代
执行过程:
·第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
·第二阶段将所有的存活对象压缩到内存的一段,按顺序排放
·之后清理边界外所有空间(整理内存,防止了内存碎片)
标记压缩算法的最终效果等同于标记-清除算法执行完后,再进行一次内存碎片整理。
二者本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩算法是移动式的。
可以看到,标记存活对象将会被整理,按照内存地址一次排列,而未被标记的内存会被清理掉。如
此一来,当我们需要给新对象分配内存是,JVM只需要持有一个内存的起始地址即可,这比维护一个
空闲列表显然少了许多开销。
【同时堆中 可触及的存活对象地址的移动,需要修改引用者的引用地址】
优点:
>解决了标记-清除算法中内存碎片和JVM必须维护一个可用空闲列表的缺点,此时JVM只需维护一个内存起始地址即可。
>消除了复制算法中内存减半的高额代价
缺点:
>效率上要低于复制算法
>移动对象的同时,还需要调整引用者引用的地址
>移动过程中,需要STW全程暂停用户应用程序
##5、三种算法的对比
复制算法在效率上讲是最快的,但是浪费了一半内存
标记-整理算法效率最低,比复制算法多了一个标记阶段,比标记-清除多了个整理内存的阶段,但却有效减少了内存碎片问题。
整体上说:复制算法适用于新生代,标记-清理和标记-整理算法适用于老年代
总结:
·目前几乎所有的GC都是采用 "分带收集算法" 执行垃圾回收的。
·新生代:区域相对小,对象生命周期短、存活率低、挥手频繁,这种情况下适用于复制算法
·老年代:区域较大,对象生命周期长、存活率高,回收不及新生代频繁,一般由标记-清除或者标记-整理算法的混合实现
##6、增量收集算法
上述算法,在垃圾回收过程中,应用软件将处于STW状态,在STW状态下,应用程序所有线程都会挂起,暂
停一切正常工作。等待垃圾的回收完成,严重影响了用户体验和系统的稳定性。
·基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线
程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直
到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除算法和复制算法。增量收集算法通过对线程间冲突的
妥善处理,允许垃圾收集线程以分段的方式完成标记、清理或复制工作。
缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。
但是,因为线程切换和上下文转换的消耗,会是的垃圾回收的总体成本上升,造成系统吞吐量的下降
##7、分区算法
一般来说,相同条件下,堆空间越大,一次GC时所需要的时间越长,有关GC产生的停顿也就越长。
为了更好滴控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的的停顿。
分代算法将按照对象的生命周期长短划分成两个部分(新生代和老年代),分区算法将整个堆空间划分成连续的不同小区间。
每个小区间都是独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
3、垃圾回收相关概念
3.1、System.GC
xml
复制代码
·默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存
·然而System.gc()调用"无法保证对垃圾收集器的调用"
·JVM实现者可以通过System.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收
应该是自动进行的,无需手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在
编写一个性能基准,我们可以在运行期间调用System.gc()
·System.runFinalization()强制调用使用引用的对象的finalize()方法
(垃圾回收此对象前,总会先调用这个对象的finalize()方法)
3.2、内存溢出与内存泄漏
xml
复制代码
##一、内存溢出OOM:
·内存溢出是引发程序崩溃的罪魁祸首之一
·由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾
回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况
·大多数情况下,GC会进行各种年龄段的垃圾回收,实现不行了就来一次独占式的Full GC操
作,这时候会回收大量的内存,供应用程序继续使用
·Javadoc中对OOM的解释是:没有空闲内存,并且垃圾收集器也无法提供更多内存
·没有空闲内存的情况:说明JVM的堆内存不够,原因有二:
>Java虚拟机的堆内存设置不够
比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的
数据量,但是没有显示指定JVM堆大小或者指定数值偏小。我们可以通过参数-Xms\-Xmx来
调整
>代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
java.lang.OutOfMemoryError:PermGen space
java.lang.OutOfMemoryError:Metaspace
·在抛出OOM之前,通常垃圾收集器会被触发,尽其所能去清理出空间
##二、内存泄漏Memory Leak:
·内存泄漏也可以称作 "存储渗漏"。严格来说,只有对象不会再被程序用到了,但是GC又不
能回收它们的情况,才叫内存泄漏。
·但实际情况很多时候一些不太好的实践会导致对象的生命周期变得很长甚至导致OOM,也可
以叫做宽泛意义上的内存泄漏
·尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被
逐步蚕食,直至耗尽所有内存,最终出现OOM异常,导致程序崩溃。
·注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于
磁盘交换区设定的大小
eg:
1、单例模式:单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对象外部
对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生
2、一些提供close的资源未关闭导致的内存泄漏
如:datasource.getConnetction()、网络连接socket和IO连接必须手动close,否
则是不能被回收的
3.3、STW
xml
复制代码
·Stop The World(STW),指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,这个停顿称为STW
·被STW中断的应用程序线程会在完成GC之后恢复.
·STW事件和曹勇哪款GC无关,所有的GC都有这个事件
·STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉
·开发中不要使用System.gc()会导致STW的发生
(JVM在遍历GC Roots的时候需要在一个能确保一致性的快照中进行,所以会暂停一切用户程序从而产生STW)
3.4、垃圾回收的并发与并行
xml
复制代码
##一、并发Concurrent
·在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个
程序都在同一个处理器上运行
·并发并不是真真意义上的"同时进行",只是CPU把一个时间段划分成几个时间片段,然后在
这几个时间区间之间来回切换,由于CPU处理速度非常快,只要时间间隔处理得当,即可让用
户感觉是多个应用程序同时在进行
##二、并行Parallel
·当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两
个进程互不抢占CPU资源,可以同时进行,我们称之为并行
·其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行
·适合科学计算,后台处理等弱交互场景
##三、并发与并行对比
并发,指的是多个事情,在同一时间段内同时发生
并行,指的是多个事情,在同一时间点上同时发生
并发的多个任务之间是互相抢占资源的
并行的多个任务之间是不互相抢占资源的
只有在多CPU或者一个CPU多核的情况下,才会发生并行。
否则看似同时发生的事情,其实都是并发执行的
##四、垃圾回收中的并发与并行
·并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
>如ParNew、Parallel Scavenge、Parallel Old
·并发:指用户线程与垃圾收集线程同时执行,垃圾回收线程在执行时不会停顿用户程序的运
行(不一定是并行的,可能会交替执行)
>用户程序在继续运行,二垃圾收集程序线程运行于另一个CPU上
>如CMS、G1
·串行
>相较于并行的概念,单线程执行
>如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收,回收完,再启动程序的线程
3.5、安全点与安全区域
xml
复制代码
##一、安全点safe point
·程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,
这些位置称为"安全点safepoint"
·safe point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运
行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据"是否具有让程序长时间执
行的特征"为标准。比如:选择一些执行时间较长的指令作为safe point,如方法调用、循
环跳转和异常跳转等
·如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来?
>抢先式中断(目前没有虚拟机采用了)
首先中断所有线程,如果线程不再安全点,就恢复线程,让线程跑到安全点
>主动式中断
设置一个中断标志,各个线程运行到safe point的时候主动轮训这个标志,如果中断标志为真,则将自己进行中断挂起
##二、安全区域safe ragion
·safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的safepoint。
但是程序不执行的时候呢?例如线程处于sleep状态或blocked状态,这时候线程无法响应
jvm的中断请求,走到安全点去中断挂起,JVM也不太可能等待线程被唤醒。杜宇这种情况,
就需要安全区域来解决
·安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置
开始GC都是安全的。我们也可以吧safe region看作是被扩展了的safepoint
·实际执行时:
>当线程运行到safe region的代码时,首先标识已经进入了safe region,如果这段时间
内发生GC,JVM会忽略标识为Safe Region状态的线程
>当线程即将离开safe region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开safe region的信号为止
3.6、强引用、软引用、弱引用、虚引用、终结器引用(重)
xml
复制代码
我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进
行垃圾收集后还是很紧张,则可以抛弃这些对象。
java对引用的概念进行了扩充:将引用分为强引用strong reference、软引用soft
reference、弱引用weak reference和虚引用phantom reference四种,这四种引用
强度一次逐渐减弱。
除了强引用外,其它3中引用可以在java.lang.ref包中找到,开发人员可以在应用程序中
直接使用它们。
java.lang.ref中只有终结器引用时包内可见的,其它均为public,可以在应用程序中直接使用
##一、概念:
>强引用:最传统的引用定义,是指在代码中普遍存在的引用赋值,即类似Object obj = new Object()这种引用关系。
无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
【只要关系存在,无论内存是否充足都不能被回收】
>软引用:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回
收。如果这次回收后还没有足够的内存,才会抛出OOM
【即使关系存在,只要内存不足还是要被回收】
>弱引用:被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论
内存空间是否足够,都会回收掉被弱引用关联的对象
【只要垃圾收集,无论关系是否存在,也无论内存是否充足都要被回收】
>虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引
用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器
回收时收到一个系统通知
【虚引用对垃圾收集没影响,只是做对象回收跟踪的,随时可能被垃圾回收器回收】
##二、强引用strong reference:不回收
·当java中使用new操作符创建一个新对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用
·只要强引用的对象时刻触及的,垃圾收集器就永远不会回收掉被引用的对象
·对于一个普通对象,如果没有其它的引用关系,只要超过了引用的作用域或者显式地将响应引用赋值为null,就可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。
·强引用时造成java内存泄漏的主要元凶之一
eg:
StringBuffer s1 = new StringBuffer("yyyyy");
StringBuffer s2 = s1;
##二、软引用soft reference:内存不足即回收
·软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象在系统将要发生内存溢出前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没足够的内存,才会抛出内存溢出异常。
【第一次回收不可触及的对象,第二次回收软引用】
·软引用通常用来实现内存敏感的缓存。
比如:高速缓存就有用到软引用,如果还有空闲内存就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同事,不会耗尽内存
·垃圾回收器在某个时候决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列reference queue
eg:
User user = new User(1,"lee","male");//声明一个强引用
SoftReference<User> sf = new SoftReference<User>(user);
user = null;//销毁强引用
//此时user对应的new User就是一个软引用
或
SoftReference<User> sr = new SoftReference<User>(new User(1,"lee","male"));
##三、弱引用weak reference:发现即回收
·弱引用也是用来描述那些非必须对象,被弱引用关联的对象只能生存岛下一次垃圾收集发生
为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用
关联的对象。
·但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的
对象。在这种情况下,弱引用对象可以存在较长时间。
eg:
WeakReference<User> wr = new WeakReference<User>(new User(1,"lee","male"));
WeakHashMap就是实现了WeakReference
##四、虚引用phantom reference:对象回收跟踪
·一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,
那么它和没有引用几乎是一样的,随时可能被垃圾回收器回收
·它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取
得对象时,总是NULL
·为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器
回收时收到一个系统通知
·由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录
eg:
ReferenceQueue rQueue = new ReferenceQueue();
PhantomReference<User> pr = new PhantomReference<User>(new User(1,"Lee","male"),rQueue);
##五、终结器引用Final Reference:包内可见
·它可以实现对象的finalize()方法,也可以成为终结器引用
·无需手动编码,其内部配合引用队列使用
·在GC是,终结器引用入队,有Finalizer线程通过终结器引用找到被引用对象并调用它的
finalize()方法,第二次GC时才能回首被引用的对象
4、垃圾回收器
4.1、GC分类与性能指标
xml
复制代码
##一、垃圾回收分类
1、按照线程数分:(垃圾回收的线程数)
·"串行垃圾回收器" 和 "并行垃圾回收器"
·串行垃圾回收器指的是在同一时段内只允许一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
·并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然和串行回收一样,采用独占式,使用STW机制
xml
复制代码
2、按照工作模式分
·并发式的垃圾回收器 和 独占式的垃圾回收器
·并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间
·独占式垃圾回收器一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束
xml
复制代码
3、按照碎片处理方式分
·压缩式垃圾回收器 和 非压缩式垃圾回收器
·压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片
·非压缩式的垃圾回收器不进行这不操作(需额外维护空闲列表)
4、按工作内存区分
·新生代垃圾回收器 和 老年代垃圾回收器
##二、评估GC的性能指标
·吞吐量:运行用户代码的时间占总运行时间的比例(主要)
>运行总时间 = 程序的运行时间 + 内存回收的时间
·暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间(主要)
·内存占用:Java堆区所占的内存大小
>内存小GC频繁,内存大GC的暂停时间会增大
·垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例
·收集频率:相对于应用程序的执行,收集操作发生的频率
·快速:一个对象从诞生到被回收所经历的时间
>高吞吐量 和 低暂停时间 是相互矛盾的
>如果选择吞吐量优先,那么必然需要降低内存回收的执行频率,但这样会导致GC需要更长的暂
停时间来执行内存回收
>如果选择低延迟优先的原则,那么为了降低每次执行内存回收时的暂停时间,只能频繁地执行
内存回收,但这又引起了新生代内存的所见和导致程序吞吐量的下降
现在标准:在最大吞吐量优先的情况下,降低停顿时间
4.2、垃圾收集器的发展 和 经典的垃圾收集器
xml
复制代码
垃圾回收:Garbage Collection
垃圾回收器:Garbage Collector
##一、垃圾回收器发展历史
·JDK1.3: 串行方式Serial GC,它是第一款GC,ParNew垃圾收集器是Serial收集器的多线程版本
·JDK1.4: Parallel GC 和 Concurrent Mark Sweep GC (CMS)
·JDK6: Parallel GC成为HotSpot默认GC
·JDK7: G1可用
·JDK9:G1成为默认的垃圾收集器,以替代CMS
·JDK11: 引入Epsilon和ZGC
·JDK12: 引入Shenadoah GC
·JDK13: 增强ZGC
·JDK14: 删除CMS,扩展ZGC
##二、7款经典的垃圾回收器
·串行回收器: Serial GC 和 Serial Old
·并行回收器: ParNew 和 Parallel Scavenge 和 Parallel Old
·并发回收器: CMS 和 G1
##三、7款经典收集器 与 垃圾分代之间的关系
·新生代收集器:Serial 和 ParNew 和 Parallel Scavenge
·老年代收集器:Serial Old 和 Prallel Old 和 CMS
·整堆收集器:G1
xml
复制代码
·为什么要有这么多垃圾收集器,因为java的使用场景不同,如移动端,服务器等。针对不用的
场景,提供不同的垃圾收集器,提高垃圾收集的性能
4.3、如何查看默认的垃圾收集器
xml
复制代码
·-XX:+PrintCommandLineFlags 查看命令行相关参数(包含使用的垃圾收集器)
·使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID
4.4、Serial回收器:串行回收
xml
复制代码
【Serial回收新生代 Serial Old回收老年代】
·Serial收集器作为HotSpot中client模式下的默认新生代垃圾收集器
·Serial收集器采用"复制算法"、"串行回收"和"STW机制"的方式执行内存回收
·Serial Old收集器同样采用了"串行回收"和"STW机制",只不过内存回收算法使用的是 "标记-压缩算法"
>client模式下 serial old是默认的老年代回收器
>server模式下 ①、与新生代的Parallel scavenge配合使用 ②、作为CMS收集器的后备垃圾收集方案
·Serial收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个
CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其它所
有工作线程,直到它收集结束(STOP THE WORLD)
##优势:
·简单而高效(与其它收集器单线程比),对于限定单个CPU的环境来说,Serial收集器由于
没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
>运行在client模式下的虚拟机是个不错的选择
·在用户的桌面应用场景中,可用内存一般不大(几十兆至一两百兆),可以在较短时间内完成
垃圾回收,只要不频繁发生使用串行回收器是可以接受的。
##配置:
·在HotSpot虚拟机中,使用-XX+UseSerialGC参数可以指定新生代和老年代都使用串行收集器
>等价于 新生代使用Serial GC,老年代使用Serial Old GC
##总结
现在已经不用串行的垃圾回收器拉,而且在限定单核CPU才可以用,现在都不是单核的了
对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在java web应用程序中是不
会采用串行垃圾收集器的
4.5、ParNew回收器:并行回收
xml
复制代码
【JDK9中已被移除】
【ParNew回收新生代】
·ParNew收集器是Serial收集器的多线程版本
>par是Parallel的缩写,New:表示新生代
·ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何
区别。ParNew收集器在新生代中同样采用"复制算法"、"STW"机制
·ParNew是很多JVM运行在server模式下新生代的默认垃圾收集器
>对于新生代,回收次数频繁,使用并行方式高效
>对于来年代,回收次数少,使用串行方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源)
##由于ParNew收集器是基于并行回收,那么是否可以判定parNew收集器的回收效率在任何场
景下都会比serial收集器效率更高?
>ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等屋里硬件资源优
势,可以更快地完成垃圾收集,提升程序吞吐量
>但是在单个CPU的环境下,ParNew收集器不必Serial收集器更高效。虽然serial收集器是
基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中
产生的一些额外开销
·除了serial外,目前只有parNew GC能与CMS收集器配合工作
##配置:
·-XX:+UserParNewGC手动指定ParNew收集器执行内存回收任务。它表示新生代使用并行收集器,不影响老年代
·-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数
·-XX:UseConcMarkSweepGC设置老年代使用CMS回收器
4.6、Parallel回收器:吞吐量优先
xml
复制代码
【JDK8默认回收器】
【Parallel回收新生代 Parallel Old回收老年代】
·Parallel是Parallel Scavenge收集器的简写
·Parallel收集器同样采用"复制算法"、"并行回收" 和 "STW"机制
##有了ParNew收集器,Parallel收集器的出现是否多此一举?
>和ParNew收集器不同,Parallel收集器的目标则是达到一个可控制的吞吐量,它被称为吞
吐量优先的垃圾收集器
>自适应调节策略也是Parallel和ParNew一个重要区别
[可动态调整内存区域分配情况,新生代大小,Eden和Survivor比例,晋升老年代所需年龄等,默认开启]
·高吞吐量可以高效低利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要
太多交互的任务。因此常见在服务器环境中使用。例如,哪些执行批量处理、订单处理、工资
支付、科学计算的应用程序
·Parallel Old收集老年代垃圾,用来替换来年代的Serial Old收集器
·Parallel Old收集器采用了"标记-压缩算法",但同样也是基于"并行回收"和"STW"机制
·在吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在Server
模式下的内存回收性能很不错
·JDK8中,默认是此垃圾回收器
##参数配置:
·-XX:+UseParallelGC 手动指定新生代使用Parallel并行收集器执行内存回收任务
·-XX:+UseParallelOldGc 手动指定老年代的并行回收收集器
>上面两个参数,默认开启一个,另一个也会被开启(互相激活)
·-XX:ParallelGCThreads 设置新生代并行收集器的线程数,最好与CPU数量相等。
>默认CPU数量小于8个,ParallelGCThreads的值等于CPU数量
>默认CPU数量大于8个时u,ParallelGCThreads的值等于3+[5*CPUCount]/8
·-XX:MaxGCPauseMillis设置垃圾收集器最大停顿时间(即STW时间),单位是ms
>为尽可能地把停顿时间控制在MaxGcPasuseMillis以内,收集器在工作时会调整java堆大小或者一些其它参数
>对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体吞吐量,所以服务器端适合Parallel,进行控制
>该参数使用需谨慎
·-XX:GCTimeRatio垃圾收集时间栈总时间比例[1/(N+1)]用于很小兔兔量大小
>取值范围(0,100),默认值99,也就是垃圾回收时间不超过1%
>与前一个-XX:MaxGCPauseMillis参数有一定矛盾性,暂停时间越长,Radio参数就越容易超过设定的比例
·-XX:+UseAdaptiveSizePolicy设置parallel scavenge收集器具有自适应调节策略
【默认开启】
>在这种模式下,新生代的大小、Eden和Surivivor的比例、晋升老年代的对象年龄等参数会
被自动调整,以达到堆大小、吞吐量和停顿时间之间的平衡点
>在手动调整比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标
的吞吐量和停段时间,让虚拟机自己完成调优工作
4.7、CMS回收器:低延迟
xml
复制代码
【回收老年代】
【JDK14删除CMS】
·Concurrent-Mark-Sweep收集器,简称CMS(标记-清除算法)
·并发收集器,第一次实现了让垃圾收集线程与用户线程同时工作
·CMS收集器关注点是尽可能缩短STW时间,STW时间越短就越适合与用户交互的程序
>目前很大一部分java应用程序在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,已给用户带来较好的体验,CMS收集器就是非常符合这类应用的要求
·CMS垃圾手机算法采用"标记-清除"算法,并且也会STW
·CMS只能和Serial和ParNew一起配合工作,不能和Parallel配合使用
·在G1之前,CMS使用非常广泛
##工作过程
·CMS工作的蒸锅过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段
>初始标记initial-mark:在这个阶段,程序中所有的工作线程都会因为STW机制而出现短
暂的暂停,这个阶段的主要任务仅仅是标记处GC Roots能直接关联到的对象。一旦标记完成
之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快
>并发标记concurrent-mark:从GC Roots的直接关联对象开始遍历整个对象图的过程,
这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
>重新标记remark:由于在并发标记阶段,程序的工作线程回合垃圾收集线程同时运行或者交
叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对
象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍微长一些,单页远比并发标记阶
段的时间短
>并发清除concurrent-sweep:此阶段清理删除掉标记段判断的已经死亡的对象,释放内存
空间,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
##CMS的特点与弊端:
·尽管CMS收集器采用并发挥收,但是在其初始化标记和重新标记两个阶段仍需执行STW,不过
暂停时间不会太长,因此可以说明目前所有的垃圾收集器都做不到完全避免STW,只是尽可能
地缩短暂停时间
·由于耗费时间和并发标记与并发清除阶段都不需要暂停工作,所以整体的回收时低停顿的
·另外由于在垃圾收集阶段用户线程没有中断,所以CMS回收过程中,还应该确保应用程序用户
线程有足够的内存可用。因此CMS收集器不能像其它收集器那样等到老年代几乎完全被填满了
再进行收集,而是当堆内存使用率达到某一阈值是,便开始进行回收,以确保应用在CMS工作
过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需
要,就会出现一次Concurrent Mode Failure,这是虚拟机将启动后背元:临时启用
serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了
·CMS收集器采用"标记-清除"算反,这意味着每次执行完内存回收后,由于被执行内存回收的
无用对象所占用的内存空间极有可能不是连续的一些内存块,不可避免地将会产生一些内存碎
片。那么CMS在位新对象分配内存空间时,将无法使用指针碰撞技术,而只能够选择空闲列表执
行内存分配(维护一个空闲列表)
##Mark-Sweep既然会产生内存碎片,为什么CMS不采用Mark-Compact呢?
·CMS是并发执行的,如果要压缩做碎片整理,就需要停掉所有用户线程,和CMS的初衷不匹配
##CMS优缺点:
·优点
>并发收集
>低延迟(STW时间非常短)
·缺点
>会产生内存碎片
>CMD收集器堆CPU资源非常敏感
在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变
慢,总吞吐量降低
>CMS收集器无法处理浮动垃圾
可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。在并发标记阶段
由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生
新的垃圾对象,CMS将无法对这些对象进行标记,最终导致这些新产生的垃圾对象没有被及时回
收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间
(浮动垃圾:在并发标记的过程中,其它用户线程产生的新垃圾即浮动垃圾)
##参数设置
·-XX:+UseConcMarkSweepGC 手动指定使用CMS收集器执行垃圾回收任务
>开启该参数后,会自动将-XX:+UseParNewGC打开,即ParNew(新)+CMS(老)+Serial Old(老)组合
·-XX:CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,
变开始进行回收
·-XX:+UseCMSCompactAtFullCollection 用于指定在执行完Full GC后堆内存空间进
行压缩整理,以此避免内存碎片的产生,不过由于内存压缩整理过程无法并发执行,所带来的的
问题就是停顿时间变得更长了
·-XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后堆内存进行压缩整理
·-XX:ParallelCMSThreads 设置CMS的线程数量
>CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是新生代并
行收集器的线程数。当CPU资源比较紧张时,收到CMS收集器线程的影响,应用程序的性能在垃
圾回首阶段可能会非常糟糕
4.8、G1回收器:区域化分代式
xml
复制代码
【JDK9以后默认使用的】
【JDK8可用,但还不是默认,需-XX:UseG1GC】
·G1:garbage first
·G1是为了适应不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量而产生的
·G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量
·G1是一款面向服务器端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,
以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征
##一、为什么叫做Garbage First?
·G1是一个"并行回收器",它把堆内存分割为很多不相关的区域region(物理上不连续的),
使用不同的region来表示Eden、幸存者0区、幸存者1区、老年代等
·G1 GC有计划地避免在整个java堆中进行全区域的垃圾收集。
G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的的空间大小以及回收需要时间的
经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region
·由于这种方式的侧重点在于回收垃圾最大量的区间region,所以我们给G1一个名字:垃圾优先Garbage First
##二、G1回收器的优势
(与其它GC收集器相比,G1采用了全新的分区算法)
1·并行与并发兼具
>并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
>并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般
来说,不会再整个回首阶段发生完全阻塞应用的情况
2·分代收集
>G1属于分代性垃圾收集器,它会区分新生代和来年代,新生代依然有Eden区和Surivivor区,但从堆的结构上看,它不要求整个Eden区、新生代或者老年代都是连续的,也不再坚持固定大小和固定数量(这段时间可以是Eden区,下次垃圾回收后可能是Surivivor区)
>将堆空间分为若干个区域,这些区域中包含了逻辑上的新生代和老年代
>和之前的各类回收器不同,它同时兼顾新生代和老年代。
3·空间整合
>CMS:"标记-清除"算法、内存碎片、若干次GC后进行一次碎片整理
>G1将内存划分为一个个的region,内存的回收是以region作为基本单位的。
region之间是复制算法,但整体上实际可看做是"标记-压缩"算法,两种算法都可以避免内存
碎片,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触
发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显
4·可预测的停顿时间模型(即:软实时soft real-time)
这是G1相对于CMS的另一大优势,G1除了追求地停顿外,还建立可预测的停顿时间模型,能让
使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒
>由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全
局停顿的情况的发生也能得到较好的控制
>G1跟踪各个region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的
收集时间,优先回收价值最大的region,保证了G1收集器在有限的时间内可以获取尽可能高的
收集效率
>相较于CMS,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多
##三、G1的缺点
相较于CMS,G1无论是为垃圾收集产生的内存占用,还是程序运行时的额外执行负载都要比CMS高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势,平衡点在6-8GB之间
##四、参数设置
·-XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务(JDK9以后是默认的,JDK8可用但须设置)
·-XX:G1HeapRegionSize 设置每个region的大小,值是2的幂,范围是1MB~32MB之间,目标是根据最小的java堆大小划分出约2048个区域。默认是堆内存的1/2000
·-XX:MAXGCPauseMillis 设置期望达到的最大GC停顿时间指标,默认值是200ms
·-XX:ParallelGCThread 设置STW时GC线程数值,最多设置为8
·-XX:ConcGCThreads 设置并发标记的线程数,将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右
·-XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值,超过此值,就触发GC。默认值是45
G1的设计原则就是简化JVM性能调优,我们只需三步即可完成调优:
1>开启G1垃圾收集器-XX:+UseG1GC
2>设置堆的最大内存-Xms -Xmn
3>设置最大的停顿时间-XX:ParallelGCThread
##五、G1回收器的使用场景
·面对服务器端,针对具有大内存、多处理器的机器
·需要低GC延迟,具有大堆(6G或者更大时)的应用程序
·下面的一些情况下,使用G1性能比CMS好
>超过50%的java堆被活动数据占用
>对象分配频率或年代提升频率变化很大
>GC停顿时间过长(长于0.5至1秒)
##六、Region的使用介绍
·使用G1收集器时,它将整个Java堆划分称谓约2048个大小相同的独立region块,每个
region块大小根据堆空间的实际大小而定,整日被控制在1MB到32MB之间,且为2的N次幂,可
以通过-XX:G1HeapRegionSize设定。
【所有region的大小相同,且在JVM生命周期内不会改变】
·虽然还保留有新生代和老年代的概念,但新生代和来年代不再是物理隔离的了,它们都是一部分region(不需要连续)的集合。通过region的动态分配方式实现逻辑上的连续
·一个region可能属于Eden,survivor或者old内存区域。但是一个region只能属于一个角色
·G1垃圾收集器还增加了一种新的内存区域Humongous内存区域,主要用于存储大对象,如果超过0.5个region,就放到H
设置H区的原因:
对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对
垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放
大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储,为了能找到连续的H
区,有时候不得不启动Full GC,G1的大多数行为都把H区作为老年代的一部分来看待
##七、G1回收器垃圾回收过程
G1的垃圾回收主要包括如下三个环节:
>新生代GC(Young GC)
>老年代并发标记过程(Concurrent Marking)
>混合回收(Mixed GC)
(如果需要,单线程、独占式、高强度的Full GC还是继续存在的,它针对GC的平菇失败提供了一种失败保护机制,即强力回收)
·应用程序分配内存,当新生代的Eden区用尽时开始新生代回收过程:
G1的新生代收集阶段是一个并行的独占式收集器。在新生代回收期,G1 GC暂停所有应用程序线
程(STW),启动多线程执行新生代回收,然后从新生代区间移动存活对象到Survivor区间或
者老年区间,也有可能是两个区间都会涉及
·当堆内存使用叨叨一定值时(默认45%),开始老年代并发标记过程
·标记完成马上开始混合回收过程。
对于一个混合回收器,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老
年代的一部分。和新生代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整
个老年代被回收,一次只需扫描/回收一小部分老年代的region就可以了。同时,这个老年代
region是和新生代一起被回收的
eg:一个Web服务器,java进程最大堆内存为4G,每分钟响应1500个请求,每45秒中会新分配
大约2G的内存。G1会每45秒中进行一次新生代回收,每31个小时整个堆的使用率会达到45%,
会开始老年代并发标记过程,标记完成后开始四到五次的混合回收
##八、记忆集与写屏障
Remembered Set: R Set
#问题:
·一个对象被不同区域引用的问题
·一个region不可能是孤立的,一个region中的对象可能被其他任意region中的对象引用,判断对象存活是,是否需要扫描整个java堆才能保证准确?
·在其他的分代收集器,也存在这样的问题(G1更突出)
·回收新生代也不得不同时扫描老年代?
·这样的话会降低Minor GC的效率
#解决
·无论G1还是其他分代收集器,JVM都是使用 Remembered Set(记忆集)来避免全局扫描
·每个region都有一个对应的remembered set
·每次reference类型数据写操作是,都会长生一个write barrier(写屏障)暂时中断操作
·然后检查将要写入的引用指向的对象是否和该refrences类型数据在不同的region
·如果不用,通过cardtable把相关引用信息记录到引用指向对象的所在region对应的remembered set中
·当进行垃圾收集时,在GC根节点的枚举范围加入remembered set,就可以保证不进行全局扫描,也不会有遗漏
##九、G1垃圾回收过程
1、新生代GC:
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽
时,G1会启动一次年轻代垃圾回收过程
年轻代垃圾回收只会回收Eden区和Surivivor区
YGC时,首先G1停止应用程序的执行STW,G1创建回收机(Collection Set),回收集是指需
要被回收的内存分段的集合,年轻代回收过程的回收集包括年轻代Eden区和Survivor区所有的
内存分段。
(Eden区满了会触发YGC,但Survivor区满了不会触发YGC)
过程:
>第一阶段:扫描根
根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等,根引用连用rset记
录的外部引用作为扫描存活对象的入口
>第二阶段:更新rset
处理dirty card queue中的card,更新rset,此阶段完成后,rset可以准确的反映老年代
对所在的内存分段中的对象引用
>第三阶段:处理rset
识别被来年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象
>第四阶段:复制对象
此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到survivor区中空的内存分段,
survivor区中内存段中存活的对象如果年龄未达到阈值,年龄会加1,大道与之会被复制到old
区中空的内存分段,如果survivor空间不够,Eden空间的部分数据直接晋升到old空间
>第五阶段:处理引用
处理soft、weak、phantom、final、JNI Weak等引用,最终Eden空间的数据为空,GC停
止工作,而目标内存中的对象都是连续存储的,没有碎片,所以赋值过程可以达到内存整理的效
果,减少碎片。
2、并发标记过程:
>初始标记阶段:
标记从根节点直接可达的对象。这个阶段是STW的没并且会触发一次年轻代GC
>根区域扫描
G1扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在YGC之
前完成
>并发标记
在整个堆中进行并发标记,此过程可能被YGC中断,在并发标记阶段,若发现对象中的所有对象
都是垃圾,那这个区域会被立即回收,同时并发标记过程中,会计算每个区域的对象活性
>再次标记
由于应用程序持续进行,需要修正上一次的标记结果,是STW的,G1中采用了比CMS更快的快照
算法
>独占清理
计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,为下阶段做铺
垫,是STW的(这个阶段并不会实际上去做垃圾的收集)
>并发清理阶段
识别并清理完全空闲的区域
3、混合回收
当越来越多的对象晋升到old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃
圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region还会回收一
部分的Old Region,这里需要注意:是一部分老年代,而不是全部老年代。可以选择那些old
region进行收集,从而可以对垃圾回收的耗时时间进行控制。也需要注意的是Mixed GC并不
是Full GC
>并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计
算了出来。默认情况下,这些老年代的内存分段会分8次被回收
>混合回收的收集器包括八分之一的来年代内存分段,Eden区内存分段,Survivor区内存分
段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段
>由于来年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存段,垃圾站村分段比例越
高,越先被回收
>混合回收并不一定要进行8次,有一个阈值-XX:G1HeapWastePercent,默认为10%,意思
是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于
10%,则不再进行混合回收。因为GC会花费很多的时间但是会受到的内存却很少
4、Full GC
G1的初衷是避免Full GC的出现,但是如果上述方式不能正常工作,G1会STW,使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长
4.9、垃圾回收器总结
xml
复制代码
·最小化地使用内存和并行开销:选择Serial GC + Serial Old
·最大化应用程序的吞吐量:选择Parallel GC + Parallel Old
·最小化GC的中断或停顿时间:选择CMS GC + ParNew + Serial Old
·JDK9 废弃了CMS JDK14删除了CMS
(新生代大部分是复制算法 老年代大部分是标记-整理、标记-清除算法)
垃圾收集器
分类
作用位置
算法
特点
场景
Serial
串行
新生代
复制算法
响应速度
单CPU环境下的Client模式
ParNew
并行
新生代
复制算法
响应速度
多CPU环境Server模式,与CMS配合使用
Parallel
并行
新生代
复制算法
吞吐量
适用于后台运算且不需要太多的交互场景
Serial Old
串行
老年代
标记-压缩算法
响应速度
单CPU环境下的Client模式
Parallel Old
并行
老年代
标记-压缩算法
吞吐量
适用于后台运算且不需要太多的交互场景
CMS
并发
老年代
标记-清除算法
响应速度
适用于互联网或B/S业务
G1
并发、并行
新生代、老年代
复制算法、标记-压缩算法
响应速度
面向服务端应用
5.0、GC日志分析
xml
复制代码
##一、日志参数
-XX:+PrintGC 打印GC日志
-XX:+PrintGCDetails 打印日志详情
-XX:+PrintGCTimeStamps 打印GC的时间戳(以基准时间形式)
-XX:+PrintGCDateStamps 打印GC的时间戳(以日期的形式)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:./logs/gc.log 日志文件的输出路径
xml
复制代码
##二、GC日志分析
GC 表示只在"新生代"上进行
Full GC 包括"新生代""老年代""元空间" (会发生STW)
PSYoungen : Parallel Scavenge收集器新生代的名称
DefNew : 使用了Serial收集器新生代的名称 Default New Generation
ParNew :ParNew收集器在新生代的名称 Parallel New Generation
garbage-first heap : G1收集器
ParOldGen : Parallel Old
Allocation Failure : GC发生的原因
3745K->1127K(58880K) : 堆在GC前的大小 和 GC后的大小 和 本身总的大小
0.0083899 secs : GC持续时间
##三、常见的日志分析工具
先使用-Xloggc:./logs/gc.log 日志文件的输出路径
在用工具:
GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等
5.1、新时期的垃圾回收器
xml
复制代码
·Epsilon : A No-Op Garbage Collector(无操作)
[只做内存分配,不做垃圾回收(运行完直接退出程序的场景)]
·Shenandoah GC : 低停顿,但吞吐量下降了 (RedHat开发)
·ZGC : A Scalable Low-Latency Garbage Collector(可扩展、低延迟[停顿])
[基于Region的内存布局、可并发的标记压缩算法、低延迟为目标]
[并发标记-并发与被重分配-并发重分配-并发重映射]