关于后端:JVM面试大总结

4次阅读

共计 16914 个字符,预计需要花费 43 分钟才能阅读完成。

一、汇总

JVM 是运行在操作系统之上的,它与硬件没有间接的交互。先说一下 JVM 的内存区域,当函数开始运行时,JVM 拿到本人的内存将本人的内存区域进行了宰割,分为五块区域:线程共享的有堆、办法区,线程公有的有 java 栈、本地办法栈、程序计数器。

办法区是用来加载 class 文件的区域,动态变量、常量、类元信息、运行时的常量池寄存在在办法区中,办法区在 jdk1.7 之前它又叫做永恒代,然而 jdk1.8 之后改成元数据空间了;

new 的对象都寄存在堆中;

栈也叫栈内存,8 种类型的根本变量、对象的援用变量、实例办法都是在函数的栈内存中调配,栈中的数据都是以栈帧的格局存在,每执行一个办法都会产生一个栈帧,保留到栈 (后进先出) 的顶部,顶部栈就是以后的办法,该办法执行结束后会主动将此栈帧出栈。java 栈随着线程创立而产生,随着线程的终结而销毁,每个线程在开拓、运行的过程中会独自创立这样的一份内存,有多少个线程就可能有多少个栈区;

本地办法栈是存储 C ++ 的 native 办法运行时候的栈区;程序计数器是指向以后程序运行的地位。

内存模型、类加载机制、GC 是重点,性能调优局部更偏差利用, 重点突出实际能力,编译器优化和执行模式局部偏差于实践根底, 重点把握知识点。

1、JMM 如何保障原子性、一致性、可见性

在 java 中提供了两个高级的字节码指令 monitorenter 和 monitorexit,应用对应的关键字 Synchronized 来保障代码块内的操作是原子的。

2、环境变量了解

classpath 是 javac 编译器的一个环境变量。它的作用与 import、package 关键字无关。

package 的所在位置,就是设置 CLASSPATH 当编译器面对 import packag 这个语句时,它先会查找 CLASSPATH 所指定的目录,并检视子目录 java/util 是否存在,而后找出名称吻合的已编译文件(.class 文件)。如果没有找到就会报错!

二、分区和内存模型

内存模型叫做内存构造。所谓模型是行为 + 数据 也就是 JVM 的内存构造布局,加上内存的执行行为,栈中数据如何调配,堆中数据如何调配,堆栈数据运行时如何同步,加锁状态数据如何同步,也就是 happen before 那一套。

1、JVM 的划分及作用

Java 虚拟机次要分为以下几个区:

(1)办法区

​ a. 有时候也成为永恒代,在该区内很少产生垃圾回收,然而并不代表不产生 GC,在这里进行的 GC 次要是对办法区里的常量池和对类型的卸载

​ b. 办法区次要用来存储已被虚拟机加载的类的信息、常量、动态变量和即时编译器编译后的代码等数据。

​ c. 该区域是被线程共享的。

​ d. 办法区里有一个运行时常量池,用于寄存动态编译产生的字面量和符号援用。该常量池具备动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

(2)虚拟机栈:

​ a. 虚拟机栈也就是咱们平时所称的栈内存, 它为 java 办法服务,每个办法在执行的时候都会创立一个栈帧,用于存储局部变量表、操作数栈、动静链接和办法进口等信息。

​ b. 虚拟机栈是线程公有的,它的生命周期与线程雷同。

​ c. 局部变量表里存储的是根本数据类型、returnAddress 类型(指向一条字节码指令的地址)和对象援用,这个对象援用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的地位。局部变量所需的内存空间在编译器间确定

​ d. 操作数栈的作用次要用来存储运算后果以及运算的操作数,它不同于局部变量表通过索引来拜访,而是压栈和出栈的形式

​ e. 每个栈帧都蕴含一个指向运行时常量池中该栈帧所属办法的援用,持有这个援用是为了反对办法调用过程中的动静连贯. 动静链接就是将常量池中的符号援用在运行期转化为间接援用。

(3)本地办法栈
本地办法栈和虚拟机栈相似,只不过本地办法栈为 Native 办法服务。

(4)堆

java 堆是所有线程所共享的一块内存,在虚拟机启动时创立,简直所有的对象实例都在这里创立,因而该区域常常产生垃圾回收操作。

堆外面分为新生代和老生代(java8 勾销了永恒代,采纳了 Metaspace),新生代包 含 Eden+Survivor 区,survivor 区外面分为 from 和 to 区,内存回收时,如果用的是复制算法,从 from 复制到 to,当通过一次或者屡次 GC 之后,存活下来的对象会被挪动到老年区,当 JVM 内存不够用的时候,会触发 Full GC,清理 JVM 老年区当新生区满了之后会触发 YGC, 先把存活的对象放到其中一个 Survice 区,而后进行垃圾清理。

因为如果仅仅清理须要删除的对象,这样会导致内存碎片,因而个别会把 Eden 进行齐全的清理,而后整顿内存。那么下次 GC 的时候,就会应用下一个 Survive,这样循环应用。如果有特地大的对象,新生代放不下,就会应用老年代的担保,间接放到老年代外面。因为 JVM 认为,个别大对象的存活工夫个别比拟长远。

(5)程序计数器

