因为热爱所以坚持,因为热爱所以等待。熬过漫长无戏可演的日子,终于换来了人生的春天。他逐渐被人熟知,被人喜爱

三年前,在苏州园区某个国企面试,第一道题目便是:JVM是什么吗?然后就是拿着笔试题目被面试官吊打,面对面试官的灵魂拷问,你坚持到了第几问?希望从今以后,这会是你的一个亮点技术

JVM_虚拟机目录

文章目录

①. JDK中包含了哪些内容?

  • ①. JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。其中的开发工具:编译工具(javac.exe) 打包工具(jar.exe)等

  • ②. JRE(Java Runtime Environment Java运行环境):包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可

②. 三大商业虚拟机

  • ①. sun公司的HotSpot VM(后来被Oracle收购)

  • ②. BEA JRocket VM(后来被Oracle收购),JRocket虚拟机中没有解释器,全部代码都靠即时编译器编译后执行

  • ③. iBM J9

③. 谈谈你对JVM整体的理解

  • ①. 类加载器子系统

  • ②. 运行时数据区[我们核心关注这里 的栈、堆、方法区]

  • ③. 执行引擎(解释器和JIT编译器共存)
    在这里插入图片描述

④. 简述Java类加载机制(Java类加载过程)

  • 加载机制是指类的加载、链接、初始化的过程
    在这里插入图片描述

⑤. 什么是类的加载、链接、初始化

  • ①. 加载: 将字节码文件中的.class文件,通过类加载器,加载进运行时数据区的方法区内,并创建一个大的Class对象

  • ②. 链接:(验证、准备、解析)

  1. 验证(比如说验证字节码文件开头是CAFFBABA,版本号等)
  2. 准备(为类变量赋予默认的初始化值,使用static+final修饰,且显示赋值不涉及到方法或者构造器调用的基本数据类型或者String类型的显示赋值都在准备阶段)
  3. 解析:将类中的符号引用变成直接引用(符号引号在字节码文件的常量池中)
  • ③. 初始化:为类变量赋予正确的初始化值,执行Clinit方法(静态代码块或使用static修饰的变量)
    注意:一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时

⑥. 类的主动使用和被动使用

  • ①. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化

  • ②. 访问某个类或接口的静态变量,或者对该静态变量赋值

  • ③. 调用类的静态方法

  • ④. 反射(比如:Class.forName(“com.xiaozhi.Test”))

  • ⑤. 初始化一个子类(当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)

  • ⑥. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

  • ⑦. JDK7开始提供的动态语言支持 (涉及解析REF_getStatic、REF_putStatic、REF_invokeSt atic方法句柄对应的类)

⑦. forName(“Java.lang.String”)和loadClass(“Java.lang.String”)有什么区别?

  • ①. forName()会导致类的主动加载,而getClassLoader()不会导致类的主动加载,Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个Class对象。该方法在将Class文件加载到内存的同时,会执行类的初始化

  • ②. ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。(该方法因为需要得到一个ClassLoader 对象,所以可以根据需要指定使用哪个类加载器)

⑧. 判定一个常量是否"废弃”还是相对简单,而要判定一个类型是否属于"不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件

  • ①. 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例

  • ②. 加载该类的类加载器已经被回收。这个条件除非是精心设计的可替换类加载器的场景。如JSP重加载等。否则通常是很难达成的

  • ③. 该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

⑨. 说说类的加载器分类?

  • ①. JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

  • ②. 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范并没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
    在这里插入图片描述

⑩. 说说你对引导类加载器、扩展类加载器和应用程序类加载器的理解

  • ①. 引导类加载器
  1. 这个类加载使用C/C++语言实现的,嵌套在JVM内部
  2. 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容),用于提供JVM自身需要的类(String类就是使用的这个类加载器)
  3. 由于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
  4. 不继承自java.lang.ClassLoader,没有父加载器
  • ②. 扩展类加载器 Extension
  1. Java语言编写,由sum.music.Launcher$ExtClassLoader实现
  2. 派生于ClassLoader类,父类加载器为启动类加载器
  3. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
  • ③. 系统类加载器(Application)
  1. Java语言编写,由sum.music.Launcher$AppClassLoader实现
  2. 派生于ClassLoader类,父类加载器为扩展类加载器
  3. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  4. 调用System.getSystemClassLoader() | Thread.currentThread().getContextClassLoader()获取到的是系统类加载器

⑩①. 谈谈你对双亲委派机制的理解

  • ①. 如果一个类加载收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行

  • ②. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器

  • ③. 如果父类的加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

⑩②. 双亲委派机制的优势

  • ①. 避免类的重复加载,确保一个类的全局唯一性(当父ClassLoader已经加载了该类的时候,就没有必要子ClassLoader再加载一次)
    比如:我们如果是引导类加载器加载了,就没必要再一次使用扩展类加载器进行加载

  • ②. 保护程序安全,防止核心API被随意篡改
    在这里插入图片描述

⑩③. 双亲委托模式的弊端

  • 检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类)

⑩④. 双亲委派机制可以打破吗?为什么

  • ①. 双亲委派模型的第一次"被破坏"其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的"远古"时代

  • ②. 第二次破坏双亲委派机制:线程上下文类加载器(ClassLoader.getSystemClassLoader( ))

  • ③. 双亲委派模型的第三次"被破坏"是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)

