JVM面试篇
一、JVM组成
1、JVM由那些部分组成,运行流程是什么?
1.1JVM是什么
Java Virtual Machine(java虚拟机) Java程序的运行环境(java二进制字节码的运行环境)
好处: 一次编写,到处运行,自动内存管理,垃圾回收机制。
1.2JVM由哪些部分组成,运行流程是什么?
从图中可以看出 JVM 的主要组成部分
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区,内存分区)
- Execution Engine(执行引擎)
- Native Method Library(本地库接口)
运行流程:
(1)类加载器(ClassLoader)把Java代码转换为字节码
(2)运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行
(3)执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。
2、什么是程序计数器?
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
解释: java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间。在任何的一个时间点上,一个处理器只会处理执行一个线程,如果当前被执行的这个线程它所分配的执行时间用完了【挂起】。处理器会切换到另外的一个线程上来进行执行。并且这个线程的执行时间用完了,接着处理器就会又来执行被挂起的这个线程。
如图: 线程1执行到第10行,cpu切换到线程2,执行到第九行,又切换回来,JVM不会再次从第一行执行,而是根据PC记录的第10行开始。
结论: PC是线程私有的,是JVM规范中唯一一个没有规定出现OOM的区域,所以这个空间也不会进行GC。
3、你能给我详细的介绍Java堆吗?
线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError(OOM)内存溢出异常。
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
- 老年代主要保存生命周期长的对象,一般是一些老的对象。
- 元空间保存的类信息、静态变量、常量、编译后的代码。
为了避免方法区出现OOM,所以在java8中将堆上的方法区【永久代】给移动到了本地内存上,重新开辟了一块空间,叫做元空间。那么现在就可以避免OOM的出现了。
3.2元空间(MetaSpace)介绍
在 HotSpot JVM 中,永久代( ≈ 方法区)中用于存放类和方法的元数据以及常量池,比如Class 和 Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。
永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即OutOfMemoryError,为此不得不对虚拟机做调优。
那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?
官网给出了解释:官网
1)由于 PermGen(永久代) 内存经常会溢出,引发OutOfMemoryError,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM。
2)移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
总结: 元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
4、4.1什么是虚拟机栈?
Java Virtual machine Stacks (java 虚拟机栈)
每个线程运行时所需要的内存,称为虚拟机栈,先进后出。
每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
4.2垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放。
4.3栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k(1M),栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半。
4.4方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
比如以下代码:
StringBuilder:在m1中创建并且没有返回,则是线程安全的。
在m2中是通过引用外界的,所以不安全。其他线程也可以修改,则线程不安全。
在m3中会将对象返回,也会被其他线程修改,则线程不安全。
4.5栈内存溢出情况
栈帧过多导致栈内存溢出,典型问题:递归调用
栈帧过大导致栈内存溢出(一般不出现)。
4.6栈和堆的区别
- 栈内存一会存储局部变量和方法调用,但堆内存用来存储Java对象和数组的。堆会GC垃圾回收,而栈不会。
- 栈内存是线程私有的,而堆内存是线程共享的。
- 异常不同,内存不足都会抛出异常,但不同。
栈空间不足:Java.lang.StackOverFlowError
堆空间不足:Java.lang.OutOfMemoryError
5、 能不能解释一下方法区(元空间)?
5.1 概述
- 方法区(Method Area)是各个线程共享的内存区域
- 主要存储类的信息、、、运行时常量池
- 虚拟机启动的时候创建,关闭虚拟机时释放
- 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace
元空间大小默认没有上限(受限于物理内存),但是可以手动设置VM:-XX:MaxMetaspaceSize=8m
会提示:MaxMetaspaceSize is too small
5.2 常量池
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息,查看字节码结构(类的基本信息、常量池、方法定义)
javap -v xx.class
比如下面是一个Application类的main方法执行,源码如下:
public class Application {
public static void main(String[] args) {
System.out.println("hello world");
}
}
main方法按照指令执行的时候,需要到常量池中查表翻译找到具体的类和方法地址去执行。
5.3 运行时常量池
常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
6 、你听过直接内存吗?
不受 JVM 内存回收管理,是虚拟机的系统内存(操作系统),常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理。
比如:需求,在本地电脑中的一个较大的文件(超过100m)从一个磁盘挪到另外一个磁盘。
代码:
public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) /
1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) /
1000_000.0);
}
可以发现,使用传统的IO的时间要比NIO操作的时间长了很多了,也就说NIO的读性能更好。
这个是跟我们的JVM的直接内存是有一定关系,如下图,是传统阻塞IO的数据传输流程:
在内核态需要将系统内存的缓冲区copy一份到java堆内存,再进行读写操作。
下图是NIO传输数据的流程,在这个里面主要使用到了一个直接内存,不需要在堆中开辟空间进行数据的拷贝,jvm可以直接操作直接内存,从而使数据读写传输更快。
详解:参考快速了解 直接内存
二、类加载器
1 、什么是类加载器,类加载器有哪些?
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。现有的类加载器基本上都是java.lang.ClassLoader的子类,该类的只要职责就是用于将指定的类找到或生成对应的字节码文件,同时类加载器还会负责加载程序所需要的资源。
类加载器种类
类加载器根据各自加载范围的不同,划分为四种类加载器:
启动类加载器(BootStrap ClassLoader):
该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。
扩展类加载器(ExtClassLoader):
该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
应用类加载器(AppClassLoader):
该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。比如Student类。
自定义类加载器:(用的很少)
开发者自定义类继承ClassLoader,实现自定义类加载规则。
示意图:
类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
2、 什么是双亲委派模型?
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。
那问题来了:为啥JVM采用双亲委派模型? 其实上面说到了关键的一点。
(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
(2)为了安全,保证类库API不会被修改。
比如:
public class String {
public static void main(String[] args) {
System.out.println("hello");
}
}
根本执行不了的,因为String被我们修改了。
出现该信息是因为由双亲委派的机制,java.lang.String的在启动类加载器(Bootstrap classLoader)得到加载,因为在核心jre库中有其相同名字的类文件,但该类中并没有main方法。这样就能防止恶意篡改核心API库。
3、说一下类装载的执行过程?
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
3.1 加载
- 通过类的全名,获取类的二进制数据流。
- 解析类的二进制数据流为方法区内的数据结构(Java类模型)
- 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
3.2 验证
验证类是否符合JVM规范,安全性检查
(1)文件格式验证:是否符合Class文件的规范。
(2)元数据验证
这个类是否有父类(除了Object这个类之外,其余的类都应该有父类)
这个类是否继承(extends)了被final修饰过的类(被final修饰过的类表示类不能被继承)类中的字段、方法是否与父类产生矛盾。(被final修饰过的方法或字段是不能覆盖的)
(3)字节码验证
主要的目的是通过对数据流和控制流的分析,确定程序语义是合法的、符合逻辑的。
(4)符号引用验证:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量。
比如:int i = 3; 字面量:3 ;符号引用:i
3.3准备
为类变量分配内存并设置类变量初始值
static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成。
static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成。
static变量是final的引用类型,那么赋值也会在初始化阶段完成。
3.4解析
把类中的符号引用转换为直接引用
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
3.5.初始化
对类的静态变量,静态代码块执行初始化操作
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
注意:如果使用子类调用父类的静态变量,会初始化父类代码块但是子类是不会初始化的。
3.6.使用
JVM 开始从入口方法开始执行用户的程序代码
- 调用静态类成员信息(比如:静态字段、静态方法)
- 使用new关键字为其创建对象实例
3.7卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
三、垃圾回收
1、 简述Java垃圾回收机制?(GC是什么?为什么要GC)
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉GC(Garbage Collection)。
有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。
在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机,换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。
2、 对象什么时候可以被垃圾器回收
垃圾回收主要是针对堆中的内存
简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法。
2.1 引用计数法
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收。
String demo = null;
致命缺点:当对象间出现了循环引用的话,则引用计数法就会失效。
前四行使得ab存在循环引用,虽然执行a=null,b=null,但是循环引用还存在,永远不会回收,最终可能导致内存溢出。
优点:
- 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
- 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报OOM错误。
- 区域性, 更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:
- 每次对象被引用时,都需要去更新计数器,有一点时间开销。
- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
- 无法解决循环引用问题,会引发内存泄露。(最大的缺点)
2.2 可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。
根对象是那些肯定不能当做垃圾回收的对象,就可以当做根对象
如:局部变量,静态方法,静态变量,类信息(一般都是栈,方法区中的东西)
GC ROOTS:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
/*
demo是栈帧中的本地变量,当 demo = null 时,由于此时 demo 充当了 GCRoot
的作用,demo与原来指向的实例 new Demo() 断开了连接,对象被回收。
*/
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo = null;
}
}
- 方法区中类静态属性引用的对象
/**
* 当栈帧中的本地变量 b = null 时,由于 b 原来指向的对象与 GC Root (变量
b) 断开了连接,所以 b 原来指向的对象会被回收,而由于我们给 a 赋值了变量的
引用,a在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存
活!
*/
public class Demo {
public static Demo a;
public static void main(String[] args) {
Demo b = new Demo();
b.a = new Demo();
b = null;
}
}
- 方法区中常量引用的对象
/**
* 常量 a 指向的对象并不会因为 demo 指向的对象被回收而回收
*/
public class Demo {
public static final Demo a = new Demo();
public static void main(String[] args) {
Demo demo = new Demo();
demo = null;
}
}
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
核心是:判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收。
X,Y这两个节点是可回收的,但是并不会马上的被回收!! 对象中存在一个方法【finalize】。当对象被标记为可回收后,当发生GC时,首先会判断这个对象是否执行了finalize方法,如果这个方法还没有被执行的话,那么就会先来执行这个方法,接着在这个方法执行中,可以设置当前这个对象与GC ROOTS产生关联,那么这个方法执行完成之后,GC会再次判断对象是否可达,如果仍然不可达,则会进行回收,如果可达了,则不会进行回收。
finalize方法对于每一个对象来说,只会执行一次。如果第一次执行这个方法的时候,设置了当前对象与RC ROOTS关联,那么这一次不会进行回收。 那么等到这个对象第二次被标记为可回收时,那么该对象的finalize方法就不会再次执行了。
3、JVM 垃圾回收算法有哪些?
3.1 标记清除算法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
1.根据可达性分析算法得出的垃圾进行标记
2.对这些标记为可回收的内容进行垃圾回收
可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。
缺点:
- 效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
- 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。(重要)
3.2 标记整理(压缩)算法(标记清除算法的优化)
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题。
步骤:
1)标记垃圾。
2)需要清除向右边走,不需要清除的向左边走。
3)清除边界以外的垃圾。
优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。
3.3复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
使用场景: 如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。
1)将内存区域分成两部分,每次操作其中一个。
2)当进行垃圾回收时,将正在使用的内存区域中的存活对象移动到未使用的内存区域。当移动完对这部分内存区域一次性清除。
3)周而复始。
优点:
- 在垃圾对象多的情况下,效率较高
- 清理后,内存无碎片
缺点:
- 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低。
与标记整理算法对比: 复制算法标记完就复制,但标记整理算法得等把所有存活对象都标记完毕,再进行整理。
4、分代收集算法
4.1 概述
在java8时,堆被分为了两份:新生代和老年代【1:2】,在java7时,还存在一个永久代。
- 对于新生代,内部又被分为了三个区域。Eden区,S0区,S1区【8:1:1】
- 当对新生代产生GC:MinorGC【young GC】
- 当对老年代代产生GC:Major GC
- 当对新生代和老年代产生FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
4.2工作机制
此时对象1存活下来了,被标记复制到from区。
又Eden区满了,将from中的幸存者(假设A)和Eden区幸存者(假设w)复制到to区中其他内存回收。
4.3MinorGC、Mixed GC、FullGC的区别是什么
MinorGC【young GC】发生在新生代的垃圾回收,暂停时间(STW)
Mixed GC :新生代 + 老年代部分区域的垃圾回收,G1 收集器特有(后续会详细介绍 )
FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长(STW),应尽力避免。
STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成
5、说一下 JVM 有哪些垃圾回收器?
在jvm中,实现了多种垃圾收集器,包括:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS(并发)垃圾收集器
- G1垃圾收集器
5.1 串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑。
- Serial 作用于新生代,采用复制算法
- Serial Old 作用于老年代,采用标记-整理算法
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
5.2 并行垃圾收集器(串行的多线程模式)
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器。
- Parallel New作用于新生代,采用复制算法
- Parallel Old作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
5.2 CMS(并发)垃圾收集器
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。
这里涉及到重复标记:如图:假设在初始标记以及后面的并发标记的时候, X内存是未被引用的,也就是还没有被A引用,但是重新标记的时候,发现A引用了X,这导致后续的并发清理不会清理X内存(注意:只有重新标记完成后才会清理,前面的标记是不会清理的)
6 详细聊一下G1垃圾回收器
6.1 概述
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾(并发)
- 分成三个阶段:新生代回收、并发标记、混合收集 ,如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
演示一下执行流程:
6.2 Young Collection(年轻代垃圾回收)
刚开始所有区域都是空闲的,然后创建新对象,其流程和之前介绍的分代回收算法中的MinorGC【young GC】是一样的,只不过区域都一样大。Eden满了利用复制算法复制幸存对象到幸存区。
反复以上操作,Eden区又满了,这个时候就将Eden区的幸存者和之前的幸存区的幸存者复制到新的幸存区中S,其中较老对象晋升至老年代O
6.3 Young Collection + Concurrent Mark (年轻代垃圾回收+并发标记)
当老年代占用内存超过阈值(默认是45%)后,触发并发标记,这时无需暂停用户线程。
- 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。
- 这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间(STW)目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。(暗红色为被标记区域)
这里的STW是可以设置的,所以优先回收价值高的
6.4 Mixed Collection (混合垃圾回收)
续上图:开始混合回收,Eden区和S区和之前同理,老年区则根据复制算法将幸存者复制到新的老年区中
复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集,一直重复。
这个时候如果对象非常大,会开辟一块连续的空间存储对象 称之为humongous区(H)
注意:
再完成多次以上流程后还可能发生一种情况:回收速度赶不上对象创建速度,会触发 Full GC。
可以参考大佬对FullGC的理解:什么是FullGC
对于Full GC
Full GC(完全垃圾回收)是 Java 虚拟机(JVM)中一种垃圾回收的过程。它是对整个堆内存进行垃圾回收的操作,涉及到年轻代(Young Generation)和老年代(Old Generation)中的所有对象。以下是一些关于 Full GC 的关键点:
触发条件:Full GC 通常在以下情况中被触发:
- 老年代内存不足,需要回收更多空间。
- 系统内存不足,需进行回收以释放空间。
- 显示调用 System.gc() 方法。
影响:Full GC 会暂停应用程序的所有线程,这可能导致显著的停顿时间,因为它涉及到整个堆内存的检查和清理。频繁的 Full GC 会影响应用程序的性能和响应时间。
目的: 其主要目的是释放内存中的无用对象,减少内存占用,从而提高应用程序的运行效率。
优化: 为了减少 Full GC 的频率和影响,可以采取以下措施:
- 调整 JVM 的垃圾回收器参数,选择合适的垃圾回收策略(如 G1、CMS、ZGC 等)。
- 优化内存使用,减少对象创建和持有。
- 监控和分析内存使用情况,识别并解决内存泄漏问题。
总之,Full GC 是垃圾回收中的重要组成部分,但其频繁发生可能会影响应用性能,因此需要通过合理的配置和优化来减少其对系统的影响。
7 强引用、软引用、弱引用、虚引用的区别?
7.1 强引用
其实就是我们之前(可达性分析算法)判断该对象是否为垃圾中 GC Root指向对象的这个引用称之为强引用,GC是不会清理的。
7.2 软引用
软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收。
一开始内存充足的时候GC扫描到了该引用是不会回收的,但是当内存出现不足的时候就会将其回收。
7.3 弱引用
弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
这里不得不提起之前JUC说的threadLocal内存泄漏问题
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v; //强引用,不会被回收
}
}
Entry 的key是当前ThreadLocal,value值是我们要设置的数据。
WeakReference 表示的是弱引用,当JVM进行GC时,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。但是 value 是强引用,它不会被回收掉。这个时候就会导致key为null,但是value有值,造成内存泄漏。(及时使用remove方法)
7.4 虚引用
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存。
四、JVM实践
1、 JVM 调优的参数可以在哪里设置参数值?
一种的tomcat打包的war文件,一种是spring boot打包的jar文件
1.1 tomcat的设置vm参数
修改TOMCAT_HOME/bin/catalina.sh(linux)文件,如下图
堆初始化为521m,最大为1024m。
1.2 springboot项目jar文件启动
通常在linux系统下直接加参数启动springboot项目
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --
spring.profiles.active=prod &
nohup : 用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
参数 & :让命令在后台执行,终端退出后命令仍旧执行。
2、 用的 JVM 调优的参数都有哪些?
对于JVM调优,主要就是调整年轻代、年老大、元空间的内存空间大小及使用的垃圾回收器类型。
可参考官网:官网
2.1设置堆空间大小
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值。
-Xms:设置堆的初始化大小
-Xmx:设置堆的最大大小
堆空间设置多少合适?
- 最大大小默认为物理内存的1/4,初始化大小是物理内存的1/64。
- 堆太小,可能会频繁导致年轻代和老年代的垃圾回收,会产生stw,暂停用户的线程。
- 堆内存大肯定是好的,存在风险,假设发生FullGC,它会扫描整个堆空间,暂停用户线程的时间长
- 推荐:尽量大,但是也要考察当前计算机其他程序的内存使用情况。
2.2虚拟机栈的设置
每个线程默认会开启1M的内存,用于存放栈帧,调用参数,局部变量等,但一般256K就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上受限于操作系统。
-Xss 对每个线程stack大小的调整,-Xss128k
2.3设置年轻代中Eden区和两个Survivor区的大小比例
设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。Java官方通过增大Eden区的大小,来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。
-XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3
3表示的是Eden区,Survivor区共占2份(from区1,to区1)
2.4年轻代晋升老年代阈值
-XX:MaxTenuringThreshold=threshold
默认值为15,取值范围是0-15。越小越容易进入老年区。
2.5设置垃圾回收器
通过增大吞吐量提高系统性能,可以设置并行垃圾回收器。
JDK1.8默认是
-XX:+UserParallelGC
-XX:+UserParallelOldGC
JDK1.9默认是
-XX:+UserG1GC
3、说一下 JVM 调优的工具?
3.1命令:jps(Java Process Status)
输出JVM中运行的进程状态信息(现在一般使用jconsole)
jdk/bin目录下jconsole图形界面更加直观
3.2jstack
jstack [option] <pid>
比如出现死锁,可以查看当前线程的运行情况,具体代码行数。
3.3 jmap
用于生成堆转存快照
jmap [options] pid 内存映像信息
jmap -heap pid 显示Java堆的信息
jmap -dump:format=b,file=heap.hprof pid
format=b表示以hprof二进制格式转储Java堆的内存
file=用于指定快照dump文件的文件名。
dump文件:指的是进程或系统在某一指定时间内的快照。
比如:在进程崩溃时,甚至任何时候都可以通过可视化工具将其内存备份出来供调式和分析。
它包括了模块信息、线程信息栈堆使用信息,异常信息等等。
3.4 jstat
是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
jstat -gcutil pid
jstat -gc pid
可视化工具
4.1jconsole
用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行
4.2VisualVM:故障处理工具
能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行
可查看堆dump。
4、Java内存泄漏的排查思路
原因:
- 如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量的时候,java虚拟机将抛出一个StackOverFlowError异常
- 如果java虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutOfMemoryError异常
- 如果一次加载的类太多,元空间内存不足,则会报OutOfMemoryError:
Metaspace
实战:
代码:一个list无限添加元素
public static void main(String[] args) {
Thread t1 = new Thread(()->{
ArrayList<String> list = new ArrayList<>();
while (true){
list.add("a");
}
},"t1");
t1.start();
}
这是控制台打印,但是我们项目部署在服务器中,突然宕机是看不到的。
1、通过jmap指定打印他的内存快照 dump
有的情况是内存溢出之后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式的生成dump文件,配置如下:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/app/dumps/ 指定生成后文件的保存目录
注意:如果是linux系统中的程序,则需要把dump文件下载到本地(windows环境)下,打开VisualVM工具分析。VisualVM目前只支持在windows环境下运行可视化。
2、通过工具, VisualVM(Ecplise MAT)去分析 dump文件
VisualVM可以加载离线的dump文件,如下图
文件–>装入—>选择dump文件即可查看堆快照信息
不仅可以看到原因,还可以找到对于的代码
5、 CPU飙高排查方案与思路?
代码示例(一个死循环):
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while (true){
}
},"thread1");
t1.start();
}
1.使用top命令查看占用cpu的情况
2.通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:2266
3.查看当前线程中的进程信息
ps H -eo pid,tid,%cpu | grep 2266
pid: 进程id
tid :进程中的线程id
% :cpu使用率
4.通过上图分析,在进程2266中的线程2276占用cpu较高
注意:上述的线程id是一个十进制,我们需要把这个线程id转换为16进制才行,因为通常在日志中展示的都是16进制的线程id名称。
转换方式:
在linux中执行命令:printf “%x\n” 2276
5、可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号
jstack 2266 此处是进程id
再通过线程id(16进制)找到对应的线程,就可以看到代码的位置。