【八股文】JVM篇

目录

1. Java虚拟机

1. 什么是Java虚拟机

虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的

然后可以讲一讲JVM的内存结构

2. JVM的种类

1. HotSpot JVM(热 + 点 + Java + 虚拟机)

JDK在1.8之前一直在用的虚拟机,jdk1.8的时候把JRockit VM合并到一起了,并把永久代换成了元空间

2. JRockit VM(Java + 摇滚乐 + 虚拟机)

JRockit VM由BEA公司开发,是真正意义的世界上最快的java虚拟机。后BEA公司在2008年被Oracle收购

3. 组成部分(JVM结构)

1. 类加载器(Class Loader)

加载类文件到内存。Classloader只管加载,只要符合文件结构就加载,至于能否运行,它不负责,那是有Exectution Engine 负责的。(编译器在类加载的过程执行的

2. 执行引擎(Execution Engine)

执行引擎也叫解释器,负责解释命令,交由操作系统执行。

3. 本地库接口(Native Interface)

本地接口的作用是融合不同的语言为java所用

4. 运行时数据区(Runtime Data Area)

说的就是JVM内存结构

白话文:类加载器、解释器、JNI、JVM内存结构

2. 类加载器

1. 什么是类加载器(ClassLoader)

本质是一个类,在系统运行过程中动态的将字节码文件(就是.class文件)加载到JVM中的工具

2. 分类(3个系统自带的+用户自定义的)

父级类到子级类分别为:BootStrapClassLoaderExtClassLoaderAppClassLoader、自定义类加载器

1. 根加载器(BootStrapClassLoader)

用C++编写的(挂不得jdk源码中找不到,懂了),所以打印其内容将会得到null

1. 根加载器默认加载路径(在$JRE_HOME/lib下)

根加载器会默认加载系统变量 sun.boot.class.path 指定的类库(会扫描jar文件和**.class文件**),默认是 $JRE_HOME/lib下(注意这里是jre的lib下)的类库,如 rt.jar、resources.jar 等,

白话文:可以把需要加载的类的.class文件放到这个路径下

2. 扩展类加载器(ExtClassLoader)

用于加载系统所需要的扩展类库。

1. 扩展类加载器默认加载路径(在$JRE_HOME/lib/ext下)

默认加载系统变量 java.ext.dirs 指定位置下的类库,通常是 $JRE_HOME/lib/ext 目录下的类库。

3. 应用类加载器(AppClassLoader)

加载应用程序 classpath 下所有的类库。它是应用最广泛的类加载器,是我们最常打交道的类加载器,我们在程序中调用的很多 getClassLoader() 方法返回的都是它的实例。在我们自定义类加载器时如果没有特别指定,那么我们自定义的类加载器的默认父加载器也是这个应用类加载器

白话文:AppClassLoader应用最广用的最多自定义加载器的父级加载器

4. 自定义类加载器(继承ClassLoader)

如果在创建自定义类加载器时没有指定父加载器,那么默认使用 AppClassLoader 作为父加载器

3. 系统自带的三个类加载器的区别

BootStrapClassLoader居然在代码中都找不到(亲测),说明JVM不会把这个根加载器提供给开发人员(不亏是你,怪不得叫根加载器了)

ExtClassLoaderAppClassLoader,继承了URLClassLoader最终继承ClassLoader

部分源码如下:

public class Launcher {
	static class AppClassLoader extends URLClassLoader {
	}
    static class ExtClassLoader extends URLClassLoader {
    }
}

4. 问:父加载器中的父和Java中的父类是一样吗?

Java虚拟机中的所有类加载器采用具有父子关系的树形结构进行组织(不是用extend继承的关系),在实例化每个类加载器对象时,需要为其指定一个父级类加载器对象或者默认采用应用类加载器(默认好像是AppClassLoader)为其父级类加载。

不一样,父加载器中的《父》是父级类而不是父类(哈哈,是不是看不懂了,父级类就理解为流程图一样的结构,有先后顺序)

5. 类加载器的双亲委派机制(加载类时候的机制)

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

白话文:类加载器加载一个类的时候,会先去问问父加载器能不能加载,如果不行,那就自己去加载

1. 问:为什么要用双亲委派?

  1. 为了安全,像一些系统相关的基础类,必须由根加载器加载
  2. 为了防止类被重复创建

6. 类加载的方式

1. 隐式加载(通过new方式)

1. 通过new加载

程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,

2. 显式加载

1. 通过反射加载

使用Class类的Class.forName(className)这个方法,如果找不到className,则抛出ClassNotFoundException异常

2. 通过指定的类加载器实例加载

使用ClassLoader实例的loadClass方法

3. 三者之间的关系

1. 使用的类加载器
  • newClass.forName使用的是当前的类加载器
  • classLoader.loadClass必须指定具体的类加载器
2. 初始化和实例化类
  • new:初始化类 + 实例化类
  • Class.forName:初始化类,需要调用newInstance才能实例化类
  • classLoader.loadClass:只执行初始化类中的第一步(类加载)

两者本质是一样的!都是加载到JVM虚拟机中

7. 类加载的动态特性如何体现

显式加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中(为了保证快速启动),至于其他类,则在需要的时候才加载(有种懒汉模式的感觉)。这当然就是为了节省内存开销。

白话文:懒汉模式的思想,先把保证程序运行的基类加载好,其他类用到的时候再加载

3. 类加载的过程(重要)

在这里插入图片描述

类的生命周期:加载、验证、准备、解析、初始化、使用、卸载

白话文:类加载过程就是类的生命周期的前5个

原文链接:https://www.cnblogs.com/jimoer/p/9091315.html

1. 加载(第一阶段,3件事情)

加载阶段要做的3件事
在这里插入图片描述

1. 获取二进制字节流(字节码变成字节流)

通过一个类的全限定名确定.class字节码文件,从而获取此类的二进制字节流。

加载阶段(准确地说,是获取类的二进制字节流的动作)是整个类加载过程中开发人员可控性最强的(开发人员可掌控),因为加载阶段既可以使用系统提供的引导类加载器完成,又可以由用户自定义的二类加载器去完成(为什么是二?应该说的是子级加载器),开发人员可以通过定义自己的类加载器去控制字节流的获取方式

白话文:有个类加载器ClassLoader(也可以用户自定义),根据类的全名,拿到这个类的字节流

2. 数据格式转换(转化成方法区要的格式)(这个流程还要多去理解)

将这个字节流所代表的静态存储结构转化为方法区运行时(Runtime??)数据结构

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式(啥格式?)存储在方法区中,方法区中的数据存储格式由虚拟机的实现自行定义,虚拟机规范未规定方法区的具体数据结构

白话文:方法区的数据怎么存,虚拟机管不了,所以要变一下数据格式(方法区比较倔,虚拟机要听它的,它要什么样的数据格式,虚拟机就要转成什么格式)

3. 生成Class对象(在方法区!这时候还没有分配内存空间)

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

白话文:在方法区生成Class对象,这时候还没有分配内存空间

1. 题外话:Class类对象存在方法区吗?

是的!一般的对象(也就是我们常见的实例对象)都存在堆中,但Class对象比较特殊,存在方法区里,代表的是类对象

2. 题外话:Class对象干嘛用的?(多去理解)

这个对象将作为程序访问方法区中的这些类型数据(哪些数据类型?其他对象)的外部接口

可删掉:加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证东西)是交叉进行的,但是这两个阶段的开始时间仍然保持着固定的 先后顺序。

