关于java:JVM虚拟机知识问答总结简单复习快速回忆

3次阅读

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

写在最后面

这个我的项目是从 20 年末就立好的 flag,通过几年的学习,回过头再去看很多知识点又有新的了解。所以趁着找实习的筹备,联合以前的学习储备,创立一个次要针对应届生和初学者的 Java 开源常识我的项目,专一 Java 后端面试题 + 解析 + 重点常识详解 + 精选文章的开源我的项目,心愿它能随同你我始终提高!

阐明:此我的项目内容参考了诸多博主(已注明出处),材料,N 本书籍,以及联合本人了解,从新绘图,从新组织语言等等所制。集体之力菲薄,或有不足之处,在劫难逃,但更新 / 欠缺会始终进行。大家的每一个 Star 都是对我的激励!心愿大家能喜爱。

注:所有波及图片未应用网络图床,文章等均开源提供给大家。

我的项目名: Java-Ideal-Interview

Github 地址:Java-Ideal-Interview – Github

Gitee 地址:Java-Ideal-Interview – Gitee(码云)

继续更新中,在线浏览将会在前期提供,若认为 Gitee 或 Github 浏览不便,可克隆到本地配合 Typora 等编辑器舒服浏览

若 Github 克隆速度过慢,可抉择应用国内 Gitee 仓库

一 JVM 常识问答总结

1. JVM 根底

1.1 请你谈谈你对 JVM 的意识和了解

注:此局部在 /docs/java/javase-basis/001-Java 基础知识.md 曾经提到过。

JVM 又被称作 Java 虚拟机,用来运行 Java 字节码文件(.class),因为 JVM 对于特定零碎(Windows,Linux,macOS)有不同的具体实现,即它屏蔽了具体的操作系统和平台等信息,因而同一字节码文件能够在各种平台中任意运行,且失去同样的后果。

1.1.1 什么是字节码?

扩大名为 .class 的文件叫做字节码,是程序的一种低级示意,它不面向任何特定的处理器,只面向虚拟机(JVM),在通过虚拟机的解决后,能够使得程序能在多个平台上运行。

1.1.2 采纳字节码的益处是什么?

Java 语言通过字节码的形式,在肯定水平上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比拟高效,而且,因为字节码并不专对一种特定的机器,因而,Java 程序毋庸从新编译便可在多种不同的计算机上运行。

为什么肯定水平上解决了传统解释型语言执行效率低的问题(参考自思否 -scherman,仅供参考)

首先晓得两点,① 因为 Java 字节码是伪机器码,所以会比解析型语言效率高 ② JVM 不是解析型语言,是半编译半解析型语言

解析型语言没有编译过程,是间接解析源代码文本的,相当于在执行时进行了一次编译,而 Java 的字节码尽管无奈和本地机器码齐全一一对应,但能够简略映射到本地机器码,不须要做简单的语法分析之类的编译解决,当然比纯解析语言快。

1.1.3 你能谈一谈 Java 程序从代码到运行的一个过程吗?

过程:编写 -> 编译 -> 解释(这也是 Java 编译与解释共存的起因)

首先通过 IDE/ 编辑器编写源代码而后通过 JDK 中的编译器(javac)编译成 Java 字节码文件(.class 文件),字节码通过虚拟机执行,虚拟机将每一条要执行的字节码送给解释器,解释器会将其翻译成特定机器上的机器码(及其可执行的二进制机器码)。

1.2 你对类加载器有理解吗?

定义:类加载器会依据指定 class 文件的全限定名称,将其加载到 JVM 内存,转为 Class 对象。

1.2.1 类加载器的执行流程

1.2.1.1 加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个二进制字节流所代表的动态存储构造导入为办法区的运行时数据结构。
  3. 在 java 堆中生成一个 java.lang.Class 对象,来代表的这个类,作为办法区这些数据的入口。