内存空间小,字节码解释器工作时通过扭转这个计数值能够选取下一条须要执行的字节码指令,分支、循环、跳转、异样解决和线程复原等性能都须要依赖这个计数器实现。该内存区域是惟一一个 java 虚拟机标准没有规定任何 OOM 状况的区域。

2、heap 和 stack 有什么区别

(1)申请形式

stack: 由零碎主动调配。例如,申明在函数中一个局部变量 int b; 零碎主动在栈中为 b 开拓空间

heap: 须要程序员本人申请,并指明大小,在 c 中 malloc 函数,对于 Java 须要手动 new Object()的模式开拓

(2)申请后零碎的响应

stack:只有栈的残余空间大于所申请空间,零碎将为程序提供内存,否则将报异样提醒栈溢出。

heap:首先应该晓得操作系统有一个记录闲暇内存地址的链表,当零碎收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,而后将该结点从闲暇结点链表中删除,并将该结点的空间调配给程序。另外,因为找到的堆结点的大小不肯定正好等于申请的大小,零碎会主动的将多余的那局部从新放入闲暇链表中。

(3)申请大小的限度

stack:栈是向低地址扩大的数据结构,是一块间断的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是零碎预先规定好的,在 WINDOWS 下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的残余空间时,将提醒 overflow。因而,能从栈取得的空间较小。

heap:堆是向高地址扩大的数据结构,是不间断的内存区域。这是因为零碎是用链表来存储的闲暇内存地址的,天然是不间断的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中无效的虚拟内存。由此可见, 堆取得的空间比拟灵便,也比拟大。

(4)申请效率的比拟

stack:由零碎主动调配,速度较快。但程序员是无法控制的。

heap:由 new 调配的内存,个别速度比较慢,而且容易产生内存碎片,不过用起来最不便。

(5)heap 和 stack 中的存储内容

stack:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,而后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,而后是函数中的局部变量。留神动态变量是不入栈的。

当本次函数调用完结后,局部变量先出栈,而后是参数,最初栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点持续运行。

heap:个别是在堆的头部用一个字节寄存堆的大小。堆中的具体内容有程序员安顿。

3、Jvm 内存模型(重排序、内存屏障)

内存屏障:能够阻挡编译器的优化,也能够阻挡处理器的优化

happens-before 准则:

1:一个线程的 A 操作总是在 B 之前,那多线程的 A 操作必定切实 B 之前。

2:monitor 再加锁的状况下,持有锁的必定先执行。

3:volatile 润饰的状况下,写先于读产生

4:线程启动在一起之前 strat

5:线程死亡在所有之后 end

6:线程操作在所有线程中断之前

7:一个对象构造函数的完结都该对象的 finalizer 的开始之前

8:传递性,如果 A 必定在 B 之前,B 必定在 C 之前,那 A 必定是在 C 之前。

主内存:所有线程共享的内存空间

工作内存:每个线程特有的内存空间

三、类加载过程

1、JVM 的类加载过程

Java 类加载须要经验一下几个过程:

(1)加载

加载时类加载的第一个过程,在这个阶段,将实现一下三件事件:

​ a. 通过一个类的全限定名获取该类的二进制流。

​ b. 将该二进制流中的动态存储构造转化为办法去运行时数据结构。

​ c. 在内存中生成该类的 Class 对象,作为该类的数据拜访入口。

(2)验证

验证的目标是为了确保 Class 文件的字节流中的信息不回危害到虚拟机. 在该阶段次要实现以下四种验证:

​ a. 文件格式验证:验证字节流是否合乎 Class 文件的标准,如主次版本号是否在以后虚拟机范畴内,常量池中的常量是否有不被反对的类型.

​ b. 元数据验证: 对字节码形容的信息进行语义剖析,如这个类是否有父类,是否集成了不被继承的类等。

​ c. 字节码验证:是整个验证过程中最简单的一个阶段,通过验证数据流和控制流的剖析,确定程序语义是否正确,次要针对办法体的验证。如:办法中的类型转换是否正确,跳转指令是否正确等。

​ d. 符号援用验证:这个动作在前面的解析过程中产生,次要是为了确保解析动作能正确执行。

​ e. 筹备

筹备阶段是为类的动态变量分配内存并将其初始化为默认值,这些内存都将在办法区中进行调配。筹备阶段不调配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起调配在 Java 堆中。

(3)解析

该阶段次要实现符号援用到间接援用的转换动作。解析动作并不一定在初始化动作实现之前,也有可能在初始化之后。

(4)初始化

初始化时类加载的最初一步,后面的类加载过程,除了在加载阶段用户应用程序能够通过自定义类加载器参加之外,其余动作齐全由虚拟机主导和管制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。

2、类加载器

类加载器 就是依据指定全限定名称将 class 文件加载到 JVM 内存,转为 Class 对象。

次要有一下四品种加载器:

(1)启动类加载器 (Bootstrap ClassLoader) 用来加载 java 外围类库,无奈被 java 程序间接援用,由 C ++ 语言实现(针对 HotSpot), 负责将寄存在 <JAVA_HOME>\lib 目录或 -Xbootclasspath 参数指定的门路中的类库加载到内存中。。

(2)扩大类加载器(extensions class loader): 它用来加载 Java 的扩大库。Java 虚拟机的实现会提供一个扩大库目录。该类加载器在此目录外面查找并加载 Java 类,负责加载 <JAVA_HOME>\lib\ext 目录或 java.ext.dirs 零碎变量指定的门路中的所有类库。。