⑩⑤. 沙箱安全机制谈谈你的认识

  • ①. 自定义String类,但是在加载String类的时候会使用引导类加载器进行加载,而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中的java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制在一定程度上可以保护程序安全,保护原生的JDK代码

⑩⑥. 谈谈你对ClassLaoder中的loadClass的理解

  • ①. 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。

  • ②. 判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name, false)接口进行加载

  • ③. 反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassOrNull(name)接口,让引导类加载器进行加载

  • ④. 如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lan g.ClassLoader接口的defineClass系列的native接口加载目标Java类

⑩⑦. Java中基本数据类型存储在JVM中的存储位置

  • 局部变量存储在栈中,实例变量和静态变量存储在堆中

⑩⑧. 字节码文件中都有什么?

  • ①. 魔术、版本(副版本、主版本)、常量池(字节码文件的基石)、访问标识、类索引|父类索引|接口索引集合、字段表集合、方法表集合、属性表集合

⑩⑨. 常量池中都有什么?

  • ①. 字面量和符号引号

  • ②. 字面量:Java中的常量和字符串

  • ③. 符号引号:
    类和接口的全限定名
    属性的名称和修饰符
    方法的名称和修饰符

②0.JVM的永久代中会发生垃圾回收么?

  • ①. 方法区中的垃圾回收主要是:废弃的常量和不再使用的类型

  • ②. HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收

  • ③. 判定一个常量是否"废弃”还是相对简单,而要判定一个类型是否属于"不再被使用的类”的条件就比较苛刻
    了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收。也就是Java堆中不存在类以及任何派生子类的实例
  2. 加载该类的加载器已经被回收。这个条件除非是经过设计的可替换类加载器的场景,入OSGI、JSP的重加载等,否则通常是很难达成的
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

②①. JVM在加载类的时候,是否按照类的加载、链接(验证、准备、解析)和初始化的顺序执行的?

  • 不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot VM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行

②②."父加载器"和"子加载器"之间的关系是继承的吗?

  • 不是继承的关系,是包含的关系

②③. 如果我们自定义加载器,没有使用双亲委派机制,那么Java中的核心类库是不是还是会进行加载?

  • 会由引导类加载器进行加载,这是因为不管是自定义加载器还是系统类加载器或者扩展类加载器,最终都必须调用java.lang.ClassLoader.defineClass(String,byte[],int,int,ProtectionDomain)方法,而该方法会执行preDefineC lass()接口,该接口中提供给了对JDK核心类库的保护

②④. 为什么JDBC需要打破双亲委派机制?(tomcat也打破了)

  • JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包,DriverManager类中要加载各个实现了Driver接口的类,然后进行管理,也就是说BootStrap类加载器还要去加载jar包中的Driver接口的实现类,这就打破了双亲委派机制

②⑤. 既然Tomcat不遵循双亲委派机制,那么如果我自己定义一个恶意的HashMap,会不会有风险呢

  • 显然不会有风险,如果有,Tomcat都运行这么多年了,那能不改进吗?tomcat不遵循双亲委派机制,只是自定义的classLoader顺序不同,但顶层还是相同的,还是要去顶层请求classloader

②⑥. 我们思考一下:Tomcat是个web容器,那么它要解决什么问题?

  • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离

  • ①. 如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份

  • ②. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
    默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

  • ③. web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来
    第三个问题和第一个问题一样

  • ④. web容器要支持jsp的修改,我们知道,jsp文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用?所以,web容器需要支持jsp修改后不用重启
    我们想我们要怎么实现jsp文件的热替换,jsp文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件

②⑦. 如果tomcat的CommonClassLoader想加载WebAppClassLoader中的类,该怎么办?

  • 看了前面的关于破坏双亲委派模型的内容,我们心里有数了,我们可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作

②⑧. 为什么java文件放在Eclipse/IDEA中的src文件夹下会优先jar包中的class

  • Tomcat类加载机制的理解,就不难明白。因为Eclipse/IDEA中的src文件夹中的文件java以及webContent中的JSP都会在tomcat启动时,被编译成class文件放在WEB-INF/class中。
    而Eclipse/IDEA外部引用的jar包,则相当于放在WEB-INF/lib中。
    因此肯定是java文件或者JSP文件编译出的class优先加载。

②⑨. 谈谈你对程序计数器的理解

  • ①. 作用:是用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令

  • ②. 是线程私有的 、不会存在内存溢出(唯一一个运行时数据区没有OOM的区域)

  • ③. 如果执行的是一个Native方法,那这个计数器是undefined

在这里插入图片描述

③0. 为什么使用PC寄存器记录当前线程的执行地址呢?

  • ①. 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行

  • ②. JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
    在这里插入图片描述

③①. PC寄存器为什么设定为线程私有?

  • 为了能够准确记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器

③①. 说说你对本地方法栈的理解

  • ①.本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须由调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies

  • ②. native方法的举例:Object类中的clone wait notify hashCode等Unsafe类都是native方法
    在这里插入图片描述