2. 验证(UTF-8、Java语法、符号引用验证等)

总结:对字节流的验证,其中要记住的是符号引用验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击

白话文:检验字节流有没有问题、有没有危险,注意这里是字节流!不是字节码

验证分为4个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

1. 文件格式验证(UTF-8编码格式验证、文件完整性验证)

这一阶段主要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

1. 验证的内容
  1. 是否以魔数0xCAFEBABE开头,主次版本号是否在当前虚拟机处理范围之内
  2. 常量池的常量是否有不被支持的常量类型,指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量
  3. CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
  4. Class文件中各个部分及文件本身是否有被删除的或附近的其他信息等等

白话文:验证文件是否被改了、验证编码格式UTF-8等等

2. 元数据验证(Java语法验证)

第二阶段主要是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。

1. 验证的内容
  1. 当前类是否有父类(除了Object类之外,所有类都该有父类,Enum也有父类)
  2. 当前类是否继承了被final修饰的类
  3. 如果当前类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  4. 类中的字段、方法是否与父类产生矛盾(如覆盖了父类的final字段等)等等

白话文:验证Java语法,比如继承、实现相关的语法

3. 字节码验证(最复杂,解释不了)

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

疑点:验证的不是字节流吗?怎么又有字节码验证了。。。

1. 验证的内容
  1. 保证任意时刻操作栈的数据类型与指令代码序列都能配合工作,例如:保证不会出现在操作栈放置了一个int类型的数据,使用时却按long类型来加载如本地变量表中。
  2. 保证跳转指令不会跳转到方法体以为的字节码指令上。保证方法体上的类型转换是有效的,例如:可以把一个子类对象赋值给父类数据类型,但是不能把父类对象赋值给子类数据类型。

白话文:比如我给你的是int,你不能给我一个long(有点牵强)

4. 符号引用验证(验证栈内的引用是否能找到对应的类)

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段。符号引用验证可以看做是对类自身以外的信息进行匹配校验。

1. 验证的内容
  1. 符号引用通过全限定名是否能找到对应的类
  2. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。(是否存在说的字段)
  3. 符号引用中的类、字段、方法的访问性是否可以被当前类访问等等。

白话文:比如private、protect这种的规则验证

3. 准备(真正地分配内存空间并赋值0)

总结:为Class类对象分配内存空间,并赋值0

准备阶段是正式为类变量分配内存并设置类变量初始值0的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段分配内存的仅仅是类变量不包括实例变量

注意:这里给类变量设置的初始值是零值(还没运行程序呢,所以不是程序中设置的值),真正设置值的阶段是初始化阶段

当然还有例外,如果类中自带常量(如用public static final 修饰的就是常量),才会赋值为设定的值

