CMS收集器(并发)
G1收集器
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。JVM帮我们处理了不同硬件之间的差异,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。(一次编译到处运行)
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
JVM 内存区域主要分为虚拟机栈、本地方法栈、程序计数器、方法区、堆,
其中程序计数器、虚拟机栈和本地方法栈为线程私有,堆和方法区为线程共有。
堆主要用于存放各种类的实例对象和数组。堆是JVM中最大的一块内存。在java中被分为两个区域:年轻代和老年代。
栈是描述 java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调 用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
程序计数器是线程私有、占用内存较小、没有OOM异常,主要用于指令切换。
程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变计数器的值来选取下一条字节码指令。其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。 Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计 数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的 内存。
和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使⽤的(执行Java方法(字节码)服务),⽽本地⽅法栈是给本地⽅法使⽤的(C/C++)(Native方法服务。)
方法区也是线程共有的,存储的内容主要有常量、静态变量和类信息。
方法区即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. JDK8 已经被元空间取代。
元空间是JDK1.8之后的叫法,元空间存储在本地,而不在虚拟经济当中
,并将字符串常量池放到了堆中。
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看 一下这五个过程。
加载(loading) -> 验证 -> 准备 -> 解析 -> 初始化 -> 使⽤ -> 卸载
① 加载 “加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的⼀个阶段。在加载阶段,Java虚拟 机需要完成以下三件事情:
② 验证: 验证是连接阶段的第⼀步,这⼀阶段的⽬的是确保Class⽂件的字节流中包含的信息符合《Java虚拟机规 范》的全部约束要求,保证这些信息被当作代码运⾏后不会危害虚拟机⾃身的安全。
验证选项:
③ 准备: 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。⽐如此时有这样⼀⾏代码: public static int value = 123;它是初始化 value 的 int 值为 0,⽽⾮ 123。
④ 解析: 解析阶段是 Java 虚拟机将常量池内的符号引⽤替换为直接引⽤的过程,也就是初始化常量的过程。
⑤ 初始化: 初始化阶段,Java 虚拟机真正开始执⾏类中编写的 Java 程序代码,将主导权移交给应⽤程序。初始化阶段就是执⾏类构造器⽅法的过程。
虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件) 类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。 JVM 提供了 3 种类加载器:
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派 给父类去完成,(坑爹
)每一个层次类加载器都是如此,因此所有的加载请求都应该传送到引导类加载其中, 只有当父类加载器反馈自己无法完成这个请求的时候 (在它的加载路径下没有找 到所需加载的 Class),子类加载器才会尝试自己去加载。
优点:
双亲委派模型被破坏总共发⽣过 3 次:
垃圾回收(GC)是JVM的一大杀器,它使程序员可以更高效地专注于程序的开发设计,而 不用过多地考虑对象的创建销毁等操作。但是这并不是说程序员不需要了解GC。GC只是 Java编程中一项自动化工具,任何一个工具都有它适用的范围,当超出它的范围的时候,可 能它将不是那么自动,而是需要人工去了解与适应地适用。
java运行时内存的各个区域。对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言, 其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。内存分配和回收关注的为Java堆与方法区这两个区域。
垃圾回收需要完成的三件事:
哪些内存需要回收?—如何确定垃圾?(引用计数法、可达性分析算法)
如何回收?——垃圾回收算法(标记复制(新生代)、标记清除(几乎不用)、标记整理 (老年代)、分代收集)
何时回收?
引用计数描述的算法为: 给对象增加一个引用计数器
,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题
public class test {
public Object instance = null;
private byte[] bigSize = new byte[2 * 1024 * 1024];
public static void testGC() {
test test1 = new test();
test test2 = new test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
// 强制jvm进行垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
结果如下:
[GC (System.gc()) 6092K->856K(125952K), 0.0007504 secs]
从结果可以看出,GC日志包含" 6092K->856K(125952K)",意味着虚拟机并没有因为这两个对象互相引用就不回收他们。即JVM并不使用引用计数法来判断对象是否存活。
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾 对象,其余未标记的对象都是垃圾对象。
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。
在Java语言中,可作为GC Roots的对象包含下面几种:(两个栈,一个方法区)
范例:对象自我拯救
public class test {
public static test test;
public void isAlive() {
System.out.println("I am alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
test = this;
}
public static void main(String[] args)throws Exception {
test = new test();
test = null; System.gc();
Thread.sleep(500);
if (test != null) {
test.isAlive();
}else {
System.out.println("no,I am dead :");
}
// 下面代码与上面完全一致,但是此次自救失败
test = null; System.gc();
Thread.sleep(500);
if (test != null) {
test.isAlive();
}else {
System.out.println("no,I am dead :");
}
}
}
从上面代码示例我们发现,finalize方法确实被JVM触发,并且对象在被收集前成功逃脱。
但是从结果上我们发现,两个完全一样的代码片段,结果是一次逃脱成功,一次失败。这是因为,任何一个对象的finalize()方法都只会被系统自动调用一次,如果相同的对象在逃脱一次后又面临一次回收, 它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败。
public static User user = new User();
算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法,比较简单。
存在的问题:
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一 块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
优点是性能⾼,但内存利⽤率不⾼。
优化:
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
优点:不会产生内存碎片
分代收集(Generational Collector)算法的将堆内存划分为新生代、老年代和永久代。新生代又被 进一步划分为 Eden 和 Survivor 区,其中 Survivor 由FromSpace(Survivor0)和ToSpace(Survivor1)组成。
通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。分代收集,是基于 这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采 取不同的回收算法进行垃圾回收,以便提高回收效率。
“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。
特别地,在分代收集算法中,对象的存储具有以下特点:
何时回收之前,先了解一下几种 GC。
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用标记-复制和标记-清除 垃圾回收算法; 年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和 年老代分别提供了多种不同的垃圾收集器,JDK Sun HotSpot 虚拟机的垃圾收集器如下:
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使用。所处的区域,表示它是属于新生代收集器还是老年代收集器。在讲具体的收集器之前我们先来明确三个概念:
在JDK1.3.1之前,单线程回收器是唯一的选择。它的单线程意义不仅仅是说它只会使用 一个CPU或一个收集线程去完成垃圾收集工作。而且它进行垃圾回收的时候,必须暂停其他 所有的工作线程(Stop The World,STW),直到它收集完成。它适合Client模式的应用,在单CPU环境下,它简单高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。
串行的垃圾收集器有两种,Serial与Serial Old,一般两者搭配使用。新生代采用 Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法
Client应用或者命令行程序可以,通过-XX:+UseSerialGC
可以开启上述回收模式。下 图是其运行过程示意图。
整体来说,并行垃圾回收相对于串行,是通过多线程运行垃圾收集的。也会stop-the- world。适合Server模式以及多CPU环境。一般会和jdk1.5之后出现的CMS搭配使用。并行的垃圾回收器有以下几种
复制算法
。使用- XX:+UseParNewGC
和 SerialOld收集器组合进行内存回收。如下图所示。Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
特性:
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线 程,直到它收集结束(Stop The World).
应用场景:
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
优势:
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 实际上到现在为止 : 它依然是虚拟机运行在Client模式下的默认新生代收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
应用场景:
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾 收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所 有其他的工作线程。
对比分析: 与Serial收集器对比:
ParNew收集器是许多运行在Server模式下的虚拟机首选的新生代收集器,其中一个原因是,除了 Serial收集器之外,目前只有ParNew收集器能与CMS收集器配合工作
为什么只有ParNew能与CMS收集器配合?
CMS收集器(并发)
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最 短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算 法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
缩短回收停顿时间为目标、注重用户体验;真正意义并发收集器、让垃圾收集线程与用户线程(基本上) 同时工作。
CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前⾯⼏种收集器来说更复 杂⼀些,整个过程分为4个步骤:
特点
优点:并发收集、低停顿。
缺点: 一:CMS收集器对CPU资源非常敏感。二.CMS收集器无法处理浮动垃圾
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器使用两个参数控制吞吐量:
直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停
顿70毫秒。停顿时间下降的同时,吞吐量也下降了。
应用场景:
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
对比分析:
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。
GC自适应的调节策略:
Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
Parallel Old 收集器是 Parallel Scavenge 的年老代版本,使用多线程的标记-整理算 法,在 JDK1.6 才开始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只 能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了 在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑 新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略
特点
应用场景
G1收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收 集器两个最突出的改进是:会进行压缩
G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收。
G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。
G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的。无论如何,G1收集器采用的算法都意味着一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。 G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象
年轻代垃圾收集
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域。
老年代垃收集
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:
希望对大家有帮助吧,复习的时候有用吧。