文章目录
- 一、JVM组成
- 1.什么是程序计数器
- 2.什么是Java堆?
- 3.能不能介绍一下方法区(元空间)
- 4.你听过直接内存吗
- 5.什么是虚拟机栈
- 6.垃圾回收是否涉及栈内存?
- 7.栈内存分配越大越好吗?
- 8.方法内的局部变量是否线程安全?
- 9.什么情况下会导致栈内存溢出?
- 10.堆栈的区别是什么
- 二、类加载器
- 11.什么是类加载器,类加载器有哪些
- 12.什么是双亲委派模型?
- 13.JVM为什么采用双亲委派机制?
- 14.说一下类装载的执行过程
- 三、垃圾回收
- 15.强引用、软引用、弱引用、虚引用区别:
- 15.1 对象什么时候可以被垃圾器回收
- 16.JVM 垃圾回收算法有哪些?
- 16.1 标记清除算法
- 16.2 标记整理算法( 标记清除算法的升级---多了一个整理阶段)
- 16.3 复制算法
- 17.说一下JVM中的分代回收
- 17.1 MinorGC、 Mixed GC 、 FullGC的区别是什么
- 18.说一下JVM有哪些垃圾回收器?
- 19.详细聊一下G1垃圾回收器
- 四、JVM实践
- 20.JVM 调优的参数可以在哪里设置
- 21.用的 JVM 调优的参数都有哪些?
- 22.说一下 JVM 调优的工具?
- 23.Java内存泄露的排查思路?
- 24.CPU飙高排查方案与思路?
JVM是什么:
JVM(Java虚拟机)是Java程序的
运行环境
,它是Java平台的核心组成部分之一。JVM提供了一个
运行Java字节码的虚拟机
,负责将
Java程序解释和执行。
Java程序员可以在JVM上编写和运行Java程序,而不用考虑底层操作系统的差异性
。JVM的特性使得Java具备了跨平台性
,同一份Java代码可以在不同
的操作系统上运行
。
组成部分如下:
一、JVM组成
1.什么是程序计数器
程序计数器:线程私有的
,内部保存的字节码的行号
。用于记录正在执行的字节码指令的地址。(通俗的来说就是记录当前线程的程序执行的字节码指令的行号)
2.什么是Java堆?
线程共享的区域:主要用来保存对象实例
,数组
等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
Java1.8-JVM内存结构
其中:堆分为两部分
年轻代
包括三部分,Eden区
和两个大小严格相同的Survivor区
,根据JVM的策略,在经过
几次垃圾收集
后,任然存活于Survivor的对象将被移动到老年代区间。
老年代
主要保存生命周期长
的对象,一般是一些老的对象
元空间
保存的类信息
、静态变量
、常量
、编译后的代码
Java1.7-JVM内存结构
唯一的不同就是在java8的JVM中,元空间是在本地内存中的
,而java7当中元空间是作为方法区/永久代
存在于堆空间中的
因而在1.8之后做出改动,主要是为了防止OMM内存泄漏,因为元空间(方法区/永久代)保存的是类信息
、静态变量
、常量
、编译后的代码
,随着程序不断庞大,实现很难预估元空间的大小,久而久之就会产生OMM。
在旧的Java版本(如Java 7及更早版本)中,方法区和永久代(PermGen)都是
堆
的一部分,用于存储类的结构信息、常量池、静态变量等。但永久代容易导致内存溢出的问题,因为它的大小是固定
的,并且无法在运行时动态调整。
为了解决这个问题,并改进类的元数据的存储方式,Java 8 引入了元空间(Metaspace)。元空间通过使用本地内存
来存储类的元数据,有效地解决了永久代的限制和内存溢出问题。与永久代不同,元空间的大小并不受堆内存大小的限制
,而是受系统的物理内存限制
。
元空间还具有自动调整大小的能力
,可以根据需要动态地分配和释放内存
。这使得元空间更具灵活
性和可靠
性,并能更好地适应各种应用程序的需求。
3.能不能介绍一下方法区(元空间)
在不同的JVM实现中,方法区存在位置是不一样的。
这里举例在jdk1.7时的jvm
方法区(Method Area)是各个线程共
享的内存区域
主要存储类的信息
、运行时常量池
虚拟机启动
的时候创建
,关闭
虚拟机时释放
如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace
普通常量池
可以看作是一张表,虚拟机指令
根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
通俗的说,就是记录了虚拟机指令和执行的类名、方法名等信息的
映射关系
。
编译阶段:常量池的符号引用为这种
#+数字
,因为只是编译阶段,没到运行阶段,只需要直到机器指令和方法、类名等等的映射关系,能找到对应上就算编译通过
运行时常量池
在类加载阶段,JVM会将符号引用
解析为直接引用
(将符号替换为真实的地址值),并将其存储在常量池中,以供运行
时使用。
*常量池是 .class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的
符号地址
变为真实地址
4.你听过直接内存吗
直接内存:并不属于JVM中的内存结构
,不由JVM进行管理
。是虚拟机的系统内存
,常见于NIO
(非阻塞io)操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高
直接内存最直接的体现就是常规IO和NIO
的区别:
我们都知道NIO是非阻塞的,IO是传统阻塞式
IO传统阻塞式
相当于java程序不能直接去读系统内存给的数据,需要一个媒介就是堆内存java缓冲区
NIO非阻塞式
相比较IO是直接到直接内存读数据,所以只要将
磁盘文件读到直接内存
,数据准备就绪,程序切换到内核态就直接从直接内存读数据
。
5.什么是虚拟机栈
通俗来说就是线程的方法栈
Java Virtual machine Stacks (java 虚拟机栈)
每个线程运行时所需要的内存
,称为虚拟机栈,先进后出
每个栈由多个栈帧
(frame)组成(一个栈帧代表一个方法占用的内存),对应着每次方法调用时所占用的内存
每个线程
只能有一个活动栈帧
,对应着当前正在执行的那个方法
也就是一个方法(主方法)可以调用其他很多别的方法(子方法)
6.垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存
,当栈帧弹栈
以后,内存就会释放
7.栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k
栈帧过大会导致线程数变少
,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
很好理解,机器内存就那么多,单独把
栈内存调大了
,那么栈帧的个数自然的得变少
,所以对应的线程数就会变少
8.方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围
,它是线程安全
的
如果是局部变量引用了对象,并逃离方法的作用范围
,需要考虑线程安全
通俗来说,就是看
方法内的局部变量是不是完全包围在方法内
(像如果是作为方法参数
传过来的、作为返回值返回
的都可以算是逃离了方法
的作用范围),这样别的线程可以肆意修改方法内的局部变量
9.什么情况下会导致栈内存溢出?
栈帧过多导致栈内存溢出,典型问题:递归调用
栈帧过大导致栈内存溢出(基本上不存在)
栈的容量较小:相对于堆内存而言,栈内存的容量通常比较有限。
每个线程都有自己的栈空间
,而每个栈的大小通常只有几MB到几十MB
。这限制了栈帧的大小
,使得栈溢出的概率较低。
10.堆栈的区别是什么
分配对象和数据类型:堆内存用于分配Java对象
和数组
。所有通过关键字new创建的对象以及通过反射或JNI创建的对象都存储在堆中。而栈内存主要用于存储基本数据类型的变量
和方法调用时的局部变量。
存储位置:堆内存位于JVM的堆区
,是一个共享的内存区域
。栈内存位于JVM的栈区
,每个线程
都有自己的独立栈
。
内存管理方式:堆
内存的分配和回收由Java虚拟机的垃圾回收器负责管理
。垃圾回收器会自动回收不再使用的对象,并释放对应的内存空间。而栈
内存的分配和回收是由编译器自动管理的
,当方法执行结束
或变量超出作用域时,栈上的内存会自动被释放
。
内存分配速度:堆
内存的分配速度相对较慢,因为需要进行复杂的垃圾回收算法
和对象定位操作。而栈
内存的分配速度较快,仅仅是简单地进行指针移动。
内存空间大小限制:堆内存的大小一般比栈内存大得多
。堆内存的大小可以通过JVM的配置参数进行调整
。而栈
内存的大小是由线程的启动参数
决定的,每个线程的栈大小通常是固定的。
对象的生命周期:堆内存中的对象生命周期可以很长
,可以在程序的任意位置被引用和访问。而栈
内存中的局部变量生命周期与方法调用密切相关,当方法执行结束时,栈上的局部变量会自动销毁。
总结来说,堆内存主要用于存储Java对象和数组,由垃圾回收器管理,分配和回收速度相对较慢;而栈内存主要用于存储基本数据类型的变量和方法调用时的局部变量,由编译器自动管理,分配和回收速度相对较快。两者在内存管理方式、存储位置、分配速度和大小等方面有较大的区别。
二、类加载器
11.什么是类加载器,类加载器有哪些
类加载器
JVM只会运行二进制文件,类加载器
的作用就是将字节码文件
加载到JVM中,从而让Java程序能够启动起来。
类加载器包括四种
启动类加载器(加载像库里自带的类string integer等等
)(BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库
扩展类加载器(ExtClassLoader):主要加载JAVA_HOME/jre/lib/ext目录中的类
应用类加载器(加载自己java程序写的类
)(AppClassLoader):用于加载classPath下的类
自定义类加载器(很少自己写加载器
)(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。
12.什么是双亲委派模型?
加载某一个类,先委托上一级的加载器进行加载
,如果上级加载器也有上级,则会继续向上委托
,如果该类委托上级没有被加载
,子加载器尝试加载该类
举例:
假设有一个Student类
,需要加载
这个时候就会定位到AppclassLoader(应用类加载器),发现应用记载器有上级加载器,就先不加载,看看上级加载器里面有没有加载过的Student类,一直往上找,发现都没有加载过的Student类,那么此时AppclassLoader才会去加载Student类
假设有一个String类
,需要加载,也是定位到AppclassLoader(应用类加载器)往上委托直到找到启动类加载器
,找到了加载好的String类,直接加载。
13.JVM为什么采用双亲委派机制?
JVM采用双亲委派机制(Parent Delegation Model)是为了解决两个主要问题:
安全性
和避免类的重复加载
。
安全性体现在:
确保核心Java库的安全性,避免恶意代码通过自定义的类伪装成核心类库
,从而提高系统的安全性。
例如下面我们自己创建一个String包装类,此时核心类库是本身就会加载这个类,这个时候就会报错,不允许加载自定义的核心库的类。
双亲委派机制可以确保核心Java库的安全性。当一个类需要被加载时,首先会
委派给父类加载器进行查找和加载
,只有在父类加载器无法找到该类时
,才会由当前类加载器自己去加载。
避免类的重复加载体现在
通过双亲委派机制,当一个类需要被加载时,首先由父类加载器尝试加载,如果父类加载器已经加载了该类,就直接返回;否则,再由子类加载器尝试加载。这种机制可以确保在整个类加载器层次结构中
,每个类只被加载一次
,避免了类的重复加载,提高了运行效率
。
14.说一下类装载的执行过程
类在装载的时候会经历7个过程:
- 加载:查找和导入class文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:JVM 开始从入口方法开始执行用户的程序代码
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。
详细参考链接:【JVM】类装载的执行过程
三、垃圾回收
15.强引用、软引用、弱引用、虚引用区别:
强引用:只有所有 GC Roots 对象都不通过【强引用】引用该对象
,该对象才能被垃圾回收
向以下这种情况就不会被回收
软引用:仅有软引用引用该对象时,在垃圾回收后
,内存仍不足时会再次出发垃圾回收
弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足
,都会回收弱引用对象
虚引用:必须配合引用队列使用
,被引用对象回收时
,会将虚引用入队
,由 Reference Handler 线程调用虚引用相关方法释放
直接内存
总结:
- 强引用:
只要所有 GC Roots 能找到,就不会被回收
- 软引用:需要
配合SoftReference使用
,当垃圾多次回收,内存依然不够的时候会回收软引用对象
- 弱引用:需要
配合WeakReference使用
,只要进行了垃圾回收,就会把弱引用对象回收
- 虚引用:必须配合
引用队列使用
,被引用对象回收时,会将虚引用入队
,由Reference Handler线程
调用虚引用相关方法释放直接内存
15.1 对象什么时候可以被垃圾器回收
垃圾回收,回收的是针对于堆的
简单一句就是:如果一个或多个对象没有任何的引用指向它
了,那么这个对象现在就是垃圾,如果定位了垃圾
,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法
引用计数法(不常用)
一个对象被引用了一次,在当前的对象头上递增一次引用次数(ref=1)
,如果这个对象的引用次数为0(ref=0)
,代表这个对象可回收
但是当对象间出现了循环引用
的话,则引用计数法就会失效
可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
比如下面的例子
X,Y这两个节点是可回收的
Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
扫描堆中的对象,看是否能够沿着 GC Root 对象
为起点的引用链
找到该对象,找不到,表示可以回收
哪些对象可以作为 GC Root ?
-
虚拟机栈(栈帧中的本地变量表)中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
16.JVM 垃圾回收算法有哪些?
参考链接:GC详解、GC四大算法和GC Root
总共分为三种:
标记清除算法
:垃圾回收分为2个阶段,分别是标记和清除,效率高,有磁盘碎片,内存不连续
标记整理算法
:标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
复制算法:
将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低
16.1 标记清除算法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除
。
1.根据可达性分析算法
得出的垃圾进行标记
2.对这些标记为可回收的内容
进行垃圾回收
注意:标记GC Root引用链的对象,回收的就是没有被标记的
缺点:
- 需要两次扫描,耗时严重。
先扫描一次,对
存活的对象进行标记。
再次扫描,回收没有被标记的对象。
- 会产生内存碎片,导致内存空间不连续。
当需要分配一个较大的内存块时,由于
没有足够的连续内存空间
,可能会导致分配失败即内存溢出。
16.2 标记整理算法( 标记清除算法的升级—多了一个整理阶段)
优缺点同标记清除算法,解决了标记清除算法的碎片化的问题
,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。
相比较标记清除算法在垃圾回收后,会将活着的对象滑动到一侧,这样就能让空出的内存空间是连续的。
16.3 复制算法
相当于把一块堆内存分成两半来用,一般拿来存对象,另一半拿来做垃圾回收后的整理收纳仓(必须为空闲空间)
复制算法需要将存活的对象
从一个区域
复制到另一个区域
(空白区域),然后直接清空存活对象和待回收对象的那一片区域
注意:要求堆内存的
使用比例不超过50%
,因为每次垃圾回收时,需要保证目标区域有足够的空间来存放从源区域复制过来的对象。
优点:
在垃圾对象多的情况下,效率较高
清理后,内存无碎片
缺点:
分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
17.说一下JVM中的分代回收
一、堆的区域划分
堆被分为了两份:新生代和老年代
【1:2】
对于新生代,内部又被分为了三个区域。Eden区
,幸存者区survivor(分成from和to)
【8:1:1】
二、对象回收分代回收策略
- 新创建的对象,都会先分配到eden区
- 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和 from 内存都得到释放
- 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将其复制到from区
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
参考链接:【JVM】JVM中的分代回收
17.1 MinorGC、 Mixed GC 、 FullGC的区别是什么
STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成
MinorGC、 Mixed GC 、 FullGC 相当于垃圾回收的等级:
MinorGC:【young GC】发生在新生代
的垃圾回收,暂停时间短(STW)
Mixed GC: 新生代 + 老年代部分区域
的垃圾回收,G1 收集器特有
FullGC: 新生代 + 老年代完整
垃圾回收,暂停时间长(STW),应尽力避免
18.说一下JVM有哪些垃圾回收器?
在jvm中,实现了多种垃圾收集器,包括:
-
串行垃圾收集器
Serial 作用于新生代,采用复制算法
Serial Old 作用于老年代,采用标记-整理算法 -
并行垃圾收集器(
JDK8默认使用此垃圾回收器
)
Parallel New作用于新生代,采用复制算法
Parallel Old作用于老年代,采用标记-整理算法 -
CMS(并发)垃圾收集器
针对老年代垃圾回收的 -
G1垃圾收集器(
在JDK9之后默认使用
)
应用于新生代和老年代
详细参考链接:【JVM】JVM垃圾收集器
19.详细聊一下G1垃圾回收器
详细参考链接:【JVM】JVM垃圾收集器
G1垃圾收集器的设计目标是
在可控的停顿时间
内实现高吞吐量的垃圾回收。
- 应用于
新生代
和老年代
- 划分成
多个区域
,每个区域
都可以充当 eden,survivor,old, humongous,其中humongous 专为大对象准备
- 采用
标记整理算法
因为基本上G1主要针对大型堆内存进行垃圾回收,而复制算法在大型堆内存上的应用存在一些挑战和限制。(必须考虑内存空间使用率)
-
响应时间与吞吐量兼顾
-
分成三个阶段:新生代回收、并发标记、混合收集(在不同的条件下被触发)
-
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC(尽量避免)
如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.
四、JVM实践
20.JVM 调优的参数可以在哪里设置
war包部署在tomcat中设置
jar包部署在启动参数设置
21.用的 JVM 调优的参数都有哪些?
- 设置堆空间大小
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值
。
堆空间设置多少合适?
最大大小的默认值是物理内存的1/4,初始大小是物理内存的1/64
堆太小,可能会频繁的导致年轻代和老年代的垃圾回收,会产生stw,暂停用户线程
堆内存大肯定是好的,存在风险,假如发生了fullgc,它会扫描整个堆空间,暂停用户线程的时间长
设置参考推荐:尽量大,也要考察一下当前计算机其他程序的内存使用情况
- 虚拟机栈的设置
虚拟机栈的设置:每个线程默认会开启1M的内存
,用于存放栈帧、调用参数、局部变量等,但一般256K就够用
。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
- 年轻代中Eden区和两个Survivor区的大小比例
设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1
。通过增大Eden区的大小,来减少YGC发生的次数
,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。
-
年轻代晋升老年代阈值
-
设置垃圾回收收集器
22.说一下 JVM 调优的工具?
命令工具
-
jps 进程状态信息
-
jstack 查看java进程内线程的堆栈信息
-
jmap 查看堆转信息
-
jhat 堆转储快照分析工具
-
jstat JVM统计监测工具
可视化工具
-
jconsole 用于对jvm的内存,线程,类 的监控
-
VisualVM 能够监控线程,内存情况(只有jdk8才有)
23.Java内存泄露的排查思路?
内存泄漏通常是指堆内存
,通常是指一些大对象不被回收的情况
1、通过jmap或设置jvm参数获取堆内存快照dump
2、通过工具, VisualVM去分析dump文件
,VisualVM可以加载离线的dump文件
3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
4、找到对应的代码,通过阅读上下文的情况,进行修复即可
参考链接:【JVM】Java内存泄露的排查思路?
24.CPU飙高排查方案与思路?
1.使用top命令查看占用cpu的情况
2.通过top命令查看后,可以查看是哪一个进程占用cpu较高
3.使用ps命令查看进程中的线程信息
4.使用jstack命令查看进程中哪些线程出现了问题,最终定位问题
参考链接:【JVM】CPU飙高排查方案与思路