1.2.1.2 链接

  1. 验证:保障二进制的字节流所蕴含的信息符号虚拟机的要求,并且不会危害到虚拟机本身的平安。
  2. 筹备:为 static 动态变量(类变量)分配内存,并为其设置初始值。

    • 注:这些内存都将在办法区内分配内存,实例变量在堆内存中,而且实例变量是在对象初始化时才赋值
  3. 解析:解析阶段就是虚拟机将常量池中的符号援用转化为间接援用的过程。

    • 例如 import xxx.xxx.xxx 属于符号援用,而通过指针或者对象地址援用就是间接援用

1.2.1.3 初始化

  1. 初始化会对变量进行赋值,即对最后的零值,进行显式初始化,例如 static int num = 0 变成了 static int num = 3,这些工作都会在类结构器 <clinit>() 办法中执行。而且虚拟机保障了会先去执行父类 <clinit>() 办法。

    • 如果在动态代码块中批改了动态变量的值,会对后面的显示初始化的值进行笼罩

1.2.1.4 卸载

GC 垃圾回收内存中的无用对象

1.2.2 类加载器有哪几种,加载程序是什么样的?

JVM 中自身提供的类加载器(ClassLoader)次要有三种,除了 BootstrapClassLoader 是 C++ 实现以外,其余的类加载器均为 Java 实现,而且都继承了 java.lang.ClassLoader

  1. BootStrapClassLoader(启动类加载器):C++ 实现,JDK 目录 /lib 上面的 jar 和类,以及被 -Xbootclasspath 参数指定的门路中的所有类,都归其负责加载。
  2. ExtensionClassLoader: 加载扩大的 jar 包:负责加载 JRE 目录 /lib 上面的 jar 和类,以及被 java.ext.dirs 零碎变量所指定的门路下的 jar 包。
  3. AppClassLoader:负责加载用户以后利用下 classpath 上面的 jar 包和类

注:程序为最底层向上

1.2.3 双亲委派机制有理解吗?

1.2.3.1 概念

双亲委派模型会要求除了顶层的启动类加载器外,其余的类加载器都应有本人的父类加载器,不过这里的父子关系个别不是通过继承来实现的,通常是应用组合关系来复用父加载器的代码

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的申请,他首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器去实现,每一个档次的类加载都是如此,因而所有的加载申请都最终应该传送到最顶层的启动类加载器中,只有当父加载器反馈本人无奈实现这个加载申请(也就是它的范畴搜寻中,也没有找到所须要的类),子加载器才会尝试本人去实现加载。

1.2.3.2 长处

  • 加载位于 rt.jar 包中的类(例如 java.lang.Object)时不论是哪个加载器加载,最终都会委托最顶端的启动类加载器 BootStrapClassLoader 进行加载,这样保障它在各个类加载器环境下都是同一个后果。
  • 防止了自定义代码影响 JDK 的代码,如果咱们本人也创立了一个 java.lang.Object 而后放在程序的 classpath 中,就会导致系统中呈现不同的 Object 类,Java 类型体系中最根底的行为也就无奈保障。
public class Object(){public static void main(){......}
}

1.2.3.3 如果不想应用双亲委派模型怎么办

自定义类加载器,而后重写 loadClass() 办法

1.3 讲一讲 Java 内存区域(运行时数据区)

1.3.1 总体概述

Java 程序在被虚拟机执行的时候,内存区域被划分为多个区域,而且尤其在 JDK 1.6 和 JDK 1.8 的版本下,有一些显著的变动,不过主题构造还是差不多的。

整体次要分为两个局部:

  • 线程共享局部:

    • 程序计数器
    • 虚拟机栈
    • 本地办法栈
  • 线程公有局部

    • 办法区(JDK 1.8 变为了元空间,元空间是位于间接内存中的)

注:咱们配图以 JDK 1.6 为例,至于产生的变动咱们在上面有阐明

1.3.2 程序计数器

概念:程序计数器是一块较小的内存空间,能够看作是以后线程所执行的字节码的行号指示器。

  • 作用 1(流程管制):字节码解释器工作时就是通过扭转这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异样解决、线程复原等性能都须要依赖这个计数器来实现。
  • 作用 2(线程复原):为了线程切换后能复原到正确的执行地位,每条线程都须要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程公有”的内存。
  • 线程切换的起因是:Java 虚拟机的多线程是通过线程轮流切换,调配处理器工夫片实现的,所以在任意时刻,一个处理器都(多核处理器来说是一个内核)只能执行一条指令。