③①. 什么虚拟机栈?

  • ①.虚拟机栈(Java Virtual Machine Stacks)线程是紧密联系的,每创建一个线程时就会对应创建一个Java栈, 所以Java栈也是"线程私有"的内存区域,这个栈中又会对应包含多个栈帧,每调用一个方法时就会往栈中创建并压入一个栈帧,栈帧是用来存储方法数据和部分过程结果的数据结构,每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程 [先进后出]
    在这里插入图片描述

③②. 栈里面存在GC吗?

  • 栈中是不存在GC的,存在OOM和StackOverflowError

③③. 静态变量和局部变量的对比?

  • ①. 我们知道类变量表有两次初始化的机会,第一次是在"准备阶段",执行系统初始化,对类变量设置为零值,另一次则是在"初始化"阶段,赋予程序员在代码中定义的初始值

  • ②. 和类变量初始化不同的是,局部变量表不存在初始化的过程,这意味着一旦定义了局部变量则必须认为初始化

③④. 虚拟机栈中都有什么?

  • 局部变量表、操作数栈、动态链接、方法的返回地址、附件信息

③⑤. 谈谈你对局部变量表的理解?

  • ①. 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各种基本数据类型、对象引用(reference)以及return Address类型)

  • ②. 由于局部变量是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题

  • ③. 局部变量表所需容量大小是在编译期确定下来的。(并保存在方法Code属性的maximum local variables数据项中,在方法运行期间不会改变局部变量表的大小的)

  • ④. 局部变量表,是基本的存储单元是slot(变量槽)

  • ⑤. 在局部变量表中,32位以内的类型只占有一个slot(包括引用数据类型),64位的类型(long和double)占有两个slot、byte、short、char在存储前被转换为int,boolean也被转换为int(0表示fasle,非0表示true)long和double则占据两个slot

  • ⑥. Jvm会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值

  • ⑦. 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可(比如:访问long或double类型变量)

  • ⑧. 如果当前帧是由构造方法或者实例方法创建,那么该对象引用this将会放在index为0的slot处

  • ⑨. 栈帧中的局部变量表中的槽位是可以复用的,如果一个局部变量过了其作用域,那么在其作用域之后申请的新的局部变量就很可能会复用过期局部变量的槽位,从而节省资源的目的

  • ⑩. 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

③⑥. 谈谈你对操作数栈的理解?

  • ①. 我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

  • ②. 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈

  • ③. 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值

  • ④. 栈中的任何一个元素都是可以任意的Java数据类型
    32bit的类型占用一个栈单位深度
    64bit的类型占用两个栈单位深度

  • ⑤. 操作数栈,主要用于保存计算机过程的中间结果,同时作为计算过程中变量临时的存储空间

③⑦. 说说你对动态链接的理解?

  • 动态链接就是将字节码文件中的符号引号(字符串常量池中)变成直接引用的过程

③⑧. 方法返回地址?

  • ①. 存放调用该方法的PC寄存器的值

  • ②. 一个方法的结束,有两种方式:
    正常执行完成
    出现未处理的异常,非正常退出

  • ③. 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
    而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

③⑨. 附加信息(了解)

  • 栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。

④0. 调整栈大小,就能保证不出现溢出吗?

  • 不能。因为调整栈大小,只会减少出现溢出的可能,栈大小不是可以无限扩大的,所以不能保证不出现溢出

④①. 分配的栈内存越大越好吗?

  • 不是,因为增加栈大小,会造成每个线程的栈都变的很大,使得一定的栈空间下,能创建的线程数量会变小

④②. 垃圾回收是否会涉及到虚拟机栈?

  • ①. 不会;垃圾回收只会涉及到方法区和堆中,方法区和堆也会存在溢出的可能

  • ②. 程序计数器,只记录运行下一行的地址,不存在溢出和垃圾回收

  • ③. 虚拟机栈和本地方法栈,都是只涉及压栈和出栈,可能存在栈溢出,不存在垃圾回收

④③. 方法中定义的局部变量是否线程安全?

  • 如果局部变量在内部产生并在内部消亡的,那就是线程安全的

④④. 什么情况下会发生栈内存溢出?

  • ①. 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出

  • ②. 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出

④⑤. 说说堆和栈的区别?

  • ①.从GC、OOM、StackOverflowError的角度
    [栈中不存在GC,当固定大小的栈会发生StackOverflowError,动态的会发生OOM。堆中GC、OOM、StackOverflowError都存在]

  • ②. 从堆栈的执行效率[栈的效率高于堆]

  • ③. 内存大小,数据结构
    [堆的空间比栈的大一般,栈是一种FIFO先进后出的模型。堆中结构复杂,可以有链表、数组等]

  • ④. 栈管运行,堆管存储

④⑥. 简单谈谈你对堆的理解?(共享|垃圾回收)

  • ①. Java堆区在JVM启动的时候即被创建,其空间大小也是确定的。是Jvm管理最大的一块内存空间

  • ②. 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)

  • ③. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才有被移除
    (注意:一个进程就是一个JVM实例,一个进程中包含多个线程)

  • ④. 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)