(3)零碎类加载器(system class loader)也叫利用类加载器:它依据 Java 利用的类门路(CLASSPATH)来加载 Java 类。一般来说,Java 利用的类都是由它来实现加载的。能够通过 ClassLoader.getSystemClassLoader()来获取它。负责加载用户类门路(classpath)上的指定类库,咱们能够间接应用这个类加载器。个别状况,如果咱们没有自定义类加载器默认就是用这个加载器。

(4)用户自定义类加载器,通过继承 java.lang.ClassLoader 类的形式实现。

3、双亲委派机制

如果一个类加载器收到类加载的申请,它首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器实现。每个类加载器都是如此,只有当父加载器在本人的搜寻范畴内找不到指定的类时(即 ClassNotFoundException),子加载器才会尝试本人去加载。

为什么须要双亲委派模型?

在这里,先想一下,如果没有双亲委派,那么用户是不是能够本人定义一个 java.lang.Object 的同名类,java.lang.String 的同名类,并把它放到 ClassPath 中, 那么类之间的比拟后果及类的唯一性将无奈保障,因而,为什么须要双亲委派模型?避免内存中呈现多份同样的字节码。

怎么突破双亲委派模型?

突破双亲委派机制则不仅要继承 ClassLoader 类,还要重写 loadClass 和 findClass 办法。

四、垃圾回收算法

1、java 中垃圾收集的办法有哪些?

java 中有四种垃圾回收算法,别离是标记革除法、标记整顿法、复制算法、分代收集算法;

①标记 - 革除

这是垃圾收集算法中最根底的,依据名字就能够晓得,它的思维就是标记哪些要被回收的对象,而后对立回收。

第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;

第二步:在遍历一遍,将所有标记的对象回收掉;

这种办法很简略,然而会有两个次要问题:1. 效率不高,标记和革除的效率都很低;2. 会产生大量不间断的内存碎片,导致当前程序在调配较大的对象时,因为没有短缺的间断内存而提前触发一次 GC 动作。

②复制算法:

为了解决效率问题,复制算法将可用内存按容量划分为相等的两局部,而后每次只应用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,而后一次性分明完第一块内存,再将第二块上的对象复制到第一块。

然而这种形式,内存的代价太高,每次基本上都要节约个别的内存。

于是将该算法进行了改良,内存区域不再是依照 1:1 去划分,而是将内存划分为 8:1:1 三局部,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。每次都会优先应用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,而后革除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对象通过调配担保机制复制到老年代中。(java 堆又分为新生代和老年代)

*③标记 - 整顿:

第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;

第二步:将所有的存活的对象向一段挪动,将端边界以外的对象都回收掉;

该算法次要是为了解决标记 - 革除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在革除对象的时候现将可回收对象挪动到一端,而后革除掉端边界以外的对象,这样就不会产生内存碎片了。

④分代收集

当初的虚拟机垃圾收集大多采纳这种形式,它依据对象的生存周期,将堆分为新生代和老年代。在新生代中,因为对象生存期短,每次回收都会有大量对象死去,那么这时就采纳复制算法。老年代里的对象存活率较高,没有额定的空间进行调配担保,所以能够应用标记 - 整顿 或者 标记 - 革除。

内存利用率:标记整顿算法 > 标记革除算法 > 复制算法

内存连续性:标记整顿算法 = 复制算法 > 标记革除算法

效率:

对象存活率不高:复制算法 > 标记分明算法 > 标记整顿算法对象存活率高:标记革除算法 > 复制算法 > 标记整顿算法新生代对象存活率不高:抉择复制算法

老年代对象存活率较高:抉择标记革除算法 标记整顿算法

2、JVM 如何判断一个对象能够被回收

判断一个对象是否存活有两种办法:

(1)援用计数法

所谓援用计数法就是给每一个对象设置一个援用计数器,每当有一个中央援用这个对象时,就将计数器加一,援用生效时,计数器就减一。当一个对象的援用计数器为零时,阐明此对象没有被援用,也就是“死对象”, 将会被垃圾回收.

援用计数法有一个缺点就是无奈解决循环援用问题,也就是说当对象 A 援用对象 B,对象 B 又援用者对象 A,那么此时 A,B 对象的援用计数器都不为零,也就造成无奈实现垃圾回收,所以支流的虚拟机都没有采纳这种算法。

(2)可达性算法(援用链法)

该算法的基本思路就是通过一些被称为援用链(GC Roots)的对象作为终点,从这些节点开始向下搜寻,搜寻走过的门路被称为(Reference Chain),当一个对象到 GC Roots 没有任何援用链相连时(即从 GC Roots 节点到该节点不可达),则证实该对象是不可用的。

GCRoot 对象有四种对象:

1、jvm 栈中的援用对象

2、办法区中的援用动态常量

3、办法区中的一般援用常量 4、native 办法中的援用对象

一个对象通过两次标记为垃圾对象,该对象才会被断定为垃圾对象。

3、JVM 内存调配与回收策略

内存调配:

(1)栈区:栈分为 java 虚拟机栈和本地办法栈

(2)堆区:堆被所有线程共享区域,在虚拟机启动时创立,惟一目标寄存对象实例。堆区是 gc 的次要区域,通常状况下分为两个区块年老代和年轻代。更细一点年老代又分为 Eden 区,次要放新创建对象,From survivor 和 To survivor 保留 gc 后幸存下的对象,默认状况下各自占比 8:1:1。