1.3.2.1 为什么程序计数器是线程公有的?

答:次要为了线程切换复原后,能回到本人原先的地位。

1.3.3 Java 虚拟机栈

Java 虚拟机栈形容的是 Java 办法执行的内存模型,每次办法调用时,都会创立一个栈帧,每个栈帧中都领有:局部变量表、操作数栈、动静链接、办法进口信息。

大部分状况下,很多人会将 Java 内存抽象的划分为堆和栈(尽管这样划分有些毛糙,然而这也能阐明这两者是程序员们最关注的地位),这个栈,其实就是 Java 虚拟机栈,或者说是其中的局部变量表局部。

  • 局部变量表次要寄存了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象援用(reference 类型,它不同于对象自身,可能是一个指向对象起始地址的援用指针,也可能是指向一个代表对象的句柄或其余与此对象相干的地位)和 returnAddress 类型(指向一条字节码指令的地址)

1.3.3.1 Java 虚拟机栈会呈现哪两种谬误?

  • StackOverFlowError:如果 Java 虚拟机栈容量不能动静扩大,而此时线程申请栈的深度超过以后 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 谬误。
  • OutOfMemoryError:如果 Java 虚拟机栈容量能够动静扩大,当栈扩大的时候,无奈申请到足够的内存(Java 虚拟机堆中没有闲暇内存,垃圾回收器也没方法提供更多内存)

1.3.4 本地办法栈

和虚拟机栈所施展的作用十分类似,其区别是:虚拟机栈为虚拟机执行 Java 办法(也就是字节码)服务,而本地办法栈则为虚拟机应用到的 Native 办法服务。

  • 因为本地办法栈中的办法,应用形式,数据结构,Java 虚拟机标准,未做强制要求,具体的虚拟机能够自在的本人实现,例如:HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

与虚拟机栈雷同,在栈深度溢出,以及栈扩大失败的时候,也会呈现 StackOverFlowErrorOutOfMemoryError 两种谬误。

1.3.4.1 虚拟机栈和本地办法栈为什么是公有的?

答:次要为了保障线程中的局部变量不被别的线程拜访到

1.3.5 堆

Java 虚拟机所治理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创立。此内存区域的惟一目标就是寄存对象实例,简直所有的对象实例以及数组都在这里分配内存。

  • 然而,随着即时编译技术的提高,尤其是逃逸剖析技术日渐弱小,栈上调配、标量替换优化技术将导致了一些奥妙的变动,所以,所有的对象都在堆上调配也慢慢变得不那么“相对”了。

    • JDK 1.7 曾经默认开启逃逸剖析,如果某些办法中的对象援用没有被返回或者未被里面应用(即未逃逸进来),那么对象能够间接在栈上分配内存。

补充:Java 堆是垃圾收集器治理的次要区域,因而也被称作 GC 堆(Garbage Collected Heap)

1.3.6 办法区

办法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、动态变量、即时编译器编译后的代码等数据。尽管 Java 虚拟机标准把办法区形容为堆的一个逻辑局部,然而它却有一个别名叫做 Non-Heap(非堆),目标应该是与 Java 堆辨别开来。

留神:JDK1.7 开始,到 JDK 8 版本之后办法区(HotSpot 的永恒代)被彻底移除了,变成了元空间,元空间应用的是间接内存。

1.3.6.1 永恒代是什么

在 JD K1.8 之前,许多 Java 程序员都习惯在 hotspot 虚拟机上开发,部署程序,很多人更违心把办法去称说为永恒代,或者将两者一概而论,实质上这两者不是等价的,因为仅仅是过后 hotspot 虚拟机设计团队抉择把收集器的分代设计扩大至办法区,或者说应用永恒代来实现办法区而已,这样使得 hotspot 的垃圾收集器可能像治理 Java 堆一样治理这部分内存,省去专门为办法去编写内存治理代码的工作,然而对于其余虚拟机实现是不存在永恒代的概念的。

