目录
3、MinorGC、Mixed GC 、FulIGC的区别是什么?
3、Young Collection + Concurrent Mark (新生代垃圾回收+并发标记)
一、JVM介绍,JVM运行流程
1、什么是JVM
Java Virtual Machine:Java程序的运行环境 (java二进制字节码的运行环境)
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收机制
2、JVM运行流程
二、什么是程序计数器?
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
对于每个方法,都会将代码转换为字节码,字节码中的行号就是代码的执行顺序,而每个线程根据自己的程序计数器来确定执行的行号
如上图,对于线程1,从第1行开始执行,执行到第10行之后切换到了线程2。线程2从第1行开始执行,执行到了第9行,然后又切换到了线程1,但是此刻线程1不是从第1行开始执行的,而是根据自己的程序计数器来确定继续从第10行往下执行。
1、概括
什么是程序计数器?
- 线程私有的,每个线程一份,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
三、详细介绍一下堆
1、介绍
Java堆是一个线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
如上图是Java7和Java8的内存结构
首先说Java8,Java8中有年轻代和老年代。年轻代分为伊甸园(Eden)和两个幸存区(Survivor )。新产生的对象就会存放在伊甸园区,当进行Minor GC的时候就会将伊甸园中的部分垃圾回收,存留下来的会保存到幸存区。当幸存区的对象年龄超过15次时候没被垃圾回收就会进入老年代。详细步骤后面会进行讲解。
方法区保存的是Class类。Java8将方法区/永久代取消了,转到了本地内存。因为Java7中的方法区/永久代如果空间设置少了,就会造成堆空间内存溢出出现OOM,空间设置大了,就会造成堆空间浪费。所以移动到了本地内存。
2、概括
你能给我详细的介绍Java堆吗?
- 线程共享的区域:主要用来保存对象实例,数组等,内存不够则抛出OutOfMemoryError异常。
- 组成:年轻代+老年代
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区
- 老年代主要保存生命周期长的对象,一般是一些老的对象
Jdk1.7和1.8的区别
- 1.7中有有一个永久代,存储的是类信息、静态变量、常量、编译后的代码。
- 1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出。
四、什么是虚拟机栈 ?
Java Virtual machine Stacks (java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出。
- 每个栈由多个栈帧(frame) 组成,对应着每次方法调用时所占用的内存,栈帧保存的是当前方法的参数、局部变量、返回地址等。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
1、垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放
2、栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
3、方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
如上图:
- 对于方法m1,它是线程安全的,因为方法没有局部变量和返回值,所以不存在被其他线程修改的情况。
- 对于方法m2,它是线程不安全的,因为方法有局部变量,存在着被其他线程修改的情况。
- 对于方法m3,它是线程不安全的,因为方法有返回值,这个返回值可能存在被其他线程修改的情况。
4、栈内存溢出情况
- 栈帧过多导致栈内存溢出,典型问题:递归调用
- 栈帧过大导致栈内存溢出
5、概括
什么是虚拟机栈?
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
垃圾回收是否涉及栈内存?
- 垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放
栈内存分配越大越好吗?
- 未必,默认的栈内存通常为1024k,栈帧过大会导致线程数变少
方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
什么情况下会导致栈内存溢出?
- 栈帧过多导致栈内存溢出,典型问题:递归调用
- 栈帧过大导致栈内存溢出
堆栈的区别是什么?
- 栈内存一般会用来存储局部变量方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会
- 栈内存是线程私有的,而堆内存是线程共有的
- 两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常
- 栈空间不足: java.lang.StackOverFlowError。
- 堆空间不足:java.lang.OutOfMemoryError。
五、请解释下方法区
1、介绍方法区
- 方法区(Method Area)是各个线程共享的内存区域。
- 主要存储类的信息、运行时常量池。
- 虚拟机启动的时候创建,关闭虚拟机时释放。
- 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace。
JDK1.8中将方法区移动到了直接内存的元空间中
2、 常量池
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
首先如左图,里面有方法的机器指令,当执行行号为0的字节码指令的时候,就会根据该行字节码#2然后到常量池表中找到#2所在的位置,然后进行翻译,这样才能执行该行的指令,知道该行指令的作用。
3、运行时常量池
常量池是*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
如上图,运行时常量池将符号改为内存地址。因为只有获得真正的内存地址才能执行代码指令
4、概括
能不能解释一下方法区?
- 方法区(Method Area)是各个线程共享的内存区域
- 主要存储类的信息、运行时常量池
- 虚拟机启动的时候创建,关闭虚拟机时释放
- 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace
介绍一下运行时常量池
- 常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名方法名、参数类型、字面量等信息
- 当类被加载,T它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
六、堆,虚拟机栈,方法区分别存储的是什么?
1、堆
堆存储的是使用new关键字创建的类(对象或者类的实例)或者数组的(含成员变量)
2、栈
栈存放的是基本类型的变量数据和对象的引用。
对象的声明引用解释:
对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中),在Java中,当我们使用new关键字创建一个对象时,这个对象会被存储在堆中,并返回一个引用,这个引用会被存储在栈中。这个引用指向了堆中的对象,我们可以通过这个引用来访问和操作这个对象 。
public class StackExample {
public static void main(String[] args) {
// 声明一个int类型的变量num,存储在栈中
int num = 10;
// 声明一个String类型的变量name,存储在栈中
String name = "Bing";
// 输出num和name的值
System.out.println(num + " " + name);
}
}
3、方法区
Java方法区(Method Area)用于存储已被虚拟机加载的类信息、常量、静态变量、动态生成的类等数据。方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
七、请解释直接内存
直接内存:并不属于JM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见于 NIO 操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高。
Java代码完成文件拷贝
1、常规IO数据拷贝流程
java本身不具备磁盘的读写功能,如果调用磁盘读写,必须调用操作系统所提供的函数。
当调用操作系统所提供的IO方法的时候,就会设计CPU的状态切换,首先从用户态切换到内核态,当切换到内核态的时候,就通过系统提供的函数读取磁盘中的文件,读取的文件会分批次保存在操作系统内存划出的一片系统缓存区。但是系统缓存区java无法去获取,所以java会在堆中分配一片内存名为java缓冲区,将系统缓存区读到java缓冲区中。
读到java缓冲区后CPU切换到用户态,通过输出流写出操作,反复读取,然后将文件读到目标位置。
但是有个问题就是两个缓冲区会对性能产生巨大的影响
2、NIO数据拷贝过程
NIO不在使用两个缓冲区,而是直接使用直接内存来代替缓冲区,因此性能大幅提高
3、概括
你听过直接内存吗?
- 并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存
- 常见于 NIO操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受JVM内存回收管理
八、什么是类加载器,什么是双亲委派?
1、什么是类加载器?
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
2、类加载器有哪些?
3、概括
什么是类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来
类加载器有哪些
- 启动类加载器(BootStrap ClassLoader):加载JAVA HOME/ire/lib目录下的库
- 扩展类加载器(ExtClassLoader):主要加载JAVA HOME/ire/lib/ext目录中的类
- 应用类加载器(AppClassLoader):用于加载classPath下的类
- 自定义类加载器(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。
4、什么是双亲委派模型?
双亲委派:加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类。
比如自己自定义的student类,就会首先去它的上一级加载器,扩展类加载器去加载。然后扩展类还有上一级,就去启动类加载器去加载这个类。但是启动类加载器没有上一级了,这时候启动类加载器无法加载,就去下一级的扩展类加载器加载,扩展器加载器加载不了最后到应用类加载器加载。
如果是String类,那么就会去上一级扩展类加载器尝试加载,再去启动类加载器加载。发现启动类加载器可以加载,就在启动类加载器进行加载。然后直接返回给应用类加载器让它使用即可。
5、 双亲委派机制作用
- 保证唯一性:通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
- 保证类库安全:为了安全,保证类库API不会被修改
因为类加载器的加载顺序是 Bootstrap ClassLoader------>Extension ClassLoader--------->Application ClassLoader。因此当写了一个与JRE同名的类的时候,就会向上递归进行类加载。并且系统核心类和扩展类会在应用类加载前完成加载。所以当书写一个String类的时候,启动类加载器会判断该String类已经加载过了,所以不会在应用类加载器进行加载。这样保证了唯一性和安全性。
6、概括
什么是双亲委派模型?
- 加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
JVM为什么采用双亲委派机制?
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
- 为了安全,保证类库API不会被修改
九、类装载的执行过程
1、加载
通过类的全名,获取类的二进制数据流。
解析类的二进制数据流为方法区内的数据结构(Java类模型)
创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
如上图,类的信息保存在方法区中,方法区保存着类的类型,构造器,方法等类的结构信息。
如上图,目前Person创建了两个对象保存在堆中,分别name为张三和李四。
当方法区要创建两个对象,首先在堆中创建Person类的两个实例,然后在方法区中根据其类的信息创造其数据结构,然后对其进行堆中两个对象的地址进行引用,这样才可以加载成功,就可以使用这个对象了。
2、验证
验证类是否符合 JVM规范,安全性检查
其中验证有四项验证:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
文件格式验证、元数据验证、字节码验证是格式检查,如:文件格式是否错误、语法是否错误、字节码是否合规。
符号引用验证:Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,检查它们是否存在
3、准备
为类变量分配内存并设置类变量初始值
static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成static变量是
final的引用类型,那么赋值也会在初始化阶段完成
如上图是已经完成赋值的操作
4、解析
把类中的符号引用转换为直接引用
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
如上图,类中的字节码文件右边#对应着常量池中的符号指令。解析就是将常量池中的符号引用转变为找到真正要执行的操作
如上图,字节码文件根据#4找到常量池表#4所对应的位置,并且该位置有着#24,#25两个符号引用,那么继续查表寻找
如上图是#24和#25在常量池的位置,它后面也有#31#32#33的符号位置,那么继续查找,直到最终找到要调用的指令。这就是解析,将符号引用转为直接引用,可以直接调用的指令的位置。
5、初始化
对类的静态变量,静态代码块执行初始化操作
- 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类
- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
如上图所示代码,有静态变量和静态代码块,并且有子类继承父类
- 首次访问这个类的静态变量或静态方法时,那么就会初始化这个类的静态变量或者静态方法。
- 子类初始化,如果文类还没初始化,会引发父类先初始化。
- 子类访问父类静态变量,只触发父类初始化。
6、使用
JVM 开始从入口方法开始执行用户的程序代码
- 调用静态类成员信息(比如:静态字段、静态方法)
- 使用new关键字为其创建对象实例
7、卸载
当用户代码执行完成,开始销毁Class对象
8、概括
说一下类装载的执行过程?
- 加载:查找和导入class文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:JVM 开始从入口方法开始执行用户的程序代码卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象
十、对象什么时候可以被垃圾回收?
简单一句就是,如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
1、引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
如上图,栈中对象a对象b都对Demo类有引用,所以引用计数法+1为1。然后出现了a.instance=b和b.instance=a,所以引用计数法+1为2。当执行到了a=null和b=null的时候,各自类的计数法-1为1。
如上图,对象a和对象b都对该类没有引用,但是出现了各自引用,所以会出现对象之间相互循环引用的问题,会出现内存泄漏。
内存泄漏是指程序中已经不再使用的内存没有得到及时释放,导致系统内存的浪费。在Java中,内存泄漏通常是由于程序中存在一些不合理的对象引用导致的,例如:
- 对象被无意中保留。
- 对象被无意中多次引用。
- 对象被无意中存储在集合中,但没有从集合中删除。
2、可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾
X,Y这两个节点是可回收的
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到,表示可以回收
3、哪些对象可以作为 GC Root ?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的 Native 方法)引用的对象
4、概括
对象什么时候可以被垃圾器回收
- 如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾如果定位了垃圾,则有可能会被垃圾回收器回收
定位垃圾的方式有两种
- 引用计数法
- 可达性分析算法
十一、JVM垃圾回收算法有哪些?
1、标记清楚算法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除
- 根据可达性分析算法得出的垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
如上图,根据可达性算法,将内存空间中可达的对象标记出来,未标记的对象进行GC。如右图未清理完后的情况。
优点:
- 标记清楚速度很快
缺点:
- 造成内存空间碎片,导致内存不连续
2、标记整理算法
标记整理算法,是将垃圾回收分为3个阶段
- 根据可达性分析算法得出的垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
- 对未回收的对象进行整理,使其放置到连续的内存空间中
优点:
- 解决了内存空间碎片的问题
缺点:
- 多了一步整理步骤,对效率有一定的影响
3、标记复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
优点:
- 垃圾对象较多的时候效率高
- 清理后无内存碎片
缺点:
- 内存空间浪费,每次只能使用一半的内存空间
4、概括
JVM 垃圾回收算法有哪些?
- 标记清除算法: 垃圾回收分为2个阶段,分别是标记和清除,效率高,有磁盘碎片,内存不连续
- 标记整理算法:标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
- 复制算法:将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低
十二、JVM的分代回收
1、分代回收的结构划分
在java8时,堆被分为了两份: 新生代和老年代[1: 2]
对于新生代,内部又被分为了三个区域:
- 伊甸园区Eden,新生的对象都分配到这里
- 幸存者区survivor(分成from和to)
- Eden区,from区,to区为8:1:1
2、分代回收的工作机制
如上图。新建的对象都会放在Eden区
当Eden区满的时候,发生MinorGC,根据可达性算法进行标记,然后清空未标记的对象,清理后将未清空的对象保存在to区
当Eden区又满了的时候
根据可达性算法,将保留的对象从Eden区和to区保存到from区
当from区或者to区的对象保留次数超过15次的时候,就会将其移动到老年区
如上图,A已经保留15次,下次GC的时候,已经是第16次了,将A移动到Old区,from区和to区交换
流程:
- 新创建的对象,都会先分配到eden区。
- 当伊甸园内存不足,标记伊园与 from (现阶段没有)的存活对象。
- 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放。
- 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将存活的对象复制到from区。
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)。
3、MinorGC、Mixed GC 、FulIGC的区别是什么?
- MinorGC[young GC]:发生在新生代的垃圾回收,暂停时间短(STW)
- Mixed GC:新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
- FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长(STW),应尽力避免
STW (Stop-The-World): 停所有应用程序线程,等待垃圾回收的完成
4、为什么分代回收不设置一个Survivor区,而是两个Survivor区?
因为Survivor存放的时候存活时间比较长的对象,但是也会出现垃圾回收的情况造成空间碎片。为了解决这种情况就会使用两个Survivor区,将Eden区和一个Survivor区存活的对象放到另一个Survivor区。这样就可以解决了这种情况。
5、为什么分代回收需要将Eden区和一个Survivor区存活的对象放到另一个Survivor区?
为了解决空间碎片的问题,原因同上。
4、概括
说一下JVM中的分代回收
- 堆的区域划分
- 堆被分为了两份: 新生代和老年代[1:2]
- 对于新生代,内部又被分为了三个区域。Eden区,幸存者区survivor(分成from和to)[8:1:1]
- 对象回收分代回收策略
- 新创建的对象,都会先分配到eden区
- 当伊甸园内存不足,标记伊园与 from (现阶段没有)的存活对象
- 将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和 from 内存都得到释放
- 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将其复制到from区
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
- MinorGC、 Mixed GC、 FullG的区别是什么
- MinorGC[young GC]:发生在新生代的垃圾回收,暂停时间短(STW)
- Mixed GC:新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
- FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长(STW),应尽力避免
十三、说一下JVM 有哪些垃圾回收器?
在jvm中,实现了多种垃圾收集器,包括:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS(并发垃圾收集器)
- G1垃圾收集器
1、 串行垃圾收集器
Serial和Serial old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
- Serial作用于新生代,采用复制算法
- SerialOld 作用于老年代,采用标记-整理算法
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
如上图,刚开始有多个线程在执行,当该线程发生GC的时候,该垃圾回收器就会停止其他线程,让垃圾回收线程运行,当垃圾回收完成以后,终止该线程的垃圾回收线程,让所有线程继续运行
2、并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
- ParallelNew作用于新生代,采用复制算法
- ParallelOld作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
如上图,刚开始有多个线程在执行,当该线程发生GC的时候,该垃圾回收器就会停止其他线程,让所有线程开启垃圾回收线程,当垃圾回收完成以后,终止所有线程的垃圾回收线程,让所有线程继续运行。相较于串行垃圾回收器,性能提高了很多。
3、CMS(并发垃圾收集器)
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行
如上图,标记分为三次,分别为初始标记,并发标记,重新标记,其中初始标记是标记GC Roots直接关联的对象,并发标记标记的是与GC Roots直接关联的对象相关联的对象。如GC Roots——>A——>B,那么标记的就是B对象。重新标记是标记那些在并发标记过程中可能产生关联变化的对象。当全部标记完成后就进入并发清理,这个过程不会停止其他对象。
如上图,初始标记的是与GC Roots直接关联的对象A,并发标记标记的是与A对象相关联的对象B、C、D。之所以有重新标记,是因为在并发标记阶段其他线程正在运行,可能会出现刚开始的X没有与A相关联,在并发标记阶段后期A与X对象又有了关联这种情况,所以就出现了重新标记,这个阶段会处于STW阶段,所有线程被阻塞。
或者出现刚开始D对象和B对象有关联,到了重新标记阶段B对象和D对象没有关联了这种情况都有可能发生。
最后进行并发清除,这个不会阻塞其他线程,保证性能。
十四、G1垃圾回收器 (G one)
1、G1垃圾回收器
- 应用于新生代和老年代,在JDK9之后默认使用G1。
- 划分成多个区域,每个区域都可以充当Eden,Survivor,Old,Humongous,其中 Humongous 专为大对象准备采用复制算法。
- 响应时间与吞吐量兼顾。
- 分成三个阶段:新生代回收、并发标记、混合收集。
- 如果并发失败(即回收速度赶不上创建新对象速度)会触发 FulI GC 。
2、Young Collection(新生代垃圾回收)
- 初始时,所有区域都处于空闲状态
- 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
- 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
刚开始的时候,堆空间是空白的。堆空间被划分大小相等的区域。
当创建对象的时候,就会在堆中挑出一些空闲的区域作为Eden区域。上图中,在堆中标出了3个E代表创建了三个对象在Eden区。G1垃圾回收器中新生代的内存占比不是固定的而是波动的,大概为5%-6%之间,真实大小G1垃圾回收器在5%-6%之间自动调整。
因此当超出上限的时候就会进行新生代的垃圾回收。
如上图,G1垃圾回收器的新生代垃圾回收会根据可达性算法将垃圾对象标记出来,保留下的对象根据标记复制算法移动到Survivor区。
因此其他空间的内存就会被释放掉。注意在标记的过程中和垃圾清理的过程中要进行STW,由于新生代的垃圾比较少,所以暂停的时间很短。
随着时间的流逝,Eden区的垃圾又充满了,这时候再次进行新生代垃圾回收,将保存下的对象保存到Survivor区中。
Survivor区域中有年龄过大的对象,那么就会晋升到Old区。比如S区里面的所有对象都超过了15岁,那么就会全部晋升的Old区,并把Survivor区域的内存释放。
如上图,将Survivor区域的垃圾回收到Old区域。
3、Young Collection + Concurrent Mark (新生代垃圾回收+并发标记)
当老年代占用内存超过闽值(默认是45%)后,触发并发标记,这时无需暂停用户线程
并发标记阶段之后,会有重新标记阶段,这个阶段需要暂停用户线程,这个阶段为了解决漏标问题。
4、Mixed Collection(混合垃圾回收)
这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少或垃圾对象多)的区域 (这也是Gabage First 名称的由来)
这个阶段是混合阶段,意味着不仅对Old区进行垃圾回收,还会对Eden区和Survivor区进行垃圾回收。
如上图,新增了两个区域,分别为Survivor区域和Old区域
对于混合收集,会将Eden区和Survivor区的存活对象复制到新的Survivor区域。因为Survivor中from区和to区总有一个区域是空的,所以复制算法会交换from区和to区,并将Eden存活的对象复制到Survivor区域中。
Survivor区域可能会有超出15岁年龄的对象,这些高龄对象和对其他Old区域进行GC存留下的对象都保存到新创建的Old区。
但是一般会进行多次混合收集,将剩下的Old区进行重新标记,因为有预期时间的限制,无法最大化的处理老年代的垃圾回收。G1垃圾回收器会执行多次混合收集的原因是为了尽可能地减少Full GC的次数,从而减少应用程序的停顿时间。
当所有混合收集完成以后,进入下一轮的新生代回收、并发标记、混合收集。
当老年代新生对象的速度大于垃圾回收的速度时,会导致老年代区域被填满,从而触发一次Full GC,Full GC的垃圾回收会很长。
当一个对象很大的时候,就会存储到一个巨型对象中,如果一个区域不够的话就会分配一块连续的区域用来保存该对象。如果没有连续的区域来存储巨型对象,JVM会将该对象直接分配到老年代。如果连老年代都没有足够的空间来存储巨型对象,那么就会抛出OutOfMemoryError。在这种情况下,JVM会尝试执行Full GC,以释放内存并为巨型对象腾出空间。
十五、 强引用、软引用、弱用、虚引用的区别
1、强引用
只有所有GCRoots 对象都不通过强引用引用该对象,该对象才能被垃圾回收
如上图,这个User对象无法被垃圾回收
2、软引用
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收
如上图,当开始进行垃圾回收的时候,不会对User对象进行垃圾回收。当垃圾回收之后仍然内存不够那么就会触发对User对象的垃圾回收。
3、弱引用
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
如上图,该User对象是一个弱引用对象。当触发垃圾回收的时候,不管是否内存不够,都会对User对象进行垃圾回收
4、虚引用
必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被回收以后,提醒进行下一步某些事情的操作。
如上图,比如当User1和User2被垃圾回收以后,就会将虚引用对象放入引用队列。当加入引用队列后,就会配合Reference Handler来释放虚引用对象关联的一些外部资源。比如说User1和User2只是占用Java虚拟机中的内存,但是User1和User2还关联着外部资源,如直接内存。那么就需要将垃圾回收后的虚引用对象放入引用队列,Reference Handler用来监测引用队列,看哪些对象被回收就直接找引用队列就可以了。当Reference Handler发现引用队列有X和Y对象,那么就会释放与X和Y外部相关联的资源。
5、概括
强引用、软引用、弱引用、虚引用的区别?
- 强引用:只要所有 GC Roots 能找到,就不会被回收。
- 软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够的时候会回收软引用对象。
- 弱引用: 需要配合WeakReference使用,只要进行了垃圾回收,就会把弱引用对象回收。
- 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队由Reference Handler 线程调用虚引用相关方法释放直接内存。
十六、 JVM 优的参数可以在哪里设置参数值
1、war包部署在tomcat中设置
修改TOMCAT HOME/bin/catalina.sh文件
![](https://img-blog.csdnimg.cn/1a9b5f9172d94f1081a345111a04b532.png)
2、jar包部署在启动参数设置
通常在linux系统下直接加参数启动springboot项目
- nohup : 用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行参数
- &:让命令在后台执行,终端退出后命令仍旧执行
十七、JVM 调优的参数都有哪些?
主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型
1、设置堆空间大小
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值。
不指定单位默认为字节指定单位,按照指定的单位设置
堆空间设置多少合适?
- 最大大小的默认值是物理内存的1/4,初始大小是物理内存的1/64。
- 堆太小,可能会频繁的导致年轻代和老年代的垃圾回收,会产生STW,暂停用户线程。
- 堆内存大肯定是好的,存在风险,假如发生了fullgc,它会扫描整个堆空间,暂停用户线程的时间长。
- 设置参考推荐:尽量大,也要考察一下当前计算机其他程序的内存使用情况 。
2、虚拟机栈的设置
每个线程默认会开启1M的内存,用于存放栈、调用参数、局部变量等,但一般256K就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
3、年轻代中Eden区和两个Survivor区的大小比例
设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。通过增大Eden区的大小来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。
如果改为3代表survivor:eden=2:3
4、年轻代晋升老年代阈值
默认为15
取值范围0-15
5、设置垃圾回收收集器
通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器
十八、JVM调优的工具
命令工具
- jps:进程状态信息
- jstack:查看java进程内线程的堆栈信息
- jmap:查看堆转信息
- jhat:堆转储快照分析工具
- jstat:JVM统计监测工具
可视化工具
- jconsole:用于对ivm的内存,线程,类的监控。
- VisualVM:能够监控线程,内存情况
1、jps:进程状态信息
jsp能够查看当前正在运行的进程,前面的数字为进程ID
2、jstack:查看java进程内线程的堆栈信息
在该进程的所有进程中,可以看到创建的3个线程详细信息
3、jmap:查看堆转信息
用于生成堆转内存快照、内存使用情况
- format=b表示以hprof二进制格式转储Java堆的内存
- file=<filename>用于指定快照dump文件的文件名
如上图,可以看到里面有当前堆的所有情况信息,包括设置的参数,初始化大小,当前剩余大小
如上图,可以看到里面的新生代的survivor区和eden区的使用情况,以及老年代的使用情况。
如上图 使用jmap -dump功能,file文件后面是设置的路径,最后面的数字是该进程的端口号。
4、jhat:堆转储快照分析工具
是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等.
- SO:survivor区
- S1:survivor区
- E:eden区
- O:old区
- M:元空间区
- CCS:压缩信息
5、jconsole:用于对ivm的内存,线程,类的监控。
用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能监控工具
打开方式:java安装目录 bin目录下直接启动jconsole.exe 就行
6、VisualVM:能够监控线程,内存情况
能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下直接启动ivisualvm.exe就行
十九、Java内存泄露的排查思路?
- 内存泄露:指程序申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存分配给其他程序使用。一般都是对象创建过多
- 内存溢出:指程序申请内存时,没有足够的内存提供给申请者使用,导致无法正常存储到内存中。一般都是大对象大文件
1、获取堆内存快照dump
Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中
使用jmap命令获取运行中程序的dump文件
当项目启动起来了,但是项目里的部分模块没有启动起来,那么这个命令就无法查询这个未启动的模块的信息
该命令只能查看已经运行的进程,因此可以使用下面方法查看未启动的进程详细信息
使用vm参数获取dump文件
import java.util.ArrayList;
import java.util.List;
public class Test2 {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<byte[]>();
for (int i = 0; i < 1000000; i++) {
byte[] bytes = new byte[1024 * 1024];
list.add(bytes);
}
}
}
JVM的参数设置:
-Xmx10m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\桌面\测试文件
报错:
可以看到保存的文件:
2、VisualVM去分析dump文件
打开VisualVM
选择装入,找到hprof文件
如图:
这里显示的出现OOM
3、通过查看堆信息的情况,定位内存溢出问题
点击main,就可以看到错误的位置