(3)办法区:被所有线程共享区域,用于寄存已被虚拟机加载的类信息,常量,动态变量等数据。被 Java 虚拟机形容为堆的一个逻辑局部。习惯是也叫它永恒代(permanment generation)

(4)程序计数器:以后线程所执行的行号指示器。通过扭转计数器的值来确定下一条指令,比方循环,分支,跳转,异样解决,线程复原等都是依赖计数器来实现。线程公有的。

回收策略以及 Minor GC 和 Major GC:

Minor GC 是新生代 GC,指的是产生在新生代的垃圾收集动作。因为 java 对象大都是朝生夕死的,所以 Minor GC 十分平庸,个别回收速度也比拟 i 快。

Major GC/Full GC 是老年代 GC,指的是产生在老年代的 GC,呈现 Major GC 个别常常会伴有 Minor GC,Major GC 的速度比 Minor GC 慢的多。

(1)对象优先在堆的 Eden 区调配。

(2)大对象间接进入老年代。

(3)长期存活的对象将间接进入老年代。

当 Eden 区没有足够的空间进行调配时,虚构机会执行一次 Minor GC.Minor GC 通常产生在新生代的 Eden 区,在这个区的对象生存期短,往往产生 GC 的频率较高,回收速度比拟快;Full Gc/Major GC 产生在老年代,个别状况下,触发老年代 GC 的时候不会触发 Minor GC, 然而通过配置,能够在 Full GC 之前进行一次 Minor GC 这样能够放慢老年代的回收速度。

4、GC 流程

java 堆 = 新生代 + 老年代;

新生代 = Eden + Suivivor(S0 + S1),默认分配比例是 8:1:1;

当 Eden 区空间满了的时候,就会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象会被调配到 Survivor 区

大对象(须要大量间断内存空间的对象)会间接被调配到老年代

如果对象在 Eden 中出世,并且在经验过一次 Minor GC 之后依然存活,被调配到存活区的话,年龄 +1,尔后每经验过一次 Minor GC 并且存活下来,年龄就 +1,当年龄达到 15 的时候,会被降职到老年代;

当老年代满了,而无奈包容更多对象的话,会触发一次 full gc;full gc 存储的是整个内存堆(包含年老代和老年代);

Major GC 是产生在老年代的 GC,清理老年区,常常会随同至多一次 minor gc;

5、垃圾收集器及各自的特点

垃圾收集器是 JVM 调优中最外围的一个知识点,咱们常说的 JVM 调优其实都是依据对应的垃圾收集器个性而去做调整和优化。

垃圾收集器尽管看起来数量比拟多,但其实总体逻辑都是因为咱们硬件环境的降级而演变进去的产品,不同垃圾收集器的产生总体能够划分为几个阶段。。

第一阶段:单线程收集时代(Serial 和 Serial Old)

第二阶段:多线程收集时代(Parallel Scanvenge 和 Parallel Old)

第三阶段:并发收集时代(ParNew 和 CMS)

第四阶段:智能并发收集时代(G1)

上面的图一方面介绍了有哪些垃圾收集器,另外一方面也形容了每个垃圾收集器是负责哪个分代(新生代、老年的)的垃圾收集,还有一部分信息是通知咱们每个新生代的垃圾收集器能够与哪些老年代的收集配合工作。

罕用组合:Serial+Serial Old,Parallel Scavenge+Parallel Old,ParNew+CMS,G1(不须要组合其余收集器)。

Serial 垃圾收集流程

Serial 会开启一个线程进行垃圾收集,在收集的 整个过程都会暂停用户线程(Stop the Word),直到垃圾收集结束,如果把垃圾收集的过程当作清扫房间卫生,那么 Serial 的收集过程就是在你收集房间的时候,你首先会让房间里的人都进来,而后你再安心清扫房间,直到你清扫结束了能力让里面的人进来,这样就不必放心你一边清扫房间一边还有人在房间里扔垃圾了。

留神:说到“暂停用户线程”,这里也是各种垃圾收集器的一个辨别指标,前面的有些垃圾收集器收集的某些阶段是不须要暂停用户线程的。

收集器特点

收集区域: Serial(新生代),Serial Old(老年代)。

应用算法: Serial(标记复制法),Serial Old(标记整顿法)。

收集形式: 单线程收集。

劣势: 内存资源占用少、单核 CPU 环境最佳选项。

劣势: 整个收集过程须要进展用户线程。多核 CPU、内存富足的环境,资源优势无奈利用起来。

Parallel Scavenge 工作流程

Parallel Scavenge 和 Parallel Old 的工作机制一样,这里以 Parallel Scavenge 为例,Parallel Old 在收集过程中会开启多个线程一起收集,整个过程都会暂停用户线程,直到整个垃圾收集过程完结。和之前的 Serial 垃圾收集器一比照,同样进行垃圾收集前都是先叫其他人都来到房间,然而不同的是 serial 只有一个人清扫房间,而这里却是有多集体一起清扫房间,所以从这一点看 Parallel 系列的收集器要比之前的效率高上很多。

收集器特点

收集区域: Parallel Scavenge(新生代),Parallel Old(老年代)。

应用算法: Parallel Scavenge(标记复制法),Parallel Old(标记整顿法)。

