文章目录
- JMM
- 主内存与工作内存
- 工作内存与主内存的交互的8种方法
- JVM内存结构
- 运行时数据区
- 类加载机制
- 类加载器
- 类加载分类
- 获取类加载器的途径
- 双亲委派机制
- 双亲委派的机制的弊端是什么?怎么打破双亲委派机制
- 代码热替换、模块热部署
- 自定义类加载器
- 对类加载器的引用
- String底层
- string基本特性
- 案例
- 字符串拼接操作
- new String("ab")到底创建了几个对象?new String("a") + new String("b")呢?
- String的intern()方法
- G1的String去重操作
- 垃圾回收算法
- 四种引用
- 垃圾标记
- 引用计数算法
- 可达性分析算法
- 哪些变量用作GC Roots?
- 对象的finalization机制
- 垃圾回收
- JVM的垃圾回收算法有哪些?
- System.gc() / Runtime.getRuntime().gc()
- 内存溢出与内存泄露
- 安全点与安全区域
- 垃圾回收器
- 分类
- 评估GC的性能指标
- 七款经典垃圾回收器
- 经典垃圾回收器组合使用
- CMS的优缺点
- G1
- 参数设置
- 分区Region:化整为零
- 垃圾回收过程
- young GC
- 并发标记
- 混合回收
JMM
-
java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯,多个线程之间是不能通过直接传递数据交互的,他们之间交互只能通过共享变量来实现。
-
==JMM的主要目的是定义程序中各个变量的访问规则。==java线程之间的通信由JMM控制。JMM定义了JVM在计算机内存(RAM)中的工作方式,如果想深入理解java并发编程,就要先理解好java内存模型。
主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程中的变量不是完全等同的。这里的变量指的是实例字段、静态字段、构成数组对象的元素,但不包括局部变量与方法参数,因为它们是线程私有的,不会被共享,自然就不会存在竞争的问题。
- 主内存:java内存模型规定了所有的变量都存储在主内存中。
- 工作内存:每条线程有自己的工作内存(可与物理硬件处理器的高速缓存类比),线程的工作内存中保存了该线程所使用的变量的主内存拷贝副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程间也无法访问对方工作内存中的变量,线程间的变量传递需要通过主内存完成。
工作内存与主内存的交互的8种方法
Java内存模型定义了8种方法来完成主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存。虚拟机实现的时候,必须每一种操作都是原子的、不可再分的。
lock
(锁定):作用于主内存
的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量unlock
(解锁):作用于主内存
的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定read
(读取):作用于主内存变量
,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用。load
(载入):作用于线程的工作内存
的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)use
(使用):作用于线程的工作内存
中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作assign
(赋值):作用于线程的工作内存
的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作store
(存储):作用于线程的工作内存
中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用write
(写入):作用于主内存
的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
如果要把一个变量从主内存传输到工作内存,那就要顺序的执行read和load操作,如果要把一个变量从工作内存回写到主内存,就要顺序的执行store和write操作。只需要保证相对顺序,不要求连续,下边两种执行结果是一样的:
- read a; read b; load b; load a;
- read a; load a; read b; load b;
在执行这8中操作的时候必须遵循如下的规则
- 不允许read和load、store和write操作必须成对出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况
- 不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存
- 不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存
- 一个新的变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行load或者assign操作。也就是说在执行use、store之前必须对相同的变量执行了load、assign操作
- 一个变量在同一时刻只能被一个线程对其进行lock操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
- 对变量执行lock操作,就会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新load或者assign操作初始化变量的值
- 不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作
- 对一个变量执行unlock之前,必须先把变量同步回主内存中,也就是执行store和write操作
JVM内存结构
JVM将内存分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
- 程序计数器:线程私有,是一块很小的内存空间,作为当前线程的执行的代码行号指示器,用于记录当前虚拟机正在执行的线程指令地址。
- 虚拟机栈(Java栈):线程私有,每个方法执行的时候创建一个栈帧,用于存储局部变量表,操作数、动态链接和方法返回等信息,当栈深度超过了虚拟机允许的最大深度,就会抛出StackOverFlowError
- 本地方法栈:线程私有,保存的是native方法的信息,当一个JVM创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法。
- 堆:所有线程共享的一块内存,几乎所有对象的实例和数组都在堆上分配内存,因此堆区经常发生垃圾回收操作。
- 方法区:存放已经加载的类信息,常量、静态变量、即时编译器编译后的代码数据。jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分:1、加载类信息。2、运行时常量池。加载类信息保存在元数据区,运行时常量池保存在堆中。
灰色的为单独线程私有的,红色的为多个线程共享的。即:
- 每个线程:独立包括程序计数器、栈、本地栈。
- 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)
运行时数据区
类加载机制
从字节码加载类经历三个阶段:
- 加载阶段:类加载器负责从文件系统或者网络中加载class文件,加载的类信息存放于元空间,类加载器只负责class文件的加载,至于它是否可以执行,则由执行引擎Execute Engine决定。
- 引导类加载器
- 扩展类加载器
- 系统类加载器
- 自定义类加载器
- 链接阶段
- 验证
- 准备
- 解析
- 初始化阶段
类加载器
类加载分类
在程序获取引导类加载器或者String的类加载器,会发现得到结果为null,这是因为String属于Java的核心类,由引导类加载器加载,而引导类加载器是最高级的加载器,嵌套到JVM内部,无法在程序中获取。
获取类加载器的途径
双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,当需要使用该类时,才会将它的class文件加载到内存生成的class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即将请求交由给父类来处理,它是一种任务委派模式。
- 1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派的优势在于:
- 1、避免类的重复加载
- 2、保护程序安全,防止核心API被随意篡改
- 例如自定义String类
双亲委派的机制的弊端是什么?怎么打破双亲委派机制
双亲委派机制中,类加载的委托过程是单向的,即底层的ClassLoader可以访问顶层加载的类,而顶层的ClassLoader则无法访问底层的ClassLoader,应用类可以访问系统类,而系统类访问应用类则会出现问题。
比如在系统类中提供一个接口,该接口需要在应用类中得到实现,该接口还绑定了一个工厂方法,用于创建该接口的实例,接口和工厂方法都在启动类加载器中,这时就会出现该工厂方法无法创建由应用类加载的应用实例的问题。
如何破坏:自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法
代码热替换、模块热部署
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中。
基本上大部分脚本语言都是天生支持热替换的,比如: PHP,只要替换了PHP源文件,这种改动就会立即生效,而无需重启web服务器。
但对Java来说,热替换并非天生就支持,如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重定义这个类, 因此,在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader。
注意:由不同ClassLoader加载的同名类属于不同的类型,不能相互.转换和兼容。即两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类是完全不同的。
根据这个特点,可以用来模拟热替换的实现,基本思路如下图所示:
自定义类加载器
对类加载器的引用
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的classLoader(指cia sToader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
JVM必须知道一个类型是由启加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
String底层
string基本特性
- String声明为final,不可被继承
- String实现了Serializable接口,支持序列化,实现了Compareable接口,支持比较大小。
- String在jdk8及以前内部定义了final char[] value, 用于存储字符串数据,jkd9时改为了byte[]。
- byte 是字节数据类型 ,是有符号型的,占1 个字节;大小范围为-128—127 。
- char 是字符数据类型 ,是无符号型的,占2字节(Unicode码 );大小范围 是0—65535 ;char是一个16位二进制的Unicode字符,JAVA用char来表示一个字符
大部分拉丁字符都可以用一个byte表示,用两个字节的char,浪费了空间,但是类似汉字字符,还是必须用两个字节表示,即2个byte,那么使用一个byte就可以满足需求的,可以使用编码标记(
end-flag
),表示已经编码结束。
- String的String Pool是一个固定大小的Hashtable,不可存放重复的字符串,默认值大小长度是1009,使用-XX:SstringTableSize,可设置StringTable的长度,1009是可设置的最小值。
- 使用字面值声明的String(
String a1 = "aaa";
)会直接放入字符串常量池,返回地址,而使用new String("aaa");
构造的String,会在堆区新建"aaa"的对象返回,同时入参"aaa"会被存入字符串常量池。
String a1 = "aaa";
String a2 = new String("aaa");
String a3 = "aaa";
String a4 = new String("aaa");
System.out.println(a1 == a2); // false
System.out.println(a2 == a3); // false
System.out.println(a1 == a3); // true
System.out.println(a2 == a4); // false
案例
前置知识1:
Java中,方法的参数传递只有值传递,但是对于引用类型,传递的是地址的值
。
前置知识2:Java数据类型:
因此题目中,change()方法传递的char[]数组和String,都是引用类型,也就是说传递的是地址。
那么,change()中对于char[]数组的修改是生效的,test 会变成 best。
但是对于String类型,它是不可变的,change()方法中对于str的赋值,底层会赋值一份str,再修改,而不会影响原有的str。
字符串拼接操作
-
1、常量与常量的拼接结果在常量池,原理是编译期优化
-
2、常量池不会存在相同内容的常量。
-
3、只要拼接其中有一个是变量,结果就在堆中new一个新的字符串,变量拼接的原理是StringBuilder。
-
4、如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串变量放入池中,并返回此对象的地址。
new String(“ab”)到底创建了几个对象?new String(“a”) + new String(“b”)呢?
String的intern()方法
Intern()方法设计的初衷,就是重用String对象,以节省内存消耗。
str.intern()的作用:判断字符串常量池中是否存在str表示的字符串:
- 如果存在:返回常量池中的地址
- 如果不存在,则在常量池中记录str的引用,然后返回常量池中引用。
调用String.intern()方法后,堆中的字符串实例并不会立即被销毁,但是通过intern()方法返回的引用会被重新赋值为常量池中的引用,这可能会导致原本指向堆中字符串的引用失效,从而可能会被垃圾回收器回收,从而节省内存消耗.
String s1 = new String("hello");
String s2 = "hello";
String s3 = s1.intern();System.out.println(s1 == s2); // false,不同的引用
System.out.println(s2 == s3); // true,共享常量池中的引用
- String s1 = new String(“hello”);
- hello存在于字符串常量池和堆中,s1的堆中对象的引用。
- String s2 = “hello”;
- s2是字符串常量池中的引用
- String s3 = s1.intern();
- 常量池中存在s1引用的"hello",因此返回常量持有中"hello"的引用,s3引用的是字符串常量池中的"hello’
-
String s = newString(“1”),生成了常量池中的“1” 和堆空间中的字符串对象。
-
s.intern(),这一行的作用是s对象去常量池中寻找后发现"1"已经存在于常量池中了。
-
String s2 = “1”,这行代码是生成一个s2的引用指向常量池中的“1”对象。
结果就是 s 和 s2 的引用地址明显不同。因此返回了false。 -
String s3 = new String(“1”) + newString(“1”),这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
-
s3.intern(),这一行代码,是将 s3中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,JDK1.6的做法是直接在常量池中生成一个 “11” 的对象。
但是在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。
String s4 = “11”, 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。
G1的String去重操作
垃圾回收算法
四种引用
-
强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“object obj=new object()"这种引用关系。无论任何情况下只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
-
软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常
- 软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
-
弱引用(weakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
- 弱引用发现即回收,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
-
虚引用(PhantomReference) :一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
- 虚引用必须和引用队列一起使用,在创建时必须为其提供一个引用队列,当GC时,如果发现一个对象还有虚引用,就会在对象回收后,把虚引用加入到引用队列。
垃圾标记
引用计数算法
可达性分析算法
所谓"GC Roots"根集合就是一组必须活跃的引用。
基本思路:
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象
才是存活对象。
哪些变量用作GC Roots?
在Java中,GC Roots包括如下几类元素:
- 虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。 - 本地方法栈内JNI (通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象。比如: Java类的引用类型静态变量方法区中常量引用的对象,比如:字符串常量池(string Table)里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用。比如基本数据类型对应的class对象,一些常驻的异常对象(如:
NullPointerException、outofMemoryError),系统类加载器。 - 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
finalize ()方法允许在子类中被重写,用于在对象被回收时进行资源释放.通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
Java的finalization机制与C++的析构函数有一些相似之处,但也有所不同
垃圾回收
JVM的垃圾回收算法有哪些?
- 标记清除(mark sweep):分为两步,第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记,第二步:在遍历一遍,将所有标记的对象回收掉;标记清除的缺点在于会产生大量内存碎片
- 分块拷贝算法(copying):将内存按照容量分为大小相等的两块,每次只使用其中一块,当一块内存不足时,将还存活的对象移动到另外一块,然后把使用过的那块整体清除回收。分块拷贝算法的缺点在于浪费空间
- 标记压缩(mark compact)类似于标记清除,但是标记清除的过程中对存活对象和垃圾对象集中整理,标记压缩不会产生没有内存碎片,但是效率极低
- 分代收集算法:
如下图,将内存空间分为 2 / 3 的老年代(old),1 / 3 的新生代(young),其中young区又分出 8 / 10的 伊甸区(Eden),1 / 10的from幸存区和1 / 10 的to幸存区。
- 对象优先在Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次 Minor GC(标记清除)。
- 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区,假定为from区;
- Eden 区再次 GC时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
- 对象每移动一次,年龄加1,当对象年龄大于一定阀值会直接移动到老年代,这个阈值默认是15,对象头用了4个字节记录对象年龄,因此对象年龄阈值不可大于15。
- 幸存区内存不足时,触发分配担保机制,超过指定大小的对象会直接进入老年代。
- 大对象如字符串、数组等需要大量连续内存空间的对象会直接进入老年代,避免为大对象分配内存时由于分配担保机制带来的复制导致的效率的降低。
- 当老年代容量不足时,会进行Full GC,对所有的线程STW(stop the world),对所有的区域执行标记清除。
- 分区收集算法:将整个堆空间划分为多个连续的小区间,在每个小区间里,独立使用,独立回收,这样可以减少一次GC所产生的停顿。
System.gc() / Runtime.getRuntime().gc()
System.gc()和Runtime.getRuntime().gc()都用于显式触发垃圾回收,用于建议JVM执行垃圾回收。这种方式与System.gc()的效果是一样的,它也不能保证立即执行垃圾回收,仍然取决于JVM的决策。
内存溢出与内存泄露
内存溢出:堆区没有空闲内存,并且垃圾收集器也无法提供更多的内存,称为内存溢出。
内存泄露:对象不会再被程序用到了,但是GC又不能回收他们的情况,称为内存泄露。
安全点与安全区域
安全点:程序执行时并非在所有地方都能停顿下来开始Gc,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint) ”。
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
- 主动式中断:
设置一个中断标志,各个线程运行到safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的 Safepoint 。但是,程序“不执行”的时候呢?例如线程处于Sleep 状态或Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域( Safe Region)来解决。
安全区域:安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始cc都是安全的。我们也可以把 safe Region看做是被扩展了的safepoint。
垃圾回收器
垃圾回收器(Garbage Collector,GC)
分类
- 按线程数分:串行(Serial)和并行(Parallel)
- 按工作模式分:独占式(只能STW才能GC)和并发式(边运行边GC)
- 按碎片处理方式:压缩式和非压缩式
- 按工作的内存区间:年轻代和老年代
评估GC的性能指标
现在设计垃圾回收器的标准:在最大吞吐量优先的情况下,降低停顿时间。
七款经典垃圾回收器
新生代垃圾回收器一般都采用复制算法,而老年代则除了CMS外都使用标记整理算法来兜底。
-
- Serial收集器(单线程+复制算法):新生代收集器,“单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是
它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
对于限定单个CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java 虚拟机运行在Client 模式下默认的新生代垃圾收集器。
- Serial收集器(单线程+复制算法):新生代收集器,“单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是
-
- ParNew收集器(Serial+多线程+复制算法):新生代收集器, ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew垃圾收集器是很多java虚拟机运行在Server 模式下新生代的默认垃圾收集器。
- ParNew收集器(Serial+多线程+复制算法):新生代收集器, ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
-
- Parallel Scavenge:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,
和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;
- Parallel Scavenge:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,
-
- Serial Old(单线程 + 标记整理) Serial收集器的老年代版本,单线程收集器。
-
- Parallel Old(多线程 + 标记整理) Parallel收集器的老年代版本.
-
- CMS(Concurrent Mark Sweep) 是一种老年代垃圾收集器,以获取最短回收停顿时间为目标,基于标记清除算法。
CMS收集器是第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS回收过程分为以下四步:
① 、 初始标记:暂停所有的其他用户线程(STW),并记录下直接与 root 相连的对象,由于跟GC Root直接关联的下级对象不会很多,因此这个过程很快。
②、并发标记:同时开启 GC 和用户线程,标记所有可达的对象。这个阶段不能保证在结束的时候能标记完所有的可达对象,因为应用线程在运行,可能会导致部分引用的变更,导致一些活对象不可达。为了解决这个问题,这个算法里会跟踪记录这些发生引用更新的地方。
③、重新标记阶段:再次STW,修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
④、并发清理:同时开启用户线程和GC 线程,对标记阶段判断已经死亡的对象进行清理。
- CMS(Concurrent Mark Sweep) 是一种老年代垃圾收集器,以获取最短回收停顿时间为目标,基于标记清除算法。
-
- G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。具备如下特点:
- 并行与并发:
- 并行:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。
- 并发:部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:能独立管理整个 GC 堆,但还是将堆空间分为了若干区域,这些区域包含了逻辑上的年轻代和老年代,但是从整个堆上来看,不要求整个年轻代或老年代区域一定连续。
- 空间整合:与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
垃圾回收步骤:初始标记、并发标记、最终标记、筛选回收
经典垃圾回收器组合使用
黑色实线表示组合。
- Serial GC 与 Serial Old GC(MSC)
- ParNew GC 与 CMS 与 Serial Old GC(MSC)
- CMS 采用的是标记清除算法,因此是不可以处理内存碎片的,最后需要 Serial Old 兜底,整理内存碎片。
- Parallel Scavenge GC与 Parallel Old GC
- (Parallel 系列有GC自动调节策略,与其他垃圾回收器不兼容;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量)
- (Parallel 系列有GC自动调节策略,与其他垃圾回收器不兼容;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量)
CMS的优缺点
优点:
- 第一款并发GC,边运行,边垃圾回收
- 低延迟,只在初始标记和重新标记阶段STW,降低延迟
缺点:
- 会产生内存碎片,CMS采用的是标记清除算法。
- 对CPU资源非常敏感,占用了一部分线程导致应用程序变慢。
- 无法清理浮动垃圾,重新标记阶段,只是对之前标记疑似垃圾的对象再次确认,但是对于并发标记阶段新产生的垃圾,将无法处理,需等待下一次GC。
G1
参数设置
G1的设计原则就是简化JVw性能调优,开发人员只需要简单的三步即可完成调优:
- 第一步:开启G1垃圾收集器:
-XX + UseG1GC
- 第二步:设置堆的最大内存:
-XX:G1HeapRegionSize
- 第三步:设置最大的停顿时间:
-XX:MaxGCpauseMillis
G1中提供了三种垃圾回收模式: YoungGc、Mixed GC和Full Gc,在不同的条件下被触发。
分区Region:化整为零
使用G1 收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。
可以通过-XX:G1HeapRegionsize
设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
设置H的原因:
对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。
垃圾回收过程
主要包括如下三个环节:
- 年轻代
- 老年代并发标记过程
- 混合回收(涉及年轻代和老年代一起的回收)
- 如果需求,单线程、独占式、高强度的Full GC还是继续存在的,它是真的GC的评估失败提供了一种失败保存机制,即强力回收。
一个区域对象可能被不同区域引用,为了避免全局扫描,G1为每个region设置了记忆集 remember set,记录哪些区域的对象引用了本区域的对象。
young GC
并发标记
混合回收