目录
2. 什么叫做到GC Roots不可达?什么可以作为GC Roots?
一.JVM内存
1. 说一下JVM运行时数据区
JVM运行时数据区域分为五个部分:
- 堆:存放对象实例;
- 虚拟机栈:存放局部变量,返回结果;
- 本地方法栈:和虚拟机栈差不多,存放本地方法的局部变量,返回结果;
- 方法区:存放类加载的信息(比如类名,方法名),常量,静态变量;
- 程序计数器:其实是一个行号指示器,程序运行到哪一行由它控制。
其中堆和方法区是线程共享区域;虚拟机栈、本地方法栈、程序计数器是线程隔离数据。
程序计数器属于线程私有主要是为了线程切换后能恢复到正确的执行位置
虚拟机栈、本地方法栈私有是为了线程的私有变量不被其他线程访问到
2. 深拷贝和浅拷贝
- 浅拷贝只是增加了一个指针指向已存在的内存地址
- 深拷贝增加了一个指针并申请了一块新的内存,使新指针指向新内存
对象.clone()是浅拷贝,BeanUtils.copyProperties(source, target)是深拷贝
3. 几种创建对象的方式
- new关键字:Person p = new Person();
- Class的newInstance方法:Person p = Class.forName("com.test.Person").newInstance();
- Constructor的newInstance方法:Person p = Person.class.getConstructors()[0].newInstance();
- clone方法 (浅拷贝):Person p = p1.clone();
- 反序列化:把对象流反序列成对象
4. new一个对象的时候,发生了什么事?
- 类加载:先查看当前类是否被加载,没有的话,就加载到JVM中
- 分配空间:在堆中分配一块足够大小的空间用于存储对象
- 初始化对象:给成员变量赋默认值,以及执行@PostConstructor注释的初始化方法
- 实例化对象:执行构造方法实例化对象,最后new关键字会把对象的引用进行返回
如何对象在堆中分配空间?
如果空间是规整的,就按“指针碰撞”的方式,把指针往空间空闲方向移动对象大小的距离;如果空间是不规整的,就从空闲列表中分配一块内存。因为new操作经常执行,因此分配空间可能会存在并发问题,JVM是通过CAS+失败重试的方式解决的。
5. 对象的访问定位?
对象的访问方式有两种:直接指针和句柄访问。
- 直接指针:栈中的引用指向堆中的对象实例,对象实例中包含指向方法区中对象的类型数据。
- 句柄访问:栈中的引用指向堆中句柄池中的句柄,句柄包含两个指针,一个指向堆中的实例,另一个指向方法区中对象的类型数据。
JVM 会根据不同的应用场景选择合适的访问方式
6. 内存泄漏和内存溢出
内存泄漏和内存溢出有什么区别?
- 内存泄漏:已经申请的内存一直无法释放。长生命周期的对象持有短生命周期的对象会发生内存泄漏。
- 内存溢出:没有足够的内存空间供申请者使用。
内存泄漏堆积后会导致内存溢出。
内存泄漏的场景:
- 使用ThreadLocal时没有调用remove()方法。
- 使用各种连接后不关闭,包括数据库连接,网络连接(socket),IO连接。内存泄漏原因:除非显示调用close()方法,否则连接不会被关闭。
内存溢出的场景:
- 堆溢出:上传一个非常大excel文件,每一行都转换成了java对象,然后又通过JSON.toJSONString()打印了出来。JSON.toJSONString()每次都会创建新对象,这就相当于在疯狂创建对象导致了堆内存溢出。(排查过程:先是接收到告警,内存直接被打满了,我们把日志文件保存下来后,先立即重启了服务,确保服务依然对客可用。然后在日志中发现有OOM,堆内存溢出,幸好我们jvm配置了自动保存dump文件,下载下来后用Eclipse Memory Analyzer进行了分析,发现String对象异常多,但其实程序中String对象就是很多,继续排查,发现还有一个对象占用空间很大,然后在程序中就找到了这个对象的使用位置,进而发现了问题。 最终解决:首先不要打印,其次大文件限制行数)
- 栈溢出:递归次数太多可能会导致栈溢出。
二. 垃圾回收
1. 什么时候进行垃圾回收?回收哪些垃圾?
虚拟机空闲或者堆内存不足是会触发垃圾回收。手动调用System.gc()可以通知GC执行,但是不一定会执行。
回收的是到GC Roots不可达的对象。
2. 什么叫做到GC Roots不可达?什么可以作为GC Roots?
可达性算法:从GC Roots往下搜索,当一个对象到GC Roots没有任何引用链时,证明该对象可以被回收。
可以作为GC Roots的对象:
- 栈上的引用变量
- 所有的全局变量,静态变量
- 所有的ClassLoader
- 基本数据类型对应的class对象
- 常驻的异常对象(NullPointException、OutOfMemoryError)
3. 引用分为哪些?
- 强引用(Strongly Reference):强引用是最传统的引用,Object o = new Object() 就是强引用。强引用无论什么时候都不会被回收。
- 软引用(Soft Reference):软引用是一些还有用但是非必须的对象。系统会在内存溢出前回收这些引用。
- 弱引用(Weak Reference):弱引用比软引用更弱。下一次垃圾回收就会把弱引用回收掉。
- 虚引用(Plantom Reference):虚引用也叫“幽灵引用”,是最弱的引用关系。设置虚引用的唯一作用就是为了能在垃圾回收时能收到一个系统通知。
4. 垃圾回收算法
- 标记-清除算法:先标记存活的对象,然后清除未被标记的对象。缺点:会有内存碎片。
- 标记-整理(老年代使用):把存活的对象都往一端移动,然后把边界外的空间都清空
- 复制算法(年轻代使用):内存等分成两份,只使用其中一半,垃圾回收的时候把活着的对象复制到另一半内存,然后把已使用的空间一次性清理掉。缺点:内存使用率不高。
- 分代算法:年轻代使用复制算法,老年代使用标记-整理算法
改良后的复制算法:把内存分为Eden区、s1区、s2区,默认比例是 8:1:1
(1)把Eden区和s1区存活的对象复制到s2(放不下时,会启用担保机制,把对象放入老年代中)
(2)清空Eden区和s1区
(3)把s2区复制到s1区
每次从s1 到 s2存活的对象,年龄就+1,当年龄到一定程度(默认15),对象就会进入老年代。大对象会直接进入老年代。
5. 垃圾回收器
年轻代垃圾回收器:
- Serial(复制算法):新生代单线程垃圾回收器,简单高效
- Parnew(复制算法):新生代并行收集器,是serial的多线程版本,在多核CPU环境下有着比serial更好的表现
- Parallel Scavenge(复制算法):新生代并行收集器,追求高吞吐量
老年代垃圾回收器:
- Serial Old(标记-整理算法):老年代单线程垃圾回收器
- Parallel Old(标记-整理算法):老年代并行垃圾回收器
- CMS(Concurrent Mark Sweep)(标记-清除算法):老年代并行收集器,以获取最短停顿时间为目标,具有高并发、低停顿的特点
整堆回收器:
- G1(Garbage First)(标记-整理算法):java堆并行收集器,没有内存碎片
G1垃圾回收器
G1垃圾回收器以获得可控的回收停顿时间为目的,它把java堆分为了很多小块,每个小块又分为年轻代和老年代。局部看用的是复制算法,整体看用的是整理算法。
简单说下CMS垃圾回收器
CMS是Concurrent Mark Sweep的缩写,是一个老年代并行垃圾收集器,以获得最短回收停顿时间为目的。它采用标记-清除算法,所有会有大量的内存碎片,当内存无法满足程序运行要求时,会临时标记整理算法进行垃圾回收,此时性能会降低。
CMS的几个阶段:
- 初始标记(stop the world):标记GCRoots
- 并发标记:并发标记和GCRoots关联的对象
- 二次标记(stop the world):标记并发标记阶段新产生的对象
- 并发清除:并发清除已经死亡的对象
为什么要stop the world?
为了防止标记期间已经死亡的对象又重新回到引用链中,如果不stop the world就会把不该清除的对象清除掉。
三色标记法
- 黑色:根对象 或 该对象所有直接引用都被垃圾回收器访问过
- 灰色:该对象被垃圾回收器访问过,但至少还有一个直接引用没被访问过
- 白色:未被垃圾回收器访问过的对象
扫描完成后,黑色就是存活的对象,白色就是可回收的对象。
存在的问题:
没有stop the world,标记期间已经死亡的对象又重新回到引用链中。
解决方案:
把标记期间新增的引用对象记录下来,等待遍历
G1和CMS有什么区别?
- G1是可以获得可控的停顿时间,CMS可以获得最短停顿时间;
- 如果有大对象要放入堆中,G1会先放到尚未使用的Region中,如果还是放不下就横跨多个Region来存放大对象(G1中超过region的50%就叫大对象);CMS会把大对象放入老年代中。
三. 类加载机制
1. 说一下类加载的过程
- 加载:把class文件从磁盘加载到内存中
- 验证:验证class文件的正确性
- 准备:给类中的静态变量分配内存空间
- 解析:把常量池中的符号引用替换为直接引用的过程(符号引用是类、方法、字段的符号化描述,直接引用才指向内存中的地址)
- 初始化:初始化静态变量和静态代码块
2. 类加载器有哪些?
- 启动类加载器:加载java核心类库
- 扩展类加载器:加载java扩展类
- 应用程序类加载器:根据提供的路径加载指定类库
- 自定义类加载器:通过继承java.lang.classloader来实现
3. 什么是双亲委派模型?
当一个类加载器收到加载类的请求,它不会自己先去加载这个类,而是把类委派给父加载器去加载,每一层都是如此。如果父类不能加载,再交给子类加载。
四. JVM调优
1. JVM调优参数
对于JVM调优,主要就是调整 堆、栈、元空间的内存空间大小及垃圾回收器类型
- -xms2g: 初始化堆大小为2g
- -xmx2g: 堆最大内存为2g,一般可以设置和xmx相同,防止垃圾回收完重新分配内存
- -xmn1g: 设置年轻代大小为1g
- -xss256k:设置每个线程stack的大小
- -XX:MaxMetaspaceSize:设置元空间最大值,默认-1,即不限制
- -XX+UseG1GC:使用G1垃圾回收器
- -XX:UseConcMarkSweepGC: 指定使用CMS + SerialOld垃圾回收器组合
- -XX:newRatio=4: 设置年轻代和老年代的内存比例为1:4
- -XX:survivorRatio=8: 设置年轻代中Eden区和survivor区的内存比例为8:1:1
- -XX:+UseParNewGC: 指定使用ParNew + SerialOld垃圾回收器组合
- -XX:+UseParrallelOldGC: 指定使用ParNew + ParNewOld垃圾回收器组合
- -XX:PrintGC: 打印GC信息
- -XX:PrintGCDetail: 打印详细GC信息
如何分配空间?
一般2核4G的机器,JVM分的内存大概是1/2,也就是2G。
考虑到要给元空间和虚拟机栈留空间,则给堆分1/2,也就是1G。其中新生代500M(Eden区400M,两个S各50M),老年代1G。
Xms和Xmx
设置堆的初始大小和最大大小,为了防止垃圾回收器在 初始大小 和 最大大小 之间收缩堆而产生额外的时间,通常把初始大小和最大大小设置为相同的值
最大大小默认值是物理内存的1/4,初始大小是物理内存的1/64。堆太小,可能会频繁的导致年轻代和老年代的垃圾回收产生stw,暂停用户线程;堆太大,如果发生fullgc,会扫描哦整个堆空间,暂停用户线程的时间长。
因此,要结合物理内存和其他程序内存使用情况合理设置。
Xss
虚拟机栈的设置:每个线程默认会开启1M的内存,用于存放方法的数据,局部变量、返回结果等,一般256K就足够了。太大的话必然导致支持的线程数变少;太小的话容易栈内存溢出。
元空间和方法区
他俩都是用于存储 类信息、常量、静态变量等,jdk8以后,方法区就逐渐被元空间取代了。区别在于方法区用的是堆空间中的非堆区域,元空间存储在本地内存(JVM中操作系统提供的本地内存)
2. 有没有做过JVM内存调优?
发现经常会受到请求超过1秒的告警(正常200ms),也能正常返回就是耗时长,看了日志,也没有报错,然后到服务监控上看CPU和内存使用率也不是很高,想着可能是网络抖动,但是还是每天几乎都会收到告警。然后发现告警大多数是在业务高峰期的时候。
然后就去看了一下gc.log,发现young gc的时延在1秒多(full gc一秒正常,ygc 一秒过长),时间点也能和告警时间对齐。我们当时用的是CMS垃圾回收器,CMS的回收策略是最大程度清理内存空间,而且GC的时候会有Stop The World的情况,所以GC的时延高。
解决方案:重点-控制每次gc时延,保证stw在可接受时间内,使用G1,可配置单次ygc的时延(最终配置的是500ms),G1会尽可能的保证gc时延不超过预期值。
3. 平时使用什么工具进行JVM调优?
JDK安装目录下,有很多性能监测工具
命令工具
- jps:查看所有进程
- jstack:查看线程信息
- jmap:用于生成堆内存快照文件(dump文件)
- jstat:JVM统计监测工具,比如查看JVM的GC
可视化工具
- jconsole:用于对jvm的内存、线程、类的监控
- jvisualvm:监控线程、内存情况(JDK1.8才有),分析dump文件
- Eclipse Memory Analyzer:分析dump文件,查看内存使用情况和线程堆栈
调优实例:第十三篇:Linux常见命令_xnninger的博客-CSDN博客
(1)jps:查看进程信息
(2)jstack 查看堆栈信息
思路:找java进程,找线程,查看堆栈
- top:查看进程id
- top -H -p [进程id]:查看该进程下所有线程,按CPU使用率从高到低排序
- printf "%0x[线程id]":把线程id转成16进制,因为堆栈中 nid 是 线程id 的16进制表示
- jstack [进程id] | grep [线程id十六进制表示]:查看线程堆栈
(3)jstat 查看JVM情况
jstat -gcutil [进程号] [刷新频率]
- S0、S1:S0、S1区使用百分比
- E、O、M:年轻代、老年代、元数据区 使用百分比
- CCS:压缩类空间容量使用百分比
- YGC: YGC 次数
- YGCT:YGC 花费的时间
- FGC: Full GC 次数
- FGCT:Full GC 花费的时间
- GCT: YGC和Full GC总共花费的时间
(4)jmap 生成堆内存快照文件
- jmap -dump:format=b,file=name.dump <pid>:生成堆快照文件,再使用工具分析
- jmap -histo[:live] <pid>:打印堆中所有对象的数量和大小,加live只返回存活的对象
如果要在出现OOM就生成dump,可以使用-XX:+HeapDumpOnOutOfMemoryError参数
(5)jconsole
(6)jvisualvm
4. JVM内存溢出的排查思路
(1)通过jmap命令或设置jvm参数自动生成内存快照dump文件
jmap命令(程序运行时才能使用):
jmap -dump:format=b,file=name.dump <pid>
JVM参数设置:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/app/dumps
(2)通过工具 Eclipse Memory Analyzer导入和分析dump文件
(3)查看内存使用情况和线程堆栈,大概定位出内存溢出的代码行数,再阅读代码上下文进行修复