④⑦. 堆里面的分区:Eden,survival(from+to),老年代,各自的特点

  • ①. 在JDK1.7中分为: 新生代+老年代+永久代 | 在JDK1.8中分为: 新生代+老年代+元空间

  • ②. 新生代:伊甸园区、幸存者S0、S1(8:1:1),几乎所有的Java对象都是在Eden区被new出来的,
    绝大部分的Java对象的销毁都在新生代进行了;IBM公司的专门研究表明,新生代中 80% 的对象都是"朝生夕死"的
    老年代:另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
    新生代:老年代=1:2

④⑧. 堆大小通过什么参数设置?

  • ①. -Xms:初始内存(默认为物理内存的1/64)

  • ②. -Xmx:最大内存(默认为物理内存的1/4)

  • ③. -XX:NewRatio=2
    -XX:SurvivorRatio
    -XX:HandlePromotionFailure:空间分配担保
    -Xmn:设置新生代最大内存大小,一般使用默认值就可以了

④⑨. 初始堆大小和最大堆大小一样,问这样有什么好处?

  • 通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提升性能

⑤0. 对象在堆中的分配规则?

  • ①. 优先分配到Eden

  • ②. 大对象直接分配到老年(尽量避免程序中出现过多的大对象)

  • ③. 长期存活的对象分配到老年代

  • ④. 动态对象年龄判断(如果Survivor 区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄对象可以直接进入老年代,无须等到MaxTenurningThreshold中要求的年龄

  • ⑤. 空间分配担保 -XX:HandlePromotionFailure
    (JDK6 Update24之后规则变为只有老年代的连续空间大于新生代对象总大小或者历次晋身的平均大小就会进行Minor GC,否则将进行Full GC)

⑤①. Minor GC | Major GC | Full GC

  • ①. Minor GC 在Eden伊甸园区满的时候会触发,发生在新生代中

  • ②. Major GC 在老年代中满了会进行触发,发生在老年代,major gc的时间比minor gc时间长

  • ③. Full GC 发生在整个堆中

⑤②. 什么时候会触发minor gc?

  • 在Eden伊甸园区满的时候会触发

⑤③. 是不是所有对象都分配在堆中?

  • 几乎所有对象是分配在堆中,如果一个对象在方法中没有进行逃逸,也可以分配在栈中进行分配

⑤④. 方法区中都存放什么东西?

  • 类型信息、常量、静态变量、即时编译器编译后的代码缓存

⑤⑤. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误?

  • ①. 加载大量的第三方的jar包

  • ②. tomcat部署的工程过多(30-50个)

  • ③. 大量动态的生成反射类

⑤⑥. 谈谈你对方法区中字符串常量池、静态变量的变化?

  • ①. Jdk 1.6 及之前:有永久代,静态变量、字符串常量池1.6在方法区

  • ②. Jdk 1.7 :有永久代,但已经逐步 " 去永久代 ",字符串常量池、静态变量移除,保存在堆中

  • ③. jdk 1.8 及之后:无永久代,常量池1.8在元空间。但静态变量、字符串常量池仍在堆中

⑤⑦. 为什么要用元空间取代永久代

  • ①.永久代参数设置过小,在某些场景下,如果动态加载的类过多,容易产生Perm区的OOM,比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误

  • ②.永久代参数设置过大,导致空间浪费

  • ③. 默认情况下,元空间的大小受本地内存限制)

  • ④. 对永久代进行调优是很困难的
    [方法区的垃圾收集主要回收两部分:常量池中废弃的常量和不再使用的类型,而不再使用的类或类的加载器回收比较复杂,full gc 的时间长]

⑤⑧. StringTable为什么要调整?

  • ①.jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才能触发。而full gc是老年代的空间不足、永久代不足才会触发

  • ②. 这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放到堆里,能及时回收内存

⑤⑨. new对象流程?对象的内存分配?

  • ①. 判断对象对应的类是否加载、链接、初始化

  • ②. 为对象分配内存

  • ③. 处理并发安全问题

  • ④. 初始化分配到的空间

  • ⑤. 设置对象的对象头

  • ⑥. 执行init方法进行初始化

⑥0. 对象内部结构都有什么?

  • 对象头、实例数据、对齐填充(保证8个字节的倍数)
    在这里插入图片描述

⑥②. Java对象头里有什么?

  • ①. 对象标记Mark Word(哈希值(HashCode )、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)

  • ②. 类元信息

⑥③. 谈谈你对执行引擎的理解?

  • ①. 执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以

  • ②. 解释器(负责响应时间):当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行

  • ③. JIT(负责性能) (Just In Time Compiler)编译器(即时编译器):就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
    JLT是基于计数器的热点探测技术将热点代码进行缓存,主要分为基于:方法调用计数器用于统计方法的调用次数;回边计数器则用于统计循环体执行的循环次数
    在这里插入图片描述

⑥④. 你是怎么指定JVM启动模式?

  • java -version

⑥⑤. 那你知道-server(c2)和-client(c1)的区别吗?

  • C2编译器启动时长比C1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。

⑥⑥. 为什么有了AOT静态提前编译,我们没用?而是用的JLT编译器?

  • ①. 使用JLT编译器,针对的是字节码文件,可以跨平台

  • ②. 可以在动态期间对齐进行优化,比如:逃逸分析优化(逃逸分析优化可以有如下几种:栈上分配、标量替换、同步消除)