收集形式: 多线程。

劣势: 多线程收集,CPU 多核环境下效率要比 serial 高。

劣势: 整个收集过程须要进展用户线程。

③ParNew 收集器流程

ParNew 收集流程和 Parallel Scavenge 一样 , 同样是先进行应用程序线程,再进行多线程同时收集,整个收集过程都会暂停用户线程(Stop the Word),直到垃圾收集结束。

ParNew 的特点

收集区域: 新生代。

应用算法: 标记复制法。

收集形式: 多线程。

搭配收集器: CMS。

劣势: 多线程收集,CPU 多核环境下效率要比 serial 高,新生代惟一一个能与 CMS 配合的收集器。

劣势: 整个收集过程须要进展用户线程。

CMS 收集器

为了尽量减少用户线程的进展工夫,CMS 采纳了一种全新的策略使得在垃圾回收过程中的某些阶段用户线程和垃圾回收线程能够一起工作,这样就防止了因为长时间的垃圾回收而使用户线程始终处于期待之中。

整个过程就像咱们清扫房间的时候能够让大家留在房间里工作,等我把房间的其余中央都清扫完,只剩大家工作的那局部区域的垃圾,这个时候再让大家到房间里面去,我再把房间里那些剩下的中央清理洁净就行了,这样做的益处就是大家的工作工夫变长了,在房间外期待的工夫变短了。

CMS 也是按这个逻辑把整个垃圾收集的过程分成四个阶段,别离是初始标记、并发标记、从新标记、并发清理四个阶段,而后 CMS 会依据每个阶段不同的个性来决定是否进展用户线程。

阶段一:初始标记

初始标记的目标是先把所有 GC Root 间接援用的对象进行标记,因为须要防止在标记 GC Root 的过程还有程序在持续产生 GC Root 对象,所以这个过程是须要须要进行用户线程 , 因为这个过程只会标记 GC Root 的间接援用,并不会对整个 GC Root 的援用进行遍历,所以这个过程速度也是所有阶段中最快的。

阶段二:并发标记

并发标记阶段的工作就是把阶段一标记好的 GC Root 对象进行深度的遍历,找到所有与 GC Root 关联的对象并进行标记,这个过程中是采纳多线程的形式进行遍历标记,对整个 JVM 的 GC Root 进行遍历的过程是垃圾收集过程中最耗时的一步,CMS 为了思考尽量不停顿用户线程,所以这个阶段是不进行用户线程的,也就是说这个阶段 JVM 会调配一些资源给用户线程执行工作,通过这样的形式缩小用户线程的进展工夫。

阶段三:从新标记

因为在阶段二的时候用户线程同时也在运行,这个过程中又会产生新的垃圾,所以从新标记阶段次要工作是把上一个阶段中产生的新垃圾进行标记(应用多线程标记),很显然这个过程是对上一个阶段用户线程运行遗留的垃圾进行标记,所以数量 是非常少执行工夫也是最短的,当然为了防止这个过程再次产生新的垃圾,所以从新标记的过程是会进展用户线程的。

阶段四:并发清理

并发清理阶段是对那些被标记为可回收的对象进行清理,在个别状况下并发清理阶段是应用的标记革除法,因为这个过程不会牵扯到对象的地址变更,所以 CMS 在并发清理阶段是不须要进行用户线程的。也正因为并发清理阶段用户线程也能够同时运行,所以在用户线程运行的过程中天然也会产生新的垃圾,这也就是导致 CMS 收集器会产生“浮动垃圾”的起因。

当然,在一种状况下并发清理阶段 CMS也会进展用户线程,这就和咱们之前说过的 CMS 选用的垃圾回收算法有关系,因为个别状况下应用的都是标记革除法,然而标记革除法的弊病就是在于会产生空间碎片,所以当空间碎片达到了肯定水平时,此时 CMS 会应用标记整顿法解决空间碎片的问题,不过因为标记整顿法会将对象的地位进行移动并更新对象的援用的指向地址,那么这个过程中用户线程同时运行的话会产生并发问题,所以当 CMS 进行碎片整顿的时候必须得进行用户线程。

CMS 的特点

收集区域: 老年代。

应用算法: 标记革除法 + 标记整顿法。

收集形式: 多线程。

搭配收集器: ParNew。

劣势: 多线程收集,收集过程不进行用户线程,所以用户申请进展工夫短。

CMS 遗留的问题

CMS 收集器开拓了一条垃圾收集的新思路,不过这么好的垃圾收集器却始终没有被 Hospot 虚拟机纳入到默认的垃圾收集器,到 Jdk8 应用的默认收集器都还是 Parallel scavenge 和 Parallel old,这其中十分重要的起因就是 CMS 遗留了几个比拟头疼的问题。

1、浮动垃圾

在并发清理阶段因为需垃圾收集线程是和用户线程同时执行工作的,这个时候用户线程运行时产生的垃圾是无奈在以后阶段进行回收的,所以这段时间用户线程产生的新垃圾只能遗留到下一次收集, 这些在垃圾收集过程中新产生的垃圾咱们称为浮动垃圾。

3、空间碎片整顿造成卡顿

CMS 在平时状况下会应用标记革除法进行回收,只有在老年代的空间碎片达到肯定水平,这个时候就会应用标记整顿法对内存的空间碎片进行整顿,因为标记整顿的过程须要挪动对象的地位,所以这个过程只能 Stop the word,这个时候内存越大那么这个收集工夫就越长,造成这种卡顿景象。