1.3.6.2 永恒代为什么被替换成了元空间?

  • 永恒代大小下限为固定的,无奈调整批改。而元空间应用间接内存,与本机的可用内存无关,大大减少了溢出的几率
  • JRockit 想要移植到 HotSpot 虚拟机的时候,因为两者对办法区的实现存在差别面临很多艰难,所以 JDK 1.6 的时候 HotSpot 开发团队就有了放弃永恒代,逐步扭转为本地内存的打算,到 JDK 1.7 曾经把本来放在永恒代的字符串常量池,动态变量等移出,而到了 JDK 1.8 齐全放弃了永恒代。

1.3.7 运行时常量池

运行时常量池是办法区的一部分。Class 文件中除了有类的版本、字段、办法、接口等形容信息外,还有常量池表(用于寄存编译期生成的各种字面量和符号援用,这部分内容将在类加载后寄存到办法区的运行时常量池中)

1.3.7.1 办法区的运行时常量池在 JDK 1.6 到 JDK 1.8 的版本中有什么变动?

  • 依据下面的 Java 内存区域图可知,JDK 1.6 办法区(HotSpot 的永恒代)中的运行时常量池中包含了字符串常量池,
  • JDK 1.7 版本下,字符串常量池从办法区中被移到了堆中(注:只有这一个挪动了)
  • JDK 1.8 版本下,HotSpot 的永恒代变为了元空间,字符串常量池还在堆中,运行时常量也还在办法区中,只不过办法区变成了元空间

1.3.7 间接内存

间接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机标准中定义的内存区域,然而这部分内存也被频繁地应用。而且也可能导致 OutOfMemoryError 谬误呈现。

1.4 Java 对象创立拜访到死亡

1.4.1 Java 对象的创立(JVM 方向)

1.4.1.1 类加载查看

  • 概念:JVM(此处指 HotSpot)遇到 new 指令时,先查看指令参数是否能在常量池中定位到一个类的符号援用。

    • A:如果能定位到,就查看这个符号援用代表的类是否已被加载、解析和初始化过。
    • B:如果不能定位到,或没有查看到,就先执行相应的类加载过程。

1.4.1.2 为对象分配内存

概念:加载检查和加载后,就是分配内存,对象所需内存的大小在类加载实现后便齐全确定(对象的大小 JVM 能够通过 Java 对象的类元数据获取)为对象分配内存相当于把一块确定大小的内存从 Java 堆里划分进去。

  • ① 调配形式

    • A:指针碰撞:两头有一个辨别边界的指针,两边别离是用过的内存区域,和没用过的内存区域,分配内存的时候,就向着闲暇的那边挪动指针。

      • 实用于:Java 堆是规整的状况下。
      • 利用:Serial 收集器、ParNew 收集器
    • B:闲暇列表:保护一个列表,其中记录哪些内存可用,调配时会找到一块足够大的内存来划分给对象实例,而后更新列表。

      • 实用于:堆内存不是很规整的状况下。
      • 利用:CMS 收集器

注:Java 堆是否规整,取决于 GC 收集器的算法是什么,如“标记 - 革除”就是不规整的,“标记 - 整顿(压缩)”、“复制算法”是规整的。这几种算法咱们前面都会别离解说。

  • ② 线程平安问题

    • 并发状况下,上述两种调配形式都不是线程平安的,JVM 虚拟机提供了两种解决方案
    • A:同步解决:CAS + 失败重试

      • CAS 的全称是 Compare-and-Swap,也就是比拟并替换。它蕴含了三个参数:V:内存值、A:以后值(旧值)、B:要批改成的新值

        CAS 在执行时,只有 V 和 A 的值相等的状况下,才会将 V 的值设置为 B,如果 V 和 A 不同,这阐明可能其余线程曾经做了更新操作,那么以后线程值就什么也不做,最初 CAS 返回的是 V 的值。

        在多线程的的状况下,多个线程应用 CAS 操作同一个变量的时候,只有一个会胜利,其余失败的线程,就会持续重试。

        正是这种机制,使得 CAS 在没有锁的状况下,也能实现平安,同时这种机制在很多状况下,也会显得比拟高效。

    • B:本地线程调配缓冲区:TLAB

      • 为每一个线程在 Java 堆的 Eden 区调配一小块内存,哪个线程须要分配内存,就从哪个线程的 TLAB 上调配,只有 TLAB 的内存不够用,或者用完的状况下,再采纳 CAS 机制