白话文:为所有类变量在方法区分配内存空间(真正地分配),至于赋值的话(我这只有0,如果你没有常量,那我就给你0

1. 题外话:类变量和实例变量的区别

  1. 类变量是在准备阶段的时候分配到方法区中;
  2. 实例变量在实例化阶段的时候分配到堆内存

4. 解析(简化引用,将符号引用转化为直接引用)

总结:简化引用,如果是引用指向常量池,则直接变成直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

1. 什么是符号引用(Symbolic References)

总结:符号引用本质就是一组符号,由类名、字段名、方法名组成

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用目标并不一定已经加载到内存中。

1. 符号引用涉及哪些名字(类名、字段名、方法名)
  1. 类和接口的全限定名
  2. 字段名称和描述符
  3. 方法名称和描述符

白话文:符号引用主要就是各种名字,如类、接口、字段、方法等,(还叫什么符号引用,不就是就是名字嘛)

从下文得知:符号引用存在方法区的运行时常量池

2. 什么是直接引用(Direct References)

直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有直接引用,那引用的目标必定已经在内存中存在。

白话文:直接引用应该就是我们常常说的引用吧,但是我们说的引用是存在栈里的。。。不确定

3. 这个阶段主要对谁做解析?

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符,这7类符号引用,分别对应于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 这7中常量类型。

白话文:主要对类、接口相关的调用做解析(还是没说人话哈哈哈,有点抽象理解不了)

5. 初始化

总结:编译器收集需要初始化的类(类变量赋值、静态代码块),执行clinit方法

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。在准备阶段,变量已经赋过一次系统初始零值了,而在初始化阶段,是通过程序制定的主观计划去初始化类变量和其他资源,也就是执行类构造器clinit方法的过程

构造器方法执行过程中一些可能会影响程序运行行为的特点和细节。

1. 构造器执行clinit(cl:处理,init:初始化)的流程如下:

1. 编译器收集代码(类变量赋值相关的代码、静态代码块里的代码)

clinit方法是由编译器自动收集类中的所有类变量赋值动作和静态代码块(static{})中的语句合并产生的,编译器收集顺序是由语句在源文件中出现的竖线所决定的(不一定是代码的顺序,代码可能经过了指令重排),静态代码块中只能访问到定义在静态代码块之前的变量,定义在它之后的变量,在前面静态代码块可以赋值,但是不能访问。(这是Java语法,不多解释)

白话文:编译器收集代码(类变量赋值相关的代码、静态代码块里的代码)

1. 题外话:什么是编译器?什么是解释器?
  • 编译器把.class文件变成字节码文件,一次执行所有
  • 解释器是用来执行编译器编译后的程序,一步一步执行

白话文:解释器是在编译器之后执行的,记住这点就行

2. 执行顺序会从父类开始(第一个是Object)

clinit方法与类的构造函数不同,它不需要显示的调用父类构造器,所以虚拟机中第一个被执行的clinit方法的类肯定是java.lang.Object。

白话文:从头开始,Object是所有类的父类

题外话:xxx(待补充)会先去执行哪些类呢?为什么会先执行Object类

是Object类,是所有类的父类,因为clinit不会去找一个类的父类,不能溯源(这词用的真棒!),所以要先把源头干掉

3. 过滤普通的类(也就是编译器不会收集的类)

如果一个类中没有静态代码块,也没有对变量的赋值操作,那么编译器可以不为这个类生产clinit方法

白话文:如果2个操作都没有,那就忽略这个类

4. 如果Interface接口中有变量初始化操作,也会被编译器收集

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成()方法。

白话文:如果接口中有变量初始化操作,也会被编译器收集

5. 虚拟机通过加锁的形式保证类只能被初始化一次

虚拟机会保证一个类的clinit方法在多线程的环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程区执行这个类的clinit方法,其他线程都需要阻塞等待,直到活动线程clinit方法

白话文:真牛,虚拟机居然也会遇到多线程的情况,也会进行加锁、解锁操作

4. 类的卸载(垃圾回收)

1. 什么时候会被卸载?(垃圾收集机制)

当代表这个类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。

2. 哪些类不会被卸载?(系统类加载器产生的类)

Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。包括根类加载器扩展类加载器系统类加载器

3. 哪些类会被卸载?(自定义类加载器产生的类)

用户自定义的类加载器加载的类是可以被卸载的

原文链接:https://blog.csdn.net/xorxos/article/details/80490240

5. JVM内存结构

在这里插入图片描述
在这里插入图片描述
图片来源:https://javaguide.cn/java/jvm/memory-area/#

1. 线程私有的区域

程序计数器、虚拟机栈、本地方法栈

1. 程序计数器

1. 存什么

指示Java虚拟机下一条需要执行的字节码指令

2. 线程切换后能恢复到原来的位置

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储

3. 不会OOM

程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

白话文:字节码指令都崩了,那JVM别玩了

2. 虚拟机栈(为Java方法服务)

每次方法调用的数据都是通过栈传递的

1. 虚拟机栈里存什么(基本数据类型、对象的引用)
  • 8种基本数据类型
  • 对象的引用
  • 栈帧
2. 出现的异常(要看看栈能不能支持动态扩展)
1.StackOverFlowError 栈溢出错误

若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误

白话文:我在使用递归的时候出现栈溢出错误,所以栈默认设置应该是不支持动态扩容的

2. OutOfMemoryError 内存溢出错误

若Java虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

3. 栈帧
1. 栈帧的组成

局部变量表、操作数栈、动态链接、方法出口信息、锁记录空间(Lock Record)(轻量级锁中用到,用于存储锁对象头的MarkWord拷贝

tips:如果栈的类型是引用数据类型,还会记录引用的对象内存地址

2. 栈帧的操作
1. 压栈

每一次方法调用都会有一个对应的栈帧被压入 Java 栈

2. 弹栈

每一个方法调用结束后,都会有一个栈帧被弹出

小tips:方法调用结束有2个标志:正常结束return抛出异常throw,都会弹出栈帧

3. 栈帧和方法之间的关系

一个方法就对应一个栈帧

3. 本地方法栈(为native方法服务)

可以理解为和虚拟机栈一样,除了服务对象不同

2. 线程共享的区域

1. 堆(GC堆)

总结:围绕堆的分类、堆里存什么、堆的异常等话题

1. 堆里存什么(对象实例、数组)

存放对象实例,几乎所有的对象实例以及数组都在这里分配内存

1. 题外话:所有的对象实例都存放在堆里吗?

不是的,jdk1.7优化了,《逃逸分析技术》

2. 题外话:什么是逃逸分析技术?

随着 JIT 编译器的发展逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存

public void dome(){
    User user=new user();
    user.hello();
}

User对象就未逃逸出去,所以对象会直接存在栈里了

白话文:jdk1.7优化了,如果这个引用的实例未逃逸没被返回没被其他方法调用),就留在栈里分配内存了,这样做可以大大缓解GC压力

2. 堆的分类
1. 根据年代父类

注意:永久代不在堆里!永久代是方法区的具体实现

1. 新生代内存

新生代内存又可以分为Eden区、Survivor 0区、Survivor 1区

2. 老年代内存
2. 根据存储信息分类
1. Java堆

存放绝大多数对象

2. Native堆

存放运行native方法时产生的对象

3. 堆中出现的异常
1. OutOfMemoryError 内存溢出错误(OOM还分种类呢,惊不惊喜意不意外!)

因为堆中是最容易出现OOM异常的地方,所以对具体是那种OOM异常的原因也做了划分

1. OutOfMemoryError: GC Overhead Limit Exceeded GC超出开销限制

当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。

2. java.lang.OutOfMemoryError: Java heap space 堆空间不足

假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过**-Xmx**参数配置)

1. 问:只有内存不足才会出现OOM吗?

不止,如果垃圾回收率太低,也会发生OOM异常,只不过这个OOM类型不一样,叫做GC超出开销限制

4. 题外话:-Xms和-Xmx全称是什么?(算了看了全称也不好记住)
  • -Xms:minimum memory size for pile and heap 分配最小内存数
  • -Xmx:maximum memory size for pile and heap 分配最大内存数

2. 方法区(是个概念,类似于Java中的接口)

静态常量池、运行时常量池、永久代

1. 方法区里存什么?

存储已被虚拟机加载的类信息常量静态变量(就是static修饰的变量)、即时编译器编译后的代码等数据

2. 组成部分
1. 静态常量池(又叫Class文件常量池,也在方法区里,发生在编译器)

是**.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量**,还包含类、方法的信息,占用class文件绝大部分空间。

这种常量池主要用于存放两大类常量:

1. 字面量

相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值

2. 符号引用

是一种特殊的字面量,在上面有讲,忘了可以滑上去看看,是属于编译原理方面的概念

2. 运行时常量池(在方法区里,本质是HashTable,发生在类装载之后)

是jvm虚拟机在完成类装载操作后,将Class文件中的常量池(就是静态常量池)载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池

用于存放编译生成的各种字面量符号引用

2. 永久代(jdk1.8之前这么叫)

永久代包括了运行时常量池

3. 相关问题
1. 题外话:运行时常量池和Class常量池的区别?

运行时常量池具备动态性运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

白话文:运行时常量池里的数据不一定都是来自Class常量池,还有可能是来自于开发人员,如String的intern方法

1. String的intern方法的作用

总结:运行期间将新的常量放入运行时常量池

检查字符串常量池中是否存在这个字符串,若不存在就将这个字符串放入常量池中

2. 题外话:jdk1.8前后方法区发生了什么改变
1. jdk1.8之前

方法区的具体实现是永久代运行时常量池

2. jdk1.8之后

方法区的具体实现是元空间(元空间是直接空间,可能不在方法区,如果错了要注意改一下)和运行时常量池(这个不确定还有没有,但是如果没有的话,那么,这些符号引用存哪里呢,所以90%概率是有的,先这么记!)

3. 题外话:方法区和永久代有什么区别?(好问题,可以留着)

永久代是HotSpot的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法

白话文:方法区相当于是接口,永久代相当于是接口的实现类,HotSpot虚拟机中才有永久代的概念