⑥⑦. 什么是垃圾?

  • 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾

⑥⑧. Java中垃圾回收的重点区域是?

  • ①. 垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收

  • ②. 从次数上讲:
    频繁收集Young区
    较少收集Old区
    基本不动Perm区(或元空间)

在这里插入图片描述

⑥⑨. 谈谈你对垃圾判别阶段算法都有哪些?

  • ①. 引用计数算法

  • ②. 可达性分析算法

⑦0. 引用计数算法

  • ①. 原理:假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收

  • ②. 最大的缺陷:无法解决循环引用的问题,gc永远都清除不了(这也是引用计数法被淘汰的原因)

  • ③. Java使用的不是引用计数法(Java之所以没有使用引用计数法,是由于不能解决循环引用问题) | (Python使用了是引用计数法)

  • ④. Python如何解决循环引用( 扩展了解 )

  1. 手动解决:很好理解,就是在合适的时机,解除引用关系
  2. 使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用(只要发生了回收,弱引用都会被回收)

⑦①. 你对枚举根节点做可达性分析了解吗?

  • ①. 基本思路是通过一系列名为"GC Roots"的对象(集合)作为起点,从这个被称为GC ROOTs 的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象是不可达对象(被回收),否则就是可达对象
    在这里插入图片描述

⑦②. 在java中,可作为GC Roots的对象有?

  • 注意:除了这些固定的GC Roots集合之外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时加入,共同构架完成整GC Roots 集合。比如: 分代收集和局部回收(面试加分项)
    解释:如果只针对java堆中的某一区域进行垃圾回收(比如: 典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用时候就需要一并将关联的区域对象也加入到GC Roots 集合中考虑,才能保证可达性分析的准确性
	(1).虚拟机栈(栈帧中的局部变量表)中的引用对象(比如各个线程被调用的方法中使用到的参数、
    局部变量等)
	(2).本地方法栈中JNI(即一般来说native方法)中引用的对象[ 线程中的start方法 ]
	(3).静态属性引用的对象(比如:Java类的引用类型静态变量)
	(4).方法区中常量引用的对象(比如:字符串常量池(String Table)里的引用)
		
	(5).所有被synchronized持有的对象
	(6).Java虚拟机内部的引用(基本数据类型对应的Class对象,一些常驻的异常对象
	[如NullPointerException、OutofMemoryError],系统类加载器)
	(7).反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
	(8).注意:除了这些固定的GC Roots集合之外,根据用户所选用的垃圾收集器以及当前回收的内存区域
不同,还可以有其他对象临时加入,共同构架完成整GC Roots集合。比如:分代收集和局部回收(面试加分项)

⑦③. 复制算法及优缺点你是怎么理解的?

  • ①. 复制算法的过程
    在这里插入图片描述
  • ②. 优缺点
  1. ①.没有标记和清除过程,实现简单,运行高效 ②. 不会产生内存碎片,且对象完整不丢
  2. 缺点:①. 浪费了10%的空间 ②. 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
  3. 注意:复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。因为新生代中的对象一般都是朝生夕死的,在新生代中使用复制算法是非常好的
  • ③. 注意:是当伊甸园区满后,会触发minjor gc,进行垃圾的回收

⑦④. 标记清除算法(Mark一Sweep)

  • ①. 标记一清除算法(Mark一Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J . McCarthy等人在1960年提出并并应用于Lisp语言

  • ②. 标记:Collector(垃圾回收器)从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象

  • ③. 清除: Collector(垃圾回收器)对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收

  • ④. 图解: CMS使用这种方式
    在这里插入图片描述

  • ⑤. 优缺点

  1. 优:不需要额外的空间
  2. 缺点:①.两次扫描,耗时严重 ②.清理出来的空闲内存不连续,会产生内存碎片,需要维护一个空闲列表 ③.效率比较低:递归与全堆对象遍历两次(经历了两次遍历)
  • ⑥. 注意:这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放

⑦⑤. 标记整理(压缩)算法(Mark-Compact)

  • ①. 背景:
  1. 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
  2. 标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记一压缩(Mark一Compact) 算法由此诞生
  3. 1970年前后,G. L. Steele 、C. J. Chene和D.S. Wise 等研究者发布标记一压缩算法。在许多现代的垃圾收集器中,人们都使用了标记一压缩算法或其改进版本。
  • ②. 执行过程:
  1. 第一阶段和标记一清除算法一样,从根节点开始标记所有被引用对象.
  2. 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
  3. 最后,清理边界外所有的空间。
  4. 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销
  • ③. 图解:
    在这里插入图片描述

  • ④. 指针碰撞
    (如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer))

  • ⑤. 优缺点

  1. 优点:①. 消除了标记一清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可②. 消除了复制算法当中,内存减半的高额代价
  2. 缺点:①. 从效率.上来说,标记一整理算法要低于复制算法。②. 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。移动过程中,需要全程暂停用户应用程序。即: STW

⑦⑥. JVM采用的是分代收集?

写在最前面:
( 分代算法是针对对象的不同特征,而使用合适的算法,这里面并没有实际上的新算法产生。与其说分代搜集算法是第五个算法,不如说它是对前三个算法的实际应用,在新生代使用复制算法eden在8分空间,survivor在两个1分,只浪费10%的空闲空间。老年代使用标记清除/标记压缩算法清除)

  • ①. 没有最好的算法,只有更合适的算法

  • ②. 分代算法是针对对象的不同特征,而使用合适的算法,这里面并没有实际上的新算法产生。与其说分代搜集算法是第五个算法,不如说它是对前三个算法的实际应用,在新生代使用复制算法eden在8分空间,survivor在两个1分,只浪费10%的空闲空间。老年代使用标记清除/标记压缩算法清除

  • ③. 新生代(Young Gen)

  1. 新生代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
  2. 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解
  • ④. 老年代(Tenured Gen)
  1. 老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
  2. 这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记一清除或者是标记一清除与标记一整理的混合实现。
    Mark阶段的开销与存活对象的数量成正比
    Sweep阶段的开销与所管理区域的大小成正相关
    Compact阶段的开销与存活对象的数据成正比

⑦⑦. 谈谈你对finalize( )方法中虚拟机的状态?

  • ①. 如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收,但事实上,也并非是"非死不可"的,这时候它们暂时处于"缓刑"阶段。一个无法触及的对象肯能在某一个条件下"复活"自己,如果这样,那么对它的回收就是不合理的。为此,定义虚拟机中的对象可能有三种状态。如下:(掌握)
  1. 可触及的:从根节点开始,可以到达这个对象
  2. 可复活的:对象的所有引用都被释放,但是对象有可能在finalize( )中复活
  3. 不可触及的: 对象的finalize( )被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize( )只会被调用一次
  • ②. 以上3种状态中,是由于finalize( )方法的存在,进行的区分。只有对象不可触及才可以被回收

⑦⑧. System.gc()与Runtime.getRuntime().gc()区别

  • ①. 通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存

⑦⑨. 什么是内存泄漏?

  • ①. javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存

  • ②. 说明Java虚拟机的堆内存不够。原因有二

  1. Java虚拟机的堆内存设置不够(比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数一Xms、一Xmx来调整)
  2. 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
  • ③. 这里面隐含着一层意思是,在抛出0utOfMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。
  1. 例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。
  2. 在java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。

⑧0. 什么是内存溢出?

  • ①. 也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏

  • ②. 但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏

  • ③. 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现0utOfMemory异常,导致程序崩溃。

⑧①. Java中内存泄漏的8种情况

  • ①.单例模式(单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。)

  • ②. 一些提供close的资源未关闭导致内存泄漏 数据库连接( dataSourse. getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的。

  • ③. 静态集合类(如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收)

  • ④. 内部类持有外部类(内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。)

  • ⑤. 变量不合理的作用域(一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生)

  • ⑥. 改变哈希值

  • ⑦. 缓存泄漏(内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据)

  • ⑧. 监听器和回调(内存泄漏另一个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式的取消,那么就会积聚)

//静态集合类
public class MemoryLeak {
    static List list = new ArrayList();

    public void oomTests() {
        Object obj = new Object();//局部变量
        list.add(obj);
    }
}
//变量不合理的作用域
public class UsingRandom {
     private String msg;
     public void receiveMsg(){
        //private String msg;
        readFromNet();// 从网络中接受数据保存到msg中
        saveDB();// 把msg保存到数据库中
        //msg = null;
     }
}
//改变哈希值
public class ChangeHashCode {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");

        set.add(p1);
        set.add(p2);
        p1.name = "CC";
        set.remove(p1);
        System.out.println(set);//2个对象!
        
//        set.add(new Person(1001, "CC"));
//        System.out.println(set);
//        set.add(new Person(1001, "AA"));
//        System.out.println(set);

    }
}

⑧②. 什么情况下会导致stop the world

  • ①. 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿

  • ②. 进行gc的时候会发生STW现象(调用finalize()方法的时候会暂停用户线程

  • ③. System.gc( ) | 调用finalize( )方法。我们调用System.gc( ) 会触发Full gc

⑧③. 谈谈你对安全点的理解 ?

  • ①. 程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为 “安全点(Safepoint)”

  • ②. Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择些执行时间较长的指令作为Safe Point, 如方法调用、循环跳转和异常跳转等。

  • ③. 如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  1. 抢先式中断: (目前没有虚拟机采用了) 首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
  2. 主动式中断: 设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

⑧④. 安全区域你知道吗?

  • ①. Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint 。但是,程序“不执行”的时候呢?例如线程处于Sleep 状态或Blocked状态,这时候线程无法响应JVM的中断请求,“走” 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。

  • ②. 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。

  • ③. 实际执行时:

  1. 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发GC,JVM会忽略标识为Safe Region状态的线程;
  2. 当线程即将离开Safe Region时, 会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开SafeRegion的信号为止;

⑧⑤. 强引用、软引用、弱引用、虚引用的区别?

  • ①. 强引用:不回收

  • ②. 软引用: 内存不足即回收

  • ③. 弱引用: 发现即回收

  • ④. 虚引用: 对象回收跟踪
    在这里插入图片描述

⑧⑥. Java中垃圾回收的重点区域是?

  • ①. 垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。(其中,Java堆是垃圾收集器的工作重点)

  • ②. 从次数上讲:频繁收集Young区、较少收集Old区、基本不动Perm区(或元空间)
    在这里插入图片描述

⑧⑦. GC是什么?为什么要有GC?

  • ①. 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

  • ②. 如果不进行垃圾回收,垃圾会占据内存,可能会导致OOM现象

⑧⑧. 吞吐量优先选择什么垃圾回收器?响应时间优先呢?

  • ①. 吞吐量优先选择Parallel GC 垃圾收集器

  • ②. 响应时间优先选择: CMS、G1

⑧⑨. 谈谈你对JVM中垃圾收集器的理解?

  • ①. 不同的厂商会考虑使用不同的JVM,不同的JVM会使用不同的垃圾收集器,下面我介绍下主流的垃圾收集器有哪些(主流的7种),下面你就可以展开去说明七种垃圾收集器的每一个细节

  • ②. 截止JDK 1.8,一共有7款不同的垃圾收集器。每一款不同的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器
    在这里插入图片描述

  • ③. 同厂商、不同版本的虚拟机实现差别很大。HotSpot 虚拟机在JDK7/8后所有收集器及组合(连线),如下图:
    在这里插入图片描述

⑨0. 讲一下CMS垃圾收集器垃圾回收的流程

  • ①. 初始标记(Initial一Mark)仅仅只是标记出和GCRoots能直接关联到的对象,有STW现象、暂时时间非常短

  • ②. 并发标记(Concurrent一Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行(并发标记阶段有三色标记,下文有记录)

  • ③. 重新标记(Remark) 阶段:有些对象可能开始是垃圾,在并发标记阶段,由于用户线程的影响,导致不是垃圾了,这里需要重新标记的是这部分对象,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短

  • ④. 并发清除:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

  • ⑤. 补充说明:

  1. 在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial 0ld收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
  2. CMS收集器的垃圾收集算法采用的是标记一清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。 那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer) 技术,而只能够选择空闲列表(Free List) 执行内存分配。
    (在并发标记阶段一开始不是垃圾,最后变成了垃圾)

⑨①. CMS优缺点

  • ①. 优点:并发收集、低延迟

  • ②. CMS的弊端:

  1. 会产生内存碎片
  2. CMS收集器对CPU资源非常敏感
    (在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低)
  3. CMS收集器无法处理浮动垃圾。可能出现"Concurrent Mode Failure" 失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间
  • ③.区分两个注意事项
  1. 并发标记阶段,在遍历GCRoots,用户线程也在执行,若此时遍历过一个对象发现没有引用,但由于用户线程并发执行,这期间可能导致遍历过的这个对象又被其他对象引用,所以才需要重新标记阶段再遍历一次看又没有漏标记的,否则就会导致被重新引用的对象被清理掉
  2. 浮动垃圾:在并发标记阶段一开始不是垃圾,最后变成了垃圾(属于多标的情况)

⑨②. CMS会出现漏标,怎么解决的?

  • ①. 通过增量更新和写屏障(后)的方式去解决的

  • ②. 在把我们新增的引用放到集合的时候,会实现一种写屏障的方式。在对象前后通过一个dirty card queue将引用信息, 存在card中,这个dirty card queue会放在cardtable中,而cardtable是记忆集的具体实现,最终这个引用就会放在记忆集中的
    (写屏障我们可以理解为在赋值操作的前面加一个方法,赋值的后面做一些操作,也可以理解为AOP。具体的C++实现代码如下图:)
    在这里插入图片描述

⑨③. 三色标志谈谈你的理解是什么样的?

  • ①. 在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。这里我们引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:
  1. 黑色(black):节点被遍历完成,而且子节点都遍历完成
  2. 灰色(gray): 当前正在遍历的节点,而且子节点还没有遍历
  3. 白色(white):还没有遍历到的节点,即灰色节点的子节点
    在这里插入图片描述
  • ②. 根据三色扫描算法,如果有下面两种情况发生,则会出现漏扫描的场景:
  1. 把一个白对象的引用存到黑对象的字段里,如果这个情况发生,因为标记为黑色的对象认为是扫描完成的,不会再对他进行扫描。只能通过灰色的对象(CMS垃圾收集器)
    (如上图中的D如果是白色对象没有引用,某一个时刻由于用户线程的影响,将A黑色对象引用了D的情况,解决办法:使用写屏障和增量更新解决)
  2. 某个白对象失去了所有能从灰对象到达它的引用路径(直接或间接)(G1垃圾收集器)
    (如上图中的B灰色对象某一个时刻由于用户线程的影响将B到D的引用置为null,解决办法:使用写屏障和原始快照)

在这里插入图片描述

  • ③. 三色过程:如下图所示,假如说A引入了B,B引用了C,D没有被任何引用。那么首先我们的CMS首先扫描到了A,发现A有引用B,那么我们的CMS会将A标记为黑色,B标记为灰色,然后这时候,通过B又找到了C那么这个时候发现C已经没有任何引用了就会将C标记为黑色。但是我们的D到目前为止没有被任何引用,记住我这里说的条件!那么D从始至终都没有被扫描,此时就会一直是白色,对于白色的对象来说CMS在执行并发清理的时候就会将此类对象干掉。
    但是这里有了一个问题:如果我们的扫描过程已经结束这一段了,但是此时此刻我的A突然引用了D类型怎么办,这样一来我们的D只要被GC干掉是不是就会出现问题?也就是说我这里产生了一个漏标的问题。当然,我们的JVM开发人员可不是傻子,这里他们用了一个操作叫做增量更新和写屏障来解决这种问题的。

在这里插入图片描述

⑨④. 谈谈你对G1垃圾收集器的理解?

  • ①. G1是一个并行回收器,它把堆内存分割为很多不相关的区域(region物理上不连续),把堆分为2048个区域,每一个region的大小是1 - 32M不等,必须是2的整数次幂。使用不同的region可以来表示Eden、幸存者0区、幸存者1区、老年代等

  • ②. 每次根据允许的收集时间,优先回收价值最大的Region
    (每次回收完以后都有一个空闲的region,在后台维护一个优先列表)

  • ③. 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)

  • ④. 下面说一个问题:既然我们已经有了前面几个强大的GC,为什么还要发布Garbage First(G1)GC?
    官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起"全功能收集器"的重任与期望。

⑨⑤. G1垃圾收集器的特点、缺点

  • ①. 并行和并发
  1. 并行性: G1在回收期间,可以有多个Gc线程同时工作,有效利用多核计算能力。此时用户线程STW
  2. 并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  • ②. 分代收集
  1. 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
  2. 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
  3. 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代

在这里插入图片描述在这里插入图片描述

  • ③. 空间整合
    (G1将内存划分为一个个的region。 内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记一压缩(Mark一Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显)

  • ④. 可预测的停顿时间模型(即:软实时soft real一time)
    (这是 G1 相对于 CMS 的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒、可以通过参数-XX:MaxGCPauseMillis进行设置)

  1. 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制
  2. G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
  3. 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
    (CMS的最好的情况G1不一定比的上,但是CMS最差的部分,G1可以比上)
  • ⑤. 缺点:
  1. 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
  2. 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间

⑨⑥. 谈谈你对G1中的Region的理解

  • ①. 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB, 2MB, 4MB, 8MB, 1 6MB, 32MB。可以通过-XX:G1Hea pRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变

  • ②. 一个region 有可能属于Eden, Survivor 或者0ld/Tenured 内存区域。但是一个region只可能属于一个角色。图中的E表示该region属于Eden内存区域,s表示属于Survivor内存区域,0表示属于0ld内存区域。图中空白的表示未使用的内存空间

  • ③. 垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1. 5个region,就放到H
    (对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待)

在这里插入图片描述

⑨⑦. 大致说说G1的回收过程

  • ①. G1 GC的垃圾回收过程主要包括如下三个环节:
  1. 年轻代GC (Young GC)
  2. 老年代并发标记过程 (Concurrent Marking)
  3. 混合回收(Mixed GC)
  4. 顺时针,young gc -> young gc + concurrent mark-> Mixed GC顺序,进行垃圾回收。
    在这里插入图片描述
  • ②. 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行(多个垃圾线程)的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及

  • ③. 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程

  • ④. 标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。

  • ⑤. 举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收

⑨⑧. G1的年轻代GC?

回收时机
(1). 当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程
(2). 年轻代垃圾回收只会回收Eden区和Survivor区
(3). 回收前:
在这里插入图片描述
(4). 回收后:
在这里插入图片描述

  • ①. 根扫描: 一定要考虑remembered Set,看是否有老年代中的对象引用了新生代对象
    (根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口)

  • ②.更新RSet:处理dirty card queue(见备注)中的card,更新RSet。 此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用
    (dirty card queue: 对于应用程序的引用赋值语句object.field=object,JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1会对Dirty CardQueue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。那为什么不在引用赋值语句处直接更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多)

  • ③. 处理RSet:识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象

  • ④. 复制对象:复制算法
    (此阶段,对象树被遍历,Eden区 内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到01d区中空的内存分段。如果Survivor空间不够,Eden空间的 部分数据会直接晋升到老年代空间)

  • ⑤. 处理引用:处理Soft,Weak, Phantom, Final, JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片

⑨⑨. 并发标记过程

  • ①. 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC

  • ②. 根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成(YoungGC时,会动Survivor区,所以这一过程必须在young GC之前完成)

  • ③. 并发标记(Concurrent Marking): 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

  • ④. 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot一at一the一beginning (SATB)
    (在CMS中有详细讲解)

  • ⑤. 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。(这个阶段并不会实际上去做垃圾的收集)

  • ⑥. 并发清理阶段:识别并清理完全空闲的区域

①00. 混合回收 Mixed GC

  • ①. Mixed GC并不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC

在这里插入图片描述

①0①. G1和CMS相比有哪些优势?

  • ①. G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片

  • ②. G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间

①0②. 我们怎么去选择垃圾收集器?

  • ①. 单CPU或者小内存,单机程序 -XX:+UseSerialGC

  • ②. 多CPU,需要最大吞吐量,如后台计算型应用
    -XX:+UseParallelGC 或者 -XX:+UseParallelOldGC

  • ③. 多CPU,追求低停顿时间,需快速响应如互联网应用
    -XX:+UseConcMarkSweepGC 或者 -XX:+ParNewGC

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