1.4.1.3 对象初始化零值

内存调配完结后,执行初始化零值操作,即保障对象不显式初始化零值的状况下,程序也能拜访到零值

1.4.1.4 设置对象头

初始化零值后,显式赋值前,须要先对对象头进行一些必要的设置,即设置对象头信息,类元数据的援用,对象的哈希码,对象的 GC 分代年龄等。

1.4.1.5 执行对象 init 办法

此处用来对对象进行显式初始化,即依据程序者的志愿进行初始化,会笼罩掉后面的零值

1.4.2 对象的拜访定位形式哪两种形式?

首先举个例子:Student student = new Student();

假如咱们创立了这样一个学生类,Student student 就代表作为一个本地援用,被存储在了 JVM 虚拟机栈的局部变量表中,此处代表一个 reference 类型的数据,而 new Student 作为实例数据存储在了堆中。还保留了对象类型数据(类信息,常量,动态变量)

而咱们在应用对象的时候,就是通过栈上的这个 reference 类型的数据来操作对象,它有两种形式拜访这个具体对象

  1. 句柄:在堆中划分出一块内存作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中存储着对象实例数据和数据类型的地址。这种形式比较稳定,因为对象挪动的时候,只扭转句柄中的实例数据的指针,reference 是不须要批改的。
  2. 间接指针:即 reference 中存储的就是对象的地址。这种形式的劣势就是疾速,因为少了一次指针定位的开销

句柄形式配图:

间接指针形式配图:

1.4.3 如何判断对象死亡

堆中简直放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象曾经死亡(即不能再被任何路径应用的对象)。

  • 援用计数法:给对象中增加一个援用计数器,每当有一个中央援用它,计数器就加 1;当援用生效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被应用的。

    • 援用计数法原理简略,断定效率也很高,在很多场景下是一个不错的算法,然而在支流的 Java 畛域中,因为其须要配合大量额定解决能力保障正确地工作。

      • 例如它很难解决两个对象之间循环援用的问题:对象 objA 和 objB 均含有 instance 字段,赋值令 objA.instance = objB,objB.instance = objA,除此之外,这两个对象曾经再无援用,实际上这两个曾经不可能被再拜访了,因为单方相互因哟红着对方,它们的援用计数不为零,援用技术算法也就无奈回收它们。
  • 可达性剖析算法:这个算法的根本思维就是通过一系列的称为“GC Roots”的对象作为终点,从这些节点开始向下搜寻,节点所走过的门路称为援用链,当一个对象到 GC Roots 没有任何援用链相连的话,则证实此对象是不可能再被应用的

1.4.3.1 四种援用类型的水平

无论是援用计数算法,还是可达性剖析算法,断定对象的存活都与援用无关,然而 JDK 1.2 之间的版本,将对象的援用状态分为“被援用”和“未被援用”实际上有些狭窄,形容一些食之无味,弃之可惜的对象就有一些无能为力,所以 1.2 之后边进行了更粗疏的划分。

JDK1.2 之前,援用的概念就是,援用类型存储的是一块内存的起始地址,代表这是这块内存的一个援用。

JDK1.2 当前,细分为强援用、软援用、弱援用、虚援用四种(逐步变弱)

  • 强援用:垃圾回收器不会回收它,当内存不足的时候,JVM 宁愿抛出 OutOfMemoryError 谬误,也不违心回收它。
  • 软援用:只有在内存空间有余的状况下,才会思考回收软援用。
  • 弱援用:弱援用比软援用申明周期更短,在垃圾回收器线程扫描它管辖的内存区域的过程中,只有发现了弱援用对象,就会回收它,然而因为垃圾回收器线程的优先级很低,所以,个别也不会很快发现并回收。
  • 虚援用:级别最低的援用类型,它任何时候都可能被垃圾回收器回收

1.5 讲讲几种垃圾收集算法

1.5.1 标记革除算法