4、可能导致系统长时间的假死。

因为在并发革除阶段会有新的对象产生,在有担保机制的状况下,当新生代垃圾清理的时候存活的对象大多,导致 Survior 区无奈包容全副的对象,这时就会触发担保机制,这里存活的对象外面会有一部分会间接进入老年代,所以在每次 GC 的时候老年代须要预留一部分内存进去,所以通常 CMS 在老年代占用了大略百分之七八十的时候就进行 FullGC。

不过这段时间的产生对象的总体大小是未知的,如果新生代存活的对象十分多, 这些担保的对象转移到老年代的时候可能导致老年代预留的空间也不足以包容,那么此时 CMS 不得不进行一次 Stop the word 的 Full GC,因为此时堆空间曾经齐全占满,这个时候曾经无奈应用并发的清理形式进行收集了,所以此时只能进行用户线程来分心进行垃圾收集,而这时候老年代收集器不得不从 CMS 切换成 Serial old 垃圾收集器来进行垃圾收集。

至于这里为什么要应用单线程的 Serial old, 而不抉择多线程的 Parallel Old,那是因为 CMS 的新生代收集器是 ParNew,而 ParNew 只能与 CMS 和 Serial Old 配合),所以这也是个无奈的抉择。而切换成 Serial old 来进行垃圾收集的时候就有问题了,Serial old 收集器是单线程的,它只实用于内存大小在几十到上百 M 的大小,而往往咱们当初的内存大小都是几 G 到几十 G,所以这种状况下整个垃圾收集的工夫可能会特地特地长,有时候可能达到几个小时甚至好几天的都有可能。

G1 收集器

CMS 创始了垃圾收集器的一个新时代,它实现了垃圾收集和用户线程同时执行,达到垃圾收集的过程不进行用户线程的指标,这个思路作为前面的收集器提供了一个很好的榜样。时代向前优化不止,除了须要解决了 CMS 遗留了的几个问题外,硬件资源的升级换代,可用的内存资源越来越多始终是促成垃圾收集器倒退的一个外围驱动力,可应用的内存资源变多对于软件来说这当然是个坏事,不过对于垃圾收集器来说就变得越来越麻烦了,随着倒退咱们发现传统垃圾收集器的收集形式曾经不适用于这种大内存的垃圾收集了。

不论是 Serial 系列、Parallel 系列、CMS 系列,它们都是基于把内存进行物理分区的模式把 JVM 内存分成老年代、新生代、永恒代或 MetaSpace,这种分区模式下进行垃圾收集时必须对某个区域进行整体性的收集(比方整个新生代、整个老年代收集或者整个堆),原来的内存空间都不是很大,个别就是几 G 到几十 G,但当初的硬件资源倒退可用的内存达到几百 G 甚至上 T 的水平,那么 JVM 中的某一个分代区域就可能会有几十上百 G 的大小,那么如果这时候采纳传统模式下的物理分区的收集的话,每次垃圾扫描内存区域变大了、那么须要的清理工夫天然就会变得更加长了;换做打扫卫生来说,原来你只须要清扫几个小办公室就行了,然而随着公司业务倒退整栋楼是都是你公司了,这个时候你须要清扫公司卫生的工夫无疑也会变得特地长。

所以问题呈现了,那么天然就有人会来解决的,G1 就是在这种环境下诞生的,G1 首先汲取了 CMS 低劣的思路,还是应用并发收集的模式,然而更重要的是 G1 摒弃了原来的物理分区,而是把整个内存分成若干个大小的 Region 区域,而后由不同的 Region 在逻辑上来组合成各个分代,这样做的益处是 G1 进行垃圾回收的时候就能够用 Region 作为单位来进行更细粒度的回收了,每次回收能够只针对某一个或多个 Region 来进行回收。

G1 最外围的分区根本单位 Region,G1 没有像之前一样把堆内存划分为固定间断的几块区域,而是齐全舍弃了进行内存上的物理分区,而是把堆内存拆分成了大小为 1M-32M 的 Region 块,而后以 Region 为单位自在的组合成新生代、老年代、Eden 区、survior 区、大对象区(Humonggous Region),随着垃圾回收和对象调配每个 Region 也不会始终固定属于哪个分代,咱们能够认为 Region 能够随时表演任何一个分代区域的内存。

G1 的回收流程和 CMS 逻辑大致相同,别离进行初始标记、并发标记、从新标记、筛选革除,区别在最初一个阶段 G1 不会间接进行革除,而是会依据设置的进展工夫进行智能的筛选和部分的回收。

阶段一:初始标记

初始标记额目标是先把所有 GC Root 间接援用的对象进行标记,因为须要防止在标记 GC Root 的过程还有程序在持续产生 GC Root 对象,所以这个过程是须要进行用户线程 , 因为这个过程并不会对整个 GC Root 的援用进行遍历,所以这个过程速度是十分快的。

阶段二:并发标记

并发标记阶段的工作就是把阶第一段标记好的 GC Root 对象进行深度的遍历,找到所有与 GC Root 关联的对象并进行标记,这个过程中是采纳多线程的形式进行遍历标记,对整个 JVM 的 GC Root 进行遍历的过程是垃圾收集过程中最耗时的一步,为了尽量不停顿用户线程,所以这个阶段 GC 线程会和用户线程同时运行, 通过这样的形式缩小用户线程的进展工夫。