4. 题外话:永久代在jdk 1.8被移除了,那这些数据存哪里?

元空间,元空间使用的是直接内存

3. 直接内存(又叫堆外内存,不归JVM管)

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

1. 元空间(jdk1.8的时候出来的,代替永久代)
1. 元空间里存什么?(类的元数据)

类的元数据

问题:原来方法区和运行时常量池里面的东西都存哪里去了?都放在元空间吗?

2. 元空间和永久代的区别
1. 内存容量不同
  • 方法区:有一个 JVM 本身设置的固定大小上限,无法进行调整
  • 元空间:使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

白话文:方法区内存受JVM本身限制,元空间是使用直接内存,没有限制

3. 出现的异常(OOM)
  • java.lang.OutOfMemoryError: MetaSpace 元空间OOM异常
1. 题外话:JVM为什么使用元空间替换永久代?(好问题)

总结:2个JVM合并、永久代存字符串、永久代内存有上限、GC效率低

  1. 将 HotSpot 与 JRockit 合二为一(因为 JRockit 没有永久代,所以不需要配置永久代)
  2. 字符串存在永久代中,容易出现性能问题和内存溢出(为啥?因为字符串)
  3. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出太大则容易导致老年代溢出
  4. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
2. 题外话(真):为什么NIO会这么快?(基于通道、缓存区、堆外内存)

关键字:用户态、内核态、通道、缓冲区

1. 版本1

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer(翻译:直接字节缓冲区??) 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据

白话文:NIO是可以让native方法使用堆外内存(原使用Native堆的)

2. 版本2
  1. 避免了用户态和内核态的交换直接操作内存,用户态和内核态的转换是很费时的,传统的io写入磁盘时,用户态的接口不能直接操作内存,而是通过操作系统调用内核态接口来进行io

  2. 利用buffer减少io的次数,buffer化零为整”的写入方式因为大大减小了寻址/写入次数,所以就降低了硬盘的负荷

6. Java内存模型(JMM:Java Memory Model)

原文链接:https://blog.csdn.net/mifffy_java/article/details/100882646

1. 是什么(有主内存和本地内存)

Java内存模型就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性

白话文:比如,定义了volatile关键字的变量,对于其他线程是可见的,其他线程要操作这个变量的时候,一定要先去主内存拿到最新数据,拷贝一份到本地内存。

2. Happens-Before是什么?有什么规则?(了解就行,可能白话文这块写的不对,以后再改TODO)

Happens-before:理解为先行发生

Happens-Before是一组排序规则,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),规则如下:

  1. 单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作

  2. 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作(在解锁之前一定要锁定?)

    白话文:先unlock,后lock

  3. volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作(volatile字段,写入操作之前第一要有读取操作?)

    白话文:volatile 的更新操作先,读取操作后

  4. 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作

    白话文:先start方法执行,后线程的操作执行

  5. 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前

    白话文:

  6. 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C

    白话文:A在B之前,B在C之前,那么A一定在C之前

3. 主要用到的规则

  • 对变量unlock之前,必须要从本地内存同步到主内存
  • 对变量进行lock时,会清空本地内存的值
  • 在执行引擎(这是什么?)使用变量前,需要重新从主内存load操作或assign操作初始化变量值

白话文:操作主内存的变量,在加锁之前,必须清空自己本地内存的对象;在解锁之前,必须把变量同步到主内存

4. 题外话:JVM体系结构、JVM内存结构和Java内存模型分别是什么?

  • JVM体系结构,描述了JVM是由什么组成的,分为类加载器、解释器、JNI、JVM内存结构
  • JVM内存结构,描述的是线程运行所设计的内存空间,有栈、堆、方法区、元空间等
  • Java内存模型,描述的是多线程允许的行为,一种抽象化的概念?实际不存在的

7. Java的对象结构

1. 什么是对象

可谓是万物皆对象,对象是客观存在的事物,可以说任何客观存在的都是可以成为对象

tips:顺便说说类,类是不存在的,是用来描述对象信息

2. 对象的组成部分

对象头、实例数据、对齐填充

1. 对象头

1. 对象自身运行时的数据(存放自身状态,如hashcode)

例如:HashCode对象锁状态标志持有锁的线程(持有或未持有)、锁偏向线程ID(偏向锁用的)、LockWord(这是什么?轻量级锁用到了)、偏向时间戳、GC分代年龄监视器锁monitor锁,这个家伙很关键好不好)等

Mark Word 被设计为没有固定的数据结构用来在很小的空间内(应该就是01、10、11这种,表示的是状态用不了太多空间)可以存储更多的信息,它会根据不同的状态存储不同的数据

白话文:对象自身的状态情况,如是否占有锁、hashcode值

2. 类型指针(存放引用、数组大小)

就是指向此对象的类元数据的指针,也就是通过这个指针来知道这个对象是哪个类的实例

如果对象是个数组(呀?数组也是对象吗List对象,好像是的。),那还需要一块地方来记录数据的长度

白话文:存放的是引用(就是在虚拟机栈的那个引用,指向这个对象),如果对象是数组(如List),则还要存大小(多一个字段)

2. 实例数据(存放数据)

实例数据就是对象真正存储的数据区,各种类型的字段内容。

3. 对齐填充(可以认为就是空格)

这部分内容没什么别的意义,就是起着占位符的作用,主要是因为HotSpot虚拟机的内存管理要求对象的大小必须是8字节的整数倍,而对象头正好是8个字节的整数倍,但是实例数据不一定,所以需要对齐填充补全。

白话文:空格,为了保证对象的大小是8字节(单位是B)的倍数

8. 说说对象分配规则(对象实例化后要被分配到那个内存区?)

1. 小对象优先分配在Eden区

如果Eden区没有足够的空间时,虚拟机执行一次Minor GC

2. 大对象直接进入老年代

大对象是指需要大量连续内存空间的对象。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存

3. 长期存活的对象进入老年代

虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值对象15次进入老年区。

1. 题外话:说说JVM开发者们分配对象的时候都考虑了哪些问题

  • 考虑内存如何分配、在哪里分配等问题
  • 内存分配算法内存回收算法密切相关
  • 考虑GC执行完内存回收是否会在内存空间中产生内存碎片

白话文:堆内存分类、分配算法、内存回收算法、内存回收后的空间碎片如何处理

9. 垃圾回收算法有哪些?

原文链接:https://www.cnblogs.com/jxxblogs/p/12208118.html

1. 标记-清除算法

从算法的名称上可以看出,这个算法分为两部分,标记和清除。首先标记出所有需要被回收的对象,然后在标记完成后统一回收掉所有被标记的对象。(标记的对象被回收?不是留下来?OopMap记录的是标记的对象,怎么和这里反一下的)