标记革除算法首先标记出所有不须要回收的对象,在标记实现后对立回收掉所有没有被标记的对象,也能够反过来。

它的次要毛病有两个:

  • 第 1 个是执行效率不稳固,如果 Java 最终含有大量对象,而且其中大部分都是须要回收的,这是须要进行大量标记和革除动作,导致标记和革除两个过程的执行效率随着对象数量增长而升高。
  • 第 2 个是内存空间的碎片化问题,标记革除后会产生大量不存间断的内存碎片空间,碎片太多会导致当前程序运行时须要调配较大对象时无奈找到足够的间断内存,而不得不提前触发一次垃圾收集工作。

它属于根底算法,后续的大部分算法,都是在其根底上改良的。

1.5.2 标记复制算法

标记复制算法将可用内存按容量划分为大小相等的两块,每次只应用其中的一块,当这一块的内存用完了就将还存活着的对象复制到另一块下面,而后再把曾经应用过的内存空间再次清理掉。

毛病:如果内存中少数对象都是存活的,这种算法将会产生大量的内存间复制的开销。**

长处:

  • 然而对于少数对象都是可回收的状况,算法须要复制的就是占有多数的存活对象
  • 每次都是针对整个半区进行内存回收,分配内存时,也不必思考有空间碎片的简单状况,只有挪动堆顶指针按程序调配即可

1.5.3 标记整顿算法(标记压缩算法)

标记复制算法在对象存活率较高的时候就要进行较多的复制,操作效率将会升高,更要害的是如果不想节约 50% 的空间,就须要有额定的空间进行调配担保以应答呗,应用内存所有对象都百分百存活的极其状况,所以在老年代个别是不采纳这种算法的。

标记整顿算法与标记革除算法统一,但后续步骤不是间接对可回收对象进行清理,而是让所有存货的对象都向内存空间一端挪动,而后间接清理掉边界以外的内存

但挪动存活对象也是有毛病的:尤其是在老年代这种每次回收都有大量对象存活的区域,挪动存活对象并更新所有援用这些对象的中央,将会是一种极为负重的操作,而且这种对象挪动操作必须全程暂停用户利用过程能力进行,这种进展被称为 stop the world。

1.6 什么是分代收集算法

分代收集实践,首先它建设在两个假说之上:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的
  2. 强分带假说:熬过越屡次垃圾收集过程的对象就越难以沦亡

所以多款罕用垃圾收集器的统一设计准则即为:收集器应该将 Java 堆划分出不同的区域,而后将回收对象根据其年龄(即熬过垃圾收集过程次数)调配到不同的区域之中存储。

很显著的,如果一个区域中大部分的对象都是朝生夕灭,难以熬过垃圾收集过程,那么把它们集中放在一起,每次回收就只须要思考如何保留大量存活的对象,而不是去标记那些大量要被回收的对象,这样就能以一种比拟低的代价回收大量空间,如果剩下的都是难以沦亡的对象,就把它们集中到一块,虚拟机便能够应用较低的频率来回收这个区域。

所以,分代收集算法的思维就是依据对象存活周期的不同,将内存分为几块,例如分为新生代(Eden 空间、From Survivor 0、To Survivor 1)和老年代,而后再各个年代抉择适合的垃圾收集算法

  • 新生代 (Young) 与老年代 (Old) 的比例的值为 1:2
  • Edem : From Survivor 0 : To Survivor 1 = 8 : 1 : 1

新生代中每次都会有大量对象死去,所以抉择革除复制算法,要比标记革除更高效,只须要复制挪动大量存活下来的对象即可。

老年代中对象存活的几率比拟高,所以要抉择标记革除或者标记整顿算法。

1.6.1 为什么新生代要分为 Eden 区和 Survivor 区?

注:此处参考援用博文:为什么新生代内存须要有两个 Survivor 区 注明出处,请尊重原创

补充:

  • Minor GC / Young GC:新生代收集
  • Major GC / Old GC:老年代收集
  • Full GC 整堆收集