阶段三:最终标记

因为在上个阶段用户线程同时也在运行,用户线程运行的过程中又会产生新的垃圾,所以从新标记阶段次要工作是把上一个阶段中产生的新垃圾进行标记(应用多线程标记),很显然这个过程是对上一个阶段用户线程运行遗留的垃圾进行标记,所以数量是非常少执行工夫也是十分短的,当然为了防止这个过程再次产生新的垃圾,所以从新标记的过程是会进展用户线程的。

阶段四:筛选回收

把存活的对象复制到闲暇 Region 区域,再依据 Collect Set 记录的可回收 Region 信息进行筛选,计算 Region 回收老本,依据用户设定的进展工夫值制订回收打算,依据回收打算筛选适合的 Region 区域进行回收。

回收算法:从部分来说 G1 是应用的标记复制法,把存活对象从一个 Region 复制到另外的 Region,但从整个堆来说 G1 的逻辑又相当于是标记整顿法,每次垃圾收集时会把存活的对象整顿到其余对应区域的 Region 里,再把原来的 Region 标记为可回收区域记录到 CSet 里,所以 G1 的每一次回收都是一次整顿过程,所以也就不会产生空间碎片问题。

G1 的特点

收集区域: 整个堆内存。

应用算法: 标记复制法

收集形式: 多线程。

搭配收集器: 无需其余收集器搭配。

劣势: 进展工夫可控,吞吐量高,可依据具体场景抉择吞吐量无限还是进展工夫无限,不须要额定的收集器搭配。

劣势: 因为须要保护的额定信息比拟多,所以须要的内存空间也要大,6G 以上的内存能力思考应用 G1 收集器。

⑥总结

从当初往回看,咱们会发现每个垃圾收集器都是一个时代的产物。

第一阶段:在单核 CPU,内存资源稀缺的时代应用的是 Serial 和 Serial Old 收集器,对于单核 CPU,内存只有几十 M 的场景 Serial 的效率是十分高的。

第二阶段:进入多核 CPU 时代后呈现了 Parallel Scavenge 和 Parallel Old 收集器,利用多线程并行收集极大的进步了垃圾收集的效率,所以在多核 CPU 场景,内存在几百 M 到几 G 的场景 Parallel Scavenge 和 Parallel Old 是实用的。

第三阶段:随着内存的变大,垃圾收集的过程工夫变得越来越长了,BS 零碎的倒退也逐步开始器重用户体验了,所以就呈现了 CMS 以缩小用户线程进展工夫为目标的收集器,CMS 通过并发收集缩小了用户线程的进展工夫,在多核 CPU,并且内存空间几 G 到几十 G 的空间、并且重视用户体验的 CMS 垃圾收集器是实用的。

第四阶段:CMS 遗留了一些比拟致命的问题,所以就有了 G1,G1 不再对内存进行物理上的分代,而只是进行逻辑上的分区,通过各种机制让垃圾收集变得更智能和可控了,多核 CPU,并且内存在 10G 到上百 G 的场景 G1 比拟适宜。

6、强援用、软利用、弱援用、虚援用的区别?

①强援用:
强援用是咱们应用最宽泛的援用,如果一个对象具备强援用,那么垃圾回收期相对不会回收它,当内存空间有余时,垃圾回收器宁愿抛出 OutOfMemoryError,也不会回收具备强援用的对象;咱们能够通过显示的将强援用对象置为 null,让 gc 认为该对象不存在援用,从而来回收它;

②软援用:
软利用是用来形容一些有用但不是必须的对象,在 java 中用 SoftReference 来示意,当一个对象只有软利用时,只有当内存不足时,才会回收它;
软援用能够和援用队列联结应用,如果软援用所援用的对象被垃圾回收器所回收了,虚构机会把这个软援用退出到与之对应的援用队列中;

③弱援用:
弱援用是用来形容一些可有可无的对象,在 java 中用 WeakReference 来示意,在垃圾回收时,一旦发现一个对象只具备软援用的时候,无论以后内存空间是否短缺,都会回收掉该对象;
弱援用能够和援用队列联结应用,如果弱援用所援用的对象被垃圾回收了,虚构机会将该对象的援用退出到与之关联的援用队列中;

④虚援用:
虚援用就是一种可有可无的援用,无奈用来示意对象的生命周期,任何时候都可能被回收,虚援用次要应用来跟踪对象被垃圾回收的流动,虚援用和软援用与弱援用的区别在于:虚援用必须和援用队列联结应用;在进行垃圾回收的时候,如果发现一个对象只有虚援用,那么就会将这个对象的援用退出到与之关联的援用队列中,程序能够通过发现一个援用队列中是否曾经退出了虚援用,来理解被援用的对象是否须要被进行垃圾回收。

五、性能调优

罕用 JVM 参数

-Xmn:调整新生代大小

-Xms:调整堆初始大小,默认内存的 1 /64

-Xmx:调整堆的最大可扩大大小,默认是 1 /4

-XX:+PrintGCDetails 输入具体的 GC 解决日志,查看堆的详细信息。

设置 JVM 参数

命令:java -Xms20m -Xmx50m xx.class

1、用过哪些调优的参数?用过 jmap 等条用工具么?

1)堆栈配置相干