1. 缺点(产生内存碎片)

  1. 效率不高:标记和清除的效率不是很高
  2. 会产生内存碎片:标记和清除后会产生很多的内存碎片,导致可用的内存空间不连续,当分配大对象的时候,没有足够的空间时不得不提前触发一次垃圾回收(那我触发垃圾回收会清除内存碎片吗?)

2. 复制算法(内存分为2块,空间换时间,效率高)

这个算法将可用的内存空间分为大小相等的两块,每次只是用其中的一块,当这一块被用完的时候,就将还存活的对象复制到另一块中,然后把原已使用过的那一块内存空间一次回收掉。这个算法常用于新生代的垃圾回收。

复制算法解决了标记-清除算法的效率问题,以空间换时间,但是当存活对象非常多的时候,复制操作效率将会变低,而且每次只能使用一半的内存空间,利用率不高。

白话文:把内存分为2块,只用1块,满的时候,会进行MinorGC,把活的放到另一块中,上述操作往复循环

1. 优点(效率高)

1.效率高,空间换时间,主要体现在回收内存上(一次性把另一块直接删掉了,管都不管)

2. 缺点(浪费内存)

  1. 内存利用率不高:最多只能用到50%的内存(需要有一半的空间做接收存货对象的准备
  2. 存活对象多的时候,效率不高:一次垃圾回收操作之后,存活对象非常多的时候,复制操作效率将会变低

白话文:一会说效率高,一会又说效率不高的,注意区分场景哈!

3. 标记-整理算法(准确来说是3步)

1. 步骤

1. 标记

标记出所有需要被回收的对象

2. 整理(移动操作)

把所有存活的对象都向一端移动

3. 清除

把所有存活对象边界以外的内存空间都回收掉

2. 优点(解决内存碎片、解决内存浪费)

  1. 解决了复制算法多复制效率低的问题
  2. 解决了空间利用率低的问题
  3. 解决了内存碎片的问题

3. 缺点(效率不高)

  1. 效率不高:相比标记-清除算法,多了一步整理操作,这个是非常耗时的

4. 分代收集算法(分为新生代、老年代)

根据对象生存周期的不同将内存空间划分为不同的块,然后对不同的块使用不同的回收算法。一般把Java堆分为新生代老年代

  • 新生代中对象的存活周期短,只有少量存活的对象,所以可以使用复制算法
  • 而老年代中对象存活时间长,而且对象比较多,所以可以采用标记-清除标记-整理算法

白话文:新生代用复制算法,老年代用标记-清除算法标记-整理算法

10. 垃圾收集器有哪些?

原文链接:https://blog.csdn.net/guorui_java/article/details/108405844

1. 串行垃圾收集器(Serial)

日常吐槽:原来是叫串行垃圾回收器的。。。叫什么回收器,这么拗口,还是叫收集器顺口

JDK1.3之前,单线程收集器(串行换一个说法)是唯一的选择。它的单线程意义不仅仅是说它只会使用一个CPU一个收集线程去完成垃圾收集工作。而且它进行垃圾回收的时候,必须暂停其它所有的工作线程(Stop The World,STW),直到它收集完成。它适合Client模式的应用(这是啥,客户端应用程序?客户端也会回收垃圾吗?),在单CPU环境下,它效率高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。

白话文:单线程,需要STW操作,效率高

1. 分类

1. Serial(新生代、复制算法)
1. 作用区域(新生代)

新生代,用复制算法

2. Serial Old(标记-整理算法)
1. 作用区域(老年代)

老年代,用标记-整理算法

2. JVM相关参数

  1. -XX:+UseSerialGC新生代使用串行收集器,且老年代使用串行收集器

    白话文:这里应该是包括开启了Serial、Serial Old两个

2. 并行垃圾回收器(2个新生代 + 1个老年代)

并行垃圾回收器是通过多线程进行垃圾收集的。也会暂停其它所有的工作线程(Stop The World,STW)。适合Server模式以及多CPU环境。一般会和JDK1.5之后出现的CMS搭配使用。

1. 分类

1. ParNew(Serial的多线程版本、新生代、复制算法)

Serial收集器的多线程版本默认开启的收集线程数和CPU数量一样,运行数量可以通过修改ParallelGCThreads(又是一个JVM参数)设定。

1. 作用区域

新生代,用复制算法

2. JVM相关参数
  1. -XX:+UseParNewGC新生代使用并行收集器

    白话文:这个参数只是改变了新生代的收集器,老年代的还没变呢。。。所以一般会和Serial Old收集器一起,使用分代收集策略。

2. Parallel Scavenge(就叫高并发扫收集器吧、新生代、复制算法)

关键字:吞吐量、最大停顿时间、新生代、复制算法、动态调整新生代比例

Parallel Scavenge [ˈskævɪndʒ] : 关注吞吐量,吞吐量优先

相关参数:最大停顿时间MaxGCPauseMillis吞吐量大小GCTimeRatio

1. 作用区域

新生代,用复制算法

2. JVM相关参数
  1. -XX:+UseParallelGC新生代使用高并发扫收集器

    白话文:Server模式下默认和Serial Old收集器一起,使用分代收集策略。

  2. -XX:+UseAdaptiveSizePolicy:动态调整新生代的大小、Eden、Survivor比例等(自适应大小策略
    目的是为了提供最合适的停顿时间或者最大的吞吐量。

3. 题外话:什么是垃圾收集器的吞吐量?和我们常说的吞吐量还是有区别的

垃圾收集器的吞吐量 = 代码运行时间 /(代码运行时间 + 垃圾收集时间)

3. Parllel Old(高并发扫收集器的老年代版本、老年代、标记整理算法)

Parallel Scavenge的老年代版本。JDK 1.6开始提供的。在此之前Parallel Scavenge的地位也很尴尬(新生代都版本升级了2次了,老年代一次都还没有,太可怜了)

1. 作用区域

老年代,什么算法呀?应该还是标记-整理算法

2. JVM相关参数
  1. -XX:+UseParallelOldGC老年代使用高并发扫收集器

白话文:Parallel Scavenge可以搭配Parallel Old器组合进行内存回收。(最高效

3. CMS收集器(Concurrent Mark Sweep、老年代、标记清除)

关键字:老年代、标记清除、低停顿时间

CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能知道它是标记-清除算法的。但是它比一般的标记-清除算法要复杂一些(强化版标记-清除算法

1. 运行流程(3次标记+1次清除)

总结:1次STW标记、1次并发标记、1次重新标记、1次并发清除

1. 初始标记(根节点枚举,拿到GC Roots引用链)

标记一下GC Roots能直接关联到的对象,会"Stop The World"(直接拿到GC Roots的引用链

2. 并发标记

GC Roots Tracing(追踪?),可以和用户线程并发执行,不用STW

3. 重新标记

对现有GC Roots引用链的对象再次判断(再次确认),修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”

1. 题外话:重新标记为什么不会标记到浮动垃圾?

浮动垃圾是无中生有的,并不在GC Roots引用链上,要想标记,必须重新再来一遍

4. 并发清除

清除对象,可以和用户线程并发执行

2. CMS收集器的算法和普通的标记-清除算法有什么区别?高级在哪里?

1. 标记阶段比较复杂

分成了3个阶段,简单标记、并发标记、确认标记(就是重新标记)

2. 有2个阶段不会阻塞用户线程

并发标记并发清除阶段,不会阻塞用户的线程的

3. 作用区域

只在老年代强化版标记-清除算法

记住:CMS只作用在老年代,没有新生代!

4. JVM相关参数

  1. -XX:+UseConcMarkSweepGC老年代使用CMS收集器

5. 优点(高并发、低停顿时间)

  1. 并发效率高
  2. 响应快:低停顿时间

6. 缺点(内存碎片、浮动垃圾)

1. 抢CPU争抢问题

由于垃圾回收线程可以和用户线程同时运行,也就是说它是并发的,那么它会对CPU的资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/ 4,当CPU<4个时,垃圾收集线程就不会少于25%,而且随着CPU减少而增加,这样会影响用户线程的执行。

2. 内存碎片问题(标记-清除算法)

整体是基于标记-清除算法的,那么就无法避免空间碎片的产生

3. 浮动垃圾问题

CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”(并发修改异常)失败而导致另一次Full GC的产生。

1. 题外话:什么是浮动垃圾?

一句话:本来可达的对象,变成不可达了

所谓浮动垃圾,在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留待下一次GC时再清理掉。

白话文:浮动垃圾就是在标记过程之后,清理过程之前,产生的垃圾(因为是并发的缘故,没法控制Java线程不产生垃圾呀)

浮动垃圾是小问题,下次GC的时候就行了

4. G1垃圾收集器

关键字:自身分区、3次标记+1次清除、局部复制算法,整体标记-清除

1. 运行流程(3次标记+1次清除)

1. 初始标记(根节点枚举,拿到GC Root引用链、标记《代》)

标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Set)的值(代表对象在新生代还是老年代),然后STW(和CMS的初始标记一样,区别就是多了一步标记《》的操作)

白话文:拿到GC Root引用链,标记对象的《》,然后STW

2. 并发标记

从GC Roots开始对堆中对象进行可达性分析,找到存活的对象,这阶段耗时较长,但是可以和用户线程并发运行

白话文:并发地就行可达性分析操作

3. 最终标记(会STW,标记浮动垃圾、Remembered Set Logs)

为了修正在并发标记期间因用户程序继续运行而导致标记产生变化的那一部分标记记录(浮动垃圾),虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,把Remembered Set Logs的数据合并到Remembered Sets中,这阶段需要暂停线程,但是可并行执行

白话文:找到浮动垃圾,并发去处理

4. 筛选回收(先排序,后清理)

首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来确定回收计划

白话文:先排序,后清理

2. G1分区的概念

G1的堆区在分代的基础上,引入分区的概念。G1将堆分成了若干Region。(这些分区不要求是连续的内存空间)。分区大小一旦设置,则启动之后不会再变化。

1. 有哪些区域
1. Eden区
2. Survivor区
3. Free区(空白区)

Region之间基于复制算法,GC后会将存活对象复制到空白区不会产生空间碎片

4. Old区
5. Humongous区(大对象区,H区)

存放大对象(占用整个区的50%以上),如果一个H区装不下一个大对象,则会通过连续的若干H分区来存储。因为大对象的转移会影响GC效率,所以并发标记阶段发现大对象不再存活时,会将其直接回收。ygc也会在某些情况下对巨型对象进行回收。(ygc为啥也会回收大对象?都不经过年轻代了)

3. 作用区域

新生代、老年代整体标记-清除,局部复制算法

准确来说G1收集器是自身实现了分区,倒没有作用区域这个概念了

3. JVM相关参数

  1. -XX:+UseG1GC:使用G1收集器(注意:这里是不区分新生代、老年代的)
  2. -XX:+G1HeapRegionSize:设置G1分区的内存大小
  3. -XX:+G1HeapWastePercent:设置混合GC触发的临界值百分比(G1收集器独有的)

4. G1的优点

1. 整体标记-清除,局部复制算法

G1从整体看还是基于标记-清除算法的,但是局部上是基于复制算法的。

2. 不会产生空间碎片

这样就意味着它空间整合做的比较好(?没有整理操作哪里做的好了,新生代复制算法)

3. 高并发

G1还是并发与并行的,它能够充分利用多CPU、多核的硬件环境来缩短“stop the world”的时间。

4. 自身实现分区

G1还是分代收集的(自身实现分区),但是G1不再像上文所述的垃圾收集器,需要分代配合不同的垃圾收集器,因为G1中的垃圾收集区域是“分区”(Region)的。

5. G1还有Mixed GC(混合GC)操作

G1的分代收集和以上垃圾收集器不同的就是除了有新生代的ygc,全堆扫描的full GC外,还有包含所有年轻代以及部分老年代Region的Mixed GC(那就叫混合GC吧)。

6. 可预测停顿

通过调整参数(具体啥参数呀?),制定垃圾收集的最大停顿时间

G1牛皮

5. G1收集器的GC的种类

1. Young GC年轻代收集

在分配一般对象(非巨型对象)时,当所有Eden区使用达到最大阀值并且无法申请足够内存时,会触发一次Young GC(Eden满了,会触发一次ygc)。每次Young GC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区(主要是为了解决内存碎片)。到Old区的标准就是在PLAB中得到的计算结果。因为Young GC会进行根扫描,所以会stop the world

白话文:处理非大对象时,如果Eden满了,会触发ygc,把存活的对象复制到Old区(要根据对象的存货次数,默认好像是15次)和另一个Survivor区

2. Mix GC混合收集(G1独有)

可以设置G1HeapWastePercent参数,在一次young GC之后,可以允许的堆垃圾百占比,超过这个值就会触发mixed GC

3. Full GC

G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发Full GC。Full GC使用的是stop the world的单线程的Serial Old模式,所以一旦触发Full GC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full GC的处理的。对于G1 GC的优化,很大的目标就是没有Full GC。

注意:G1收集器不提供Full GC处理的,所以会默认使用Serial收集器

1. 问题:为什么Full GC会这么慢?(串行收集器)

Full GC会强制采用Serial(串行收集器),是单线程的当然慢了

思考:那么其他的收集器(3个并行收集器、CMS收集器)的Full GC会有对应的处理吗?如果有的话,那应该就肯定比G1要快了

5. 如何选择合适的垃圾收集器?

1. 单CPU或者小内存

新生代使用串行收集器
老年代使用串行收集器
-XX:+UseSerialGC

2. 多CPU,要求最大吞吐量(jdk1.8默认的垃圾收集器模式)

新生代使用高并发扫收集器
老年代使用高并发扫收集器
-XX:UseParallelGC或者-XX:UseParallelOldGC

3. 多CPU,要求低停顿时间

如需快速响应如互联网应用:-XX:+UseConcMarkSweepGC
新生代使用高并发扫收集器
老年代使用CMS收集器

11. GC Roots

1. 什么是GC Roots(是引用还是对象?感觉70%概率是对象)

JVM在垃圾回收的时候会用到标记算法,在标记算法中,我们会了解到一个可达性分析算法,这个可达性分析算法的起始点就是GC Roots

白话文:GC Roots的概念是在可达性分析算法里的

2. 什么是可达性分析算法?

在标记算法中,可达性分析算法就是会标记所有能够到达GC Roots的对象(和GC Roots产生引用链,引用链就是从GC Roots点开始向下搜索, 搜索所走过的路径),剩下所有没有标记的对象(不可达)就是需要回收的对象。

3. 哪些对象可以成为GC Roots

1. 系统类加载器(就那3种)产生的对象(引用是在虚拟机栈里)

系统类加载器生成的对象是默认不能回收的,所以默认就是GC Roots,用户自定义类加载器产生的类默认是可以回收的(除非以一种特殊的方式(什么方式呢?TODO)设定成GC Roots)

2. native方法引用的对象(JNI:Java Native Interface)(引用是在本地方法栈里)

3. 静态属性引用对象(方法区)

比如静态变量(static修饰的变量),存放在运行时常量池里

4. 常量引用对象

比如常量(static final修饰的常量),存放在运行时常量池里

12. 什么是Stop The World?什么是OopMap?什么是安全点?

相关术语:STW、OopMap、安全点、安全区域、根节点枚举

1. STW(Stop The World 暂停所有的Java应用程序,除了垃圾回收器自己)

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程(所有Java代码,native代码可以执行,但不能与JVM交互),像这样的停顿

白话文:阻塞所有的线程(除了native),然后做一些全局性的操作,如GC垃圾回收等

2. OopMap(ordinary object pointer普通对象指针)

总结:程序到达安全点,oopMap记录栈上的引用类型指针

oopmap就是存放这些指针的map,OopMap 用于枚举 GC Roots,记录栈中引用数据类型(嗯?不记录基本数据类型吗?好像不需要,因为GC操作的是对象,只和栈中的引用有关)的位置。迄今为止,所有收集器(指的是GC收集器)在根节点枚举这一步骤都是必须暂停用户线程

白话文:oopMap就是一个记录栈中引用数据类型的Map

1. 场景

1. 没有OopMap的时候垃圾收集器是如何回收垃圾的

收集线程(垃圾收集器产生的守护线程)会对栈上的内存进行扫描,看看哪些位置存储了Reference引用类型)。如果发现某个位置确实存的是Reference类型,它所引用的对象这一次不能被回收

白话文:垃圾收集器会扫描整个栈,类型是引用类型的,那这个引用的对象不能被回收

问题是,栈上的本地变量表(是指本地方法栈?存的都是native方法。。。)里面只有一部分数据是Reference类型的,那些非Reference类型的数据对我们而言毫无用途,但我们还是不得不堆整个栈全部扫描一遍,这是对时间和资源的一种浪费。

白话文:垃圾收集器扫描本地方法栈收获不大(扫描全表但拿到的很少)

2. 使用OopMap,垃圾收集器是如何回收垃圾的(空间换时间)

一个很自然的想法时,能不能用空间换时间,把栈上代表的引用的位置全部记录下来,这样到真正gc的时候就可以直接读取,而不用再一点一点的扫描了,Hotspot就是实现的。它使用一种叫做OopMap的数据结构来记录这类信息。

一个线程为一个栈,一个栈由多个栈帧组成,一个栈帧对应一个方法,一个方法有多个安全点。GC发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的OopMap记录栈上哪些位置代表着引用。枚举根节点(GC Root 这是什么操作??)时,递归遍历每个栈帧的OopMap ,通过栈中记录的被引用的对象内存地址,即可找到这些对象(GC Roots)

白话文:空间换时间,GC之前,OopMap跑到安全点,记录引用,收集线程直接读取OopMap的引用,快速找到对象进行垃圾回收

2. 使用OopMap的好处(避免全栈扫描)

1. GC更快,避免全栈扫描

可以避免全栈扫描,加快枚举根节点的速度

2. GC更准确

GC更准确:可以帮助JVM实现准确式GC

3. 安全点(引用关系发生变化的点)

通过oopMap 可以快速进行GCROOT枚举,但是随着而来的又有一个问题,就是在方法执行的过程中, 可能会导致引用关系发生变化,那么保存的OopMap就要随着变化。如果每次引用关系发生了变化都要去修改OopMap的话,这又是一件成本很高的事情。所以这里就引入了安全点的概念。

不需要一发生改变就去更新这个映射表。只要这个更新在GC发生之前更新安全点。所以OopMap只需要在预先选定的一些位置上记录变化的OopMap就行了。这些特定的点就是SafePoint(安全点)。由此也可以知道,程序并不是在所有的位置上都可以进行GC的,只有在达到这样的安全点才能暂停下来进行GC。

白话文:OopMap发生改变的点,也就是引用关系发生变化的点

1. 不足之处

  • 安全点太少,会让GC等待的时间太长,太多会浪费性能
  • 安全点需要自己跑过去

2. GC的时候,安全点的选择策略(中断代码,跑到代码的下一个安全点?)

1.抢断式中断,2.主动式中断,目前都是主动式,设置一个标志,当程序运行到安全点时就去轮训该位置,发现该位置被设置为真时就自己中断挂起

白话文:跑到下一个安全点,然后立刻中断!(阻塞线程)

4. 安全区域(Sleep或Blocked的线程)

安全点的使用似乎解决了OopMap计算的效率的问题,但是这里还有一个问题。安全点需要程序自己跑过去,那么对于那些已经停在路边休息或者看风景的程序(比如那些处在Sleep或者Blocked状态的线程),他们可能并不会在很短的时间内跑到安全点去。所以这里为了解决这个问题,又引入了安全区域的概念。

安全区域很好理解,就是在程序的一段代码片段中并不会导致引用关系发生变化,也就不用去更新OopMap表了,那么在这段代码区域内任何地方进行GC都是没有问题的。这段区域就称之为安全区域。线程执行的过程中,如果进入到安全区域内,就会标志自己已经进行到安全区域了。那么虚拟机要进行GC的时候,就不会管这些已经运行到安全区域的线程。

白话文:原本就处于阻塞的线程,就是安全区域

线程要脱离安全区域的时候,要自己检查系统是否已经完成了GC或者根节点枚举(这个跟GC的算法有关系),如果完成了就继续执行,如果未完成,它就必须等待收到可以安全离开安全区域的Safe Region的信号为止。

白话文:阻塞时间到的线程会检测一下是否已经GC完毕

原文链接:我对OopMap,安全点,安全区域的理解

5. 什么是根节点枚举(可达性分析操作过程)

1. 定义

是可达性分析的操作过程,牛逼点的叫法就是根节点枚举

2. 要求

1. 需要OopMap标记

如果方法区大小就有数百兆,如果逐一检查引用,则肯定消耗性能,所以不可能这么做

2. 需要STW操作

在执行可达性分析时,必须要保证这个过程期间对象的引用关系不能再变化,否则不能保证分析结果正确性

参考原文:https://blog.csdn.net/qq_31156277/article/details/79950170

13. JVM是如何为对象分配堆内存空间的?

相关术语:指针碰撞、撞针分配、TLAB(本地缓存区)、空闲列表

在这里插入图片描述
一个对象被创建,JVM需要为对象分配堆内存空间,堆内存空间根据空间分布,可以分为以下2种:

1. 假设堆内存是完整的(连续的)

堆被指针一分为二,左边是使用,右边是未使用的,每次JVM创建一个对象,指针就会向右移动一定的距离(由对象的大小size决定)。

1. 什么是指针碰撞?

用多线程去创建对象,不加任何控制,会出现这么一种情况,JVM需要同时为对象A、B分配堆内存空间,但是只有一个指针,指针碰撞了

白话文:指针碰撞是一种现象

2. 什么叫撞针分配?(其实就是CAS操作)

通过指针碰撞的方式为对象分配内存空间,就叫撞针分配,如果遇到了撞针分配,

JVM会通过CAS的操作去处理这个问题

注意:在多线程的环境下,频繁的CAS操作也会导致效率低下,可以采用TLAB解决

3. 什么是TLAB(Thread Local Allocation Buffer)(解决指针碰撞而诞生的)

总结:TLAB是线程独享的区域

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域

白话文:类似于Java中的ThreadLocal,TLAB是线程独享的区域

如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

1. TLAB解决了什么问题?

多线程分配内存的情况下,都从主内存中分配,CAS 更新重试过于频繁导致效率低下。目前的应用,一般根据不同业务区分了不同的线程池,在这种情况下,一般每个线程分配内存的特性是比较稳定的。(这样我们就可以在Eden开辟一段只能供自己线程使用的堆空间)这里的比较稳定指的是,每次分配对象的大小,每轮 GC 分配区间内的分配对象的个数以及总大小。所以,我们可以考虑每个线程分配内存后,就将这块内存保留起来,用于下次分配,这样就不用每次从主内存中分配了。如果能估算每轮 GC 内每个线程使用的内存大小(就可以估算TLAB的百分比了),则可以提前分配好内存给线程,这样就更能提高分配效率。

白话文:多线程环境下,频繁CAS去解决碰撞指针,导致效率低下的问题(解决:别抢啦,别抢啦,人人都有)

2. TLAB的原理(start、top、end)

TLAB的本质其实是三个指针管理的区域:start,top 和 end,每个线程都会从Eden区分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。

白话文:里面有startend 是用来在Eden区划定线程私有区域的,top就是线程自己用的指针

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问(就是读取)的,只是其它线程无法在这个区域分配而已。从这一点看,它被翻译为 线程私有分配区 更为合理一点
当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

白话文:TLAB这块区域,插入操作(或者叫分配操作,分配这叫法好像更加专业哦)只能自己完成,读取操作所有线程都可以的;如果TLAB满了,会再去向Eden区要一个

4. 如何分配内存?

  1. 如果不用TLAB,通过撞针分配CAS的方式
  2. 如果用TLAB,直接在TLAB区域分配就好了,每个线程一个(私有的)

2. 假设堆内存是不完整的(不连续)

1. 堆内存为什么是不完整的?(垃圾回收,内存释放)

其实内存一般情况下都不是完整的,有内存分配自然也有内存释放。所以一般不会只靠撞针分配。一种思路是在撞针分配的基础上,加上一个空闲列表

白话文:因为垃圾回收,产生空闲内存(多思考,这里的空闲内存是内存碎片吗?应该是的,不过是大的内存碎片。如果是的话,难道说内存碎片还能重复利用?那标记-清除算法不是也还可以。。)

2. 什么是空闲列表(FreeList)

将释放的对象内存加入到FreeList

白话文:就是存放被释放的内存的列表(我想,内存不能直接存在列表吧,这里存放的应该是一种类似引用的东西)

3. 如何分配内存?

当有对象分配(不是创建呀?每次都写成了创建。。改改)的时候,优先从FreeList中寻找合适的内存大小进行分配,如果没有再在主内存撞针分配

4. JVM相关参数

  1. -XX:UseTLAB启动TLAB空间
  2. -XX:TLABWasteTargetPercent设置TLAB大小,设置TLAB空间所占用Eden空间的百分比大小默认1%

参考链接:https://www.jianshu.com/p/8be816cbb5ed

11. 什么时候会触发FullGC

1. 发生在堆内存

1. 老年代空间不足(最多情况)

大量连续存储空间的对象会直接分配到老年代、长期存活的对象晋升到老年代,老年代空间不足以存放这些对象的时候会触发Full GC。

白话文:老年代的对象积压太多导致FullGC

2. Minor GC晋升到老年代的平均大小大于老年代的剩余空间(大对象)

在发生Minor GC前,会检查老年代是否有足够的连续空间,如果当前老年代最大可用连续空间小于平均历次晋升到老年代大小,则触发Full GC。

白话文: 直接来了个超大对象(需要大量连续内存空间的对象),新生代、老年代都塞不下,直接触发FullGC

2. 发生在方法区或元空间

在这个空间发生回收操作都是:直接FullGC

1. PermGen永久代空间不足(编译器)

当系统中要加载的类、反射的类等较多时,永久代出现空间不足,在未配置为采用CMS GC的情况下会触发Full GC

白话文:永久代的堆内存大小受到JVM内存限制,当编译器加载的类太多时候,就会发生Full GC

2. Metaspace元空间不足(类的实例、类加载器、Class对象)

1. 满足以下三个条件,回触发Full GC
  • 类所有的实例都已经被回收
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class 对象没有任何地方被引用

白话文:因为元空间是操作直接内存的,所以需要的条件比较多

JVM加载.class文件的原理机制

通过类加载器实现的,然后可以讲一讲类加载器(双亲委派机制、类加载的懒汉特性)

文章参考:https://www.imooc.com/article/21124

JVM调优命令有哪些?(难,实战了)

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值