如果没有 Survivor,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发 Major GC(因为 Major GC 个别随同着 Minor GC,也能够看做触发了 Full GC)。老年代的内存空间远大于新生代,进行一次 Full GC 耗费的工夫比 Minor GC 长得多。你兴许会问,执行工夫长有什么害处?频发的 Full GC 耗费的工夫是十分可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连贯会因为超时产生连贯谬误了。

  • Survivor 的存在意义,就是缩小被送到老年代的对象,进而缩小 Full GC 的产生,Survivor 的预筛选保障,只有经验 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

1.6.2 为什么要设置两个 Survivor 区?(有争议,待批改)

援用博文的作者观点:设置两个 Survivor 区最大的益处就是解决了碎片化,刚刚新建的对象在 Eden 中,经验一次 Minor GC,Eden 中的存活对象就会被挪动到第一块 survivor space S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0 中的存活对象又会被复制送入第二块 survivor space S1(这个过程十分重要,因为这种复制算法保障了 S1 中来自 S0 和 Eden 两局部的存活对象占用间断的内存空间,防止了碎片化的产生)。S0 和 Eden 被清空,而后下一轮 S0 与 S1 替换角色,如此周而复始。如果对象的复制次数达到 16 次,该对象就会被送到老年代中。

个人观点,更实质是思考了效率问题,如果是因为产生了碎片的问题,我齐全能够应用标记整顿办法解决,我更偏向于了解为整顿空间带来的性能耗费是远大于应用两块 survivor 区进行复制挪动的耗费的。

注:如果这一块不分明,能够参考一下援用文章的图片。

#### 1.6.3 哪些对象会间接进入老年代

  1. 大对象间接进入老年代

    • 在调配空间时它容易导致内存,明明还有不少空间时就提前触发垃圾收集,以获取足够的间断空间能力好安置他们,而当复制对象时大对象就意味着高额的内存复制开销,这样做的目标就是防止在 Eden 区 以及两个 survivor 区之间来回复制产生大量的内存复制操作
  2. 长期存活的对象进入老年代

    • HotSpot 虚拟机采纳了分代收集的思维来治理内存,那么内存回收时就必须能辨认哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器,存储在对象头中。

      如果对象在 Eden 出世并通过第一次 Minor GC 后依然可能存活,并且能被 Survivor 包容的话,将被挪动到 Survivor 空间中,并将对象年龄设为 1. 对象在 Survivor 中每熬过一次 MinorGC, 年龄就减少 1 岁,当它的年龄减少到肯定水平(默认为 15 岁),就会被降职到老年代中。对象降职到老年代的年龄阈值,能够通过参数 -XX:MaxTenuringThreshold 来设置。

1.6.3 动静对象年龄断定

为了能更好的适应不同程序的内存情况,HotSpot 虚拟机并不是永远要求对象年龄必须达到 -XX:MaxTenuringThreshold,能力降职老年代,如果在 Survivor 空间中雷同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就能够间接进入老年代。

1.7 介绍一下常见的垃圾回收器

1.7.1 Serial 收集器

Serial 收集器是最根本、历史最悠久的垃圾收集器了。在 JDK 1.3.1 之前是 HotSpot 虚拟机新生代收集器的惟一抉择,大家看名字就晓得这个收集器是一个单线程收集器了。它的“单线程”的意义不仅仅意味着它只会应用一条垃圾收集线程去实现垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其余所有的工作线程(“Stop The World”),直到它收集完结。

  • 新生代采纳复制算法,老年代采纳标记 - 整顿算法。

对于 “Stop The World” 带给用户的顽劣体验晚期 HotSpot 虚拟机的设计者们示意齐全了解,但也示意冤屈:你妈妈在给你清扫房间的时候,必定会让你老老实实的在椅子上或者房间外期待,如果她一边清扫你一边乱扔纸屑,这房间还能清扫完吗?这其实是一个荒诞不经的矛盾,尽管垃圾收集这项工作听起来和清扫房间属于一个工种,但实际上必定要比清扫房间简单很多。

尽管从当初看来,这个收集器曾经老而无用,弃之可惜,然而它依然是 HotSpot 虚拟机在客户端模式下默认的新生代收集器,因为其有着优良的中央,就是简略而又高效,内存耗费也是最小的。