-Xms 设置初始堆的大小

-Xmx 设置最大堆的大小

-Xmn 设置年老代大小,相当于同时配置 -XX:NewSize 和 -XX:MaxNewSize 为一样的值

-Xss 每个线程的堆栈大小

-XX:NewSize 设置年老代大小(for 1.3/1.4)

-XX:MaxNewSize 年老代最大值(for 1.3/1.4)

-XX:NewRatio 年老代与年轻代的比值(除去长久代)

-XX:SurvivorRatio Eden 区与 Survivor 区的的比值

-XX:PretenureSizeThreshold 当创立的对象超过指定大小时,间接把对象调配在老年代。

-XX:MaxTenuringThreshold 设定对象在 Survivor 复制的最大年龄阈值,超过阈值转移到老年代

2)垃圾收集器相干

-XX:+UseParallelGC:抉择垃圾收集器为并行收集器。

-XX:ParallelGCThreads=20:配置并行收集器的线程数

-XX:+UseConcMarkSweepGC:设置年轻代为并发收集。

-XX:CMSFullGCsBeforeCompaction=5 因为并发收集器不对内存空间进行压缩、整顿,所以运行一段时间当前会产生“碎片”,使得运行效率升高。此值设置运行 5 次 GC 当前对内存空间进行压缩、整顿。

-XX:+UseCMSCompactAtFullCollection:关上对年轻代的压缩。可能会影响性能,然而能够打消 碎片

3)辅助信息相干

-XX:+PrintGCDetails 打印 GC 详细信息

-XX:+HeapDumpOnOutOfMemoryError 让 JVM 在产生内存溢出的时候主动生成内存快照, 排查问题用

-XX:+DisableExplicitGC 禁止零碎 System.gc(),避免手动误触发 FGC 造成问题.

-XX:+PrintTLAB 查看 TLAB 空间的应用状况

2、调优工具

罕用调优工具分为两类,jdk 自带监控工具:jps、jstat、jmap、jconsole 和 jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

jconsole,Java Monitoring and Management Console 是从 java5 开始,在 JDK 中自带的 java 监控和治理控制台,用于对 JVM 中内存,线程和类等的监控 jvisualvm,jdk 自带全能工具,能够剖析内存快照、线程快照;监控内存变动、GC 变动等。MAT,Memory Analyzer Tool,一个基于 Eclipse 的内存剖析工具,是一个疾速、功能丰富的 Java heap 剖析工具,它能够帮忙咱们查找内存透露和缩小内存耗费

GChisto,一款业余剖析 gc 日志的工具

3、那些 JVM 性能调优

首先须要留神的是在对 JVM 内存调优的时候不能只看操作系统级别 Java 过程所占用的内存,这个数值不能精确的反应堆内存的实在占用状况,因为 GC 过后这个值是不会变动的,因而内存调优的时候要更多地应用 JDK 提供的内存查看工具,比方 JConsole 和 Java VisualVM。

对 JVM 内存的零碎级的调优次要的目标是缩小 GC 的频率和 Full GC 的次数,过多的 GC 和 Full GC 是会占用很多的系统资源(次要是 CPU),影响零碎的吞吐量。特地要关注 Full GC,因为它会对整个堆进行整顿,导致 Full GC 个别因为以下几种状况:

旧生代空间有余

 调优时尽量让对象在新生代 GC 时被回收、让对象在新生代多存活一段时间和不要创立过大的对象及数组防止间接在旧生代创建对象

Pemanet Generation 空间有余

  增大 Perm Gen 空间,防止太多动态对象

  统计失去的 GC 后降职到旧生代的均匀大小大于旧生代残余空间

  管制好新生代和旧生代的比例

System.gc()被显示调用

  垃圾回收不要手动触发,尽量依附 JVM 本身的机制

调优伎俩次要是通过管制堆内存的各个局部的比例和 GC 策略来实现,上面来看看各局部比例不良设置会导致什么结果

1). 新生代设置过小

  一是新生代 GC 次数十分频繁,增大零碎耗费;二是导致大对象间接进入旧生代,占据了旧生代残余空间,诱发 Full GC

2). 新生代设置过大

  一是新生代设置过大会导致旧生代过小(堆总量肯定),从而诱发 Full GC;二是新生代 GC 耗时大幅度减少

  一般说来新生代占整个堆 1 / 3 比拟适合

3). Survivor 设置过小

  导致对象从 eden 间接达到旧生代,升高了在新生代的存活工夫

4). Survivor 设置过大

  导致 eden 过小,减少了 GC 频率

  另外,通过 -XX:MaxTenuringThreshold= n 来管制新生代存活工夫,尽量让对象在新生代被回收

由内存治理和垃圾回收可知新生代和旧生代都有多种 GC 策略和组合搭配,抉择这些策略对于咱们这些开发人员是个难题,JVM 提供两种较为简单的 GC 策略的设置形式

1). 吞吐量优先

  JVM 以吞吐量为指标,自行抉择相应的 GC 策略及管制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由 -XX:GCTimeRatio= n 来设置

2). 暂停工夫优先

  JVM 以暂停工夫为指标,自行抉择相应的 GC 策略及管制新生代与旧生代的大小比例,尽量保障每次 GC 造成的利用进行工夫都在指定的数值范畴内实现。这个值可由 -XX:MaxGCPauseRatio= n 来设置

正文完
 0