1.7.2 ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了应用多线程进行垃圾收集外,其余行为(控制参数、收集算法、Stop The World、对象调配规定、回收策略等)和 Serial 收集器齐全一样。

它除了反对多线程并行收集之外,与 Serial 收集器相比没有太多的翻新之处,但却是不少运行在 Server 服务端模式下的 HotSpot 虚拟机的抉择。

  • 新生代采纳复制算法,老年代采纳标记 - 整顿算法。

1.7.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器也是基于 标记 - 复制算法 的多线程收集器,看起来和 ParNew 收集器很类似。

Parallel Scavenge 的指标是达到一个可管制的吞吐量(处理器用于运行这个程序的工夫和处理器总耗费的工夫之比),即高效利用 CPU,同时它也提供了很多参数供用户找到最合适的进展工夫或最大吞吐量。

1.7.4 Serial Old 收集器

Serial 收集器的老年代版本,它同样是一个单线程收集器。其次要意义还是提供客户端模式下的 HotSpot 虚拟机应用。

  • 如果切实服务端的模式下,也可能有两种用处:

    • 一种用处是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配应用,
    • 一种用处是作为 CMS 收集器的后备计划。

1.7.5 Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。也是一个基于“标记 - 整顿”算法的多线程收集器。在重视吞吐量以及 CPU 资源的场合,都能够优先思考 Parallel Scavenge 收集器和 Parallel Old 收集器。

1.7.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以取得最短回收进展工夫为指标的收集器,能给用户带来比拟好的交互体验。基于标记革除算法。

  • 初始标记: 初始标记仅仅是标记一下 GC Roots 能 间接关联到的对象,速度很快
  • 并发标记:并发标记就是从 GC Roots 的间接关联对象,开始遍历整个对象图的过程,这个过程耗时较长,但不须要进展,用户线程能够与垃圾收集线程一起并发执行
  • 从新标记: 从新标记阶段就是为了修改并发标记期间,因为用户程序持续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的进展工夫个别会比初始标记阶段的工夫稍长,远远比并发标记阶段工夫短。
  • 并发革除: 最初是并发革除阶段,清理删除掉标记阶段判断的曾经死亡的对象,因为不须要挪动存活对象,所以这个阶段也是能够与用户线程并发的。

1.7.7 G1 收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,次要针对装备多颗处理器及大容量内存的机器. 以极高概率满足 GC 进展工夫要求的同时,还具备高吞吐量性能特色。同时不会产生碎片。

  • 初始标记:仅仅是标记一下 GC Roots 能间接关联到的对象,并且批改 TAMS 指针的值,让下一阶段用户线程并发运行时能正确的在可用的 Region 中调配新对象,这个阶段须要进展线程,但耗时很短,而且是借用进行 Minor GC 的时候同步实现的,所以 G1 收集器在这个阶段其实没有额定的进展。
  • 并发标记:从 GC Root 开始,对堆中对象进行可达性剖析,递归扫描整个堆里的对象图找出要回收对象,这阶段耗时较长,但能够与用户程序并发执行,当对象扫描实现后,还要重新处理 SATB 记录下的,在并发时有援用变动的对象。
  • 最终标记:对用户现场做另一个短暂的暂停,用于解决并发阶段完结后仍遗留下来最初那大量的 SATB 记录。
  • 筛选回收:负责更新 Region 的统计数据,对各个 Region 的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,能够自由选择任意多个 Region 形成回收集,而后决定回收的那一部分 Region 的存货对象复制到空的 Region 中,在清理到整个就 Region 的全副空间,这外面波及操作存活对象的挪动是必须暂停用户线程,由多条收集线程并行实现的

长处和特点:

  • G1 能在充分利用 CPU 的状况下,缩短 Stop-The-World 的工夫,GC 时为并发状态,不会暂停 Java 程序运行。
  • 保留了分代概念,然而它其实能够独立治理整个 GC 堆。
  • G1 从整顿上看是基于标记整顿算法实现的,从部分上看是基于标记复制算法的。
  • G1 除了谋求进展以外,还建设了能够预测的进展工夫模型,能让使用者明确指定在一个长度为 M 毫秒的工夫片段内。
正文完
 0