文章目录
1.内存泄漏原因
没有重写equals和hashcode方法
内部类引用外部类
threadlocal线程池线程不回收
string的intern方法
静态缓存不释放
网络 io流资源未关闭
大数据量查询未分页
导入导出大量数据,apache poi
mybatis查询拼接sql foreach
元空间大小不足metaspaceSize,方法区jdk7之前叫永久代,jdk8之后叫元空间,存放class类基本信息
2.JVM部分配置参数
stack over flow 栈内存溢出,看递归层数是否需要,调节-Xss
Xmn 年轻代大小
-XX:SurvivorRatio 伊甸园区和幸存区大小比例,默认为8
-XX:MaxTenuringThreshold,最大晋升阈值,动态年龄判断机制,对象总量大于幸存区50%,等于大于该年龄的对象放到老年代,默认15
3.垃圾回收器的选择
jdk8默认的垃圾回收器是ps + po (parallel scavenge + parallel old)
有serial + serial old单线程回收
parNew +CMS 并发回收,用户线程和回收线程
G1 jdk11默认,性能更好
高并发下
ps + po :突发大对象的产生,接口响应时间很长
parNew +CMS:突发大对象的产生,比ps+po性能要好,但是缺点是后续会触发多次GC,波动较大
G1:高并发下低延迟,突发大对象的产生,波动也很小
4.GC调优:
优化jvm参数
减少对象产生
更换垃圾回收器
优化垃圾回收参数
5.查看线程命令
top -p pid 再按H,监控该进程内部的线程
6.问题一:cpu高占用
top命令找到占用高的进程和线程,使用jstack打印线程快照,找到正在执行的方法优化
7.问题二:接口响应时间很长
通过arthas的trace和watch命令,监控方法耗时和参数,返回值等信息,定位性能瓶颈
也可以通过arthas的profile火焰图功能,找到火焰图顶部较平的方法
8.问题三:线程不可用
jstack、visualvm等看是否有deadlock死锁
面试篇
1.什么是jvm
运行java字节码文件,跨平台性,将字节码文件解释成字节码指令,管理内存中对象分配,完成自动垃圾回收,优化代码执行效率
jvm组成有类加载器、运行时数据区、执行引擎、本地接口
2.字节码文件的组成
类基本信息、常量池、字段、方法、属性
3.什么是运行时数据区
堆、本地方法栈、虚拟机栈、方法区、程序计数器
线程共享得有方法区、堆
线程不共享有本地方法栈、虚拟机栈、程序计数器
程序计数器:pc寄存器,每个线程都会通过计数器记录当前要执行的字节码指令的地址,控制指令的执行
虚拟机栈:保存方法调用时的栈帧,每个线程都有一个自己的虚拟机栈
本地方法栈:存储native本地方法的栈帧
堆:一块内存区域
方法区:存放类的基础信息的一块内存空间,叫永久代或者元空间
4.哪些区域会出现内存溢出
堆:oom
栈:stackoverflowError
方法区(元空间):oom
5.jdk6-8内存区域上有哪些不同
(1)永久代被替换为元空间
1.提高了内存上限:
JDK7及之前的版本将方法区存放在堆中,称为永久代,堆的大小由虚拟机参数来控制
JDK8之后的版本将方法区存放在元空间中,元空间是位于操作系统的直接内存中,默认情况下只要不超过操作系统内存上限,就可以一直分配
2.优化了回收的策略:
永久代在堆上面,垃圾回收的机制一般都是使用老年代的垃圾回收方式,不够灵活,使用元空间之后单独设计了一套垃圾回收机制
(2)字符串常量池位置的变化
JDK7之前字符串常量池实在方法区里面,也就是在堆里面,8之后把方法区移到操作系统直接内存里了,字符常量池则从方法区里面提取了出来放到了堆里面去,原因如下:
字符串的回收其实和对象的回收差不多
移动到堆里面之后,方法区空间的大小变得可控,因为类信息一般不会太大。
intern方法的优化,jdk7版本之前intern方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中,现在把常量池直接保存在堆上,减少了复制操作
6.类的生命周期
- 加载:根据全限定名将字节码文件加载到方法区和堆上
- 连接验证:检查版本号等信息
- 准备:给类中的静态变量分配内存设初值
- 解析:将常量池中的符号引用(编号)替换为直接引用(内存地址)
- 初始化:执行静态代码块和静态变量的赋值
7.什么是类加载器
在类加载的过程中将字节码信息以流的方式加载到内存中
分类:
-
启动类加载器(bootstrap class loader),JDK9之前使用c++编写、9之后换成了java,默认加载java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar等,核心类。
-
扩展类加载器(extension class loader)是jdk提供的、9之后采用模块化改名platform平台类加载器,默认加载/jre/lib/ext的类文件,加载扩展类。
-
应用程序类加载器(application class loader)默认加载应用程序classpath下的类。
-
自定义类加载器,可以从网络、数据库等来源来加载类信息,需要继承classloader抽象类,重写findclass方法。
7.什么是双亲委派机制
当一个类接收到加载类的任务时,会向上查找是否加载过,如果加载了直接返回,到启动类加载器查找之后,再从上到下进行加载。
- 有什么用?
确保核心类库的完整性和安全性,避免同一个类重复加载。
8.如何打破双亲委派机制
实现一个自定义的类加载器,并重写loadClass方法,将双亲委派机制代码去掉。
9.如何判断堆上的对象没有被引用
(1)引用计数法
每个对象维护一个引用计数器。引用加1,取消引用减1
缺点:每次引用和取消引用都要维护一个计数器,对系统性能有一定的影响,存在循环引用的问题,导致对象无法回收。
(2)可达性分析法(默认采用)
将对象分为了两类
垃圾回收根对象(GC Root)和普通对象,对象和对象之间存在引用关系,
A->B->C,
B在引用链上就不能被回收。
- 哪些对象是GC Root对象?
- 线程Thread对象,引用线程栈帧中的方法参数、局部变量等。
- 系统类加载器加载的java.lang.Class对象,引用类中的静态变量。
- 监视器对象,用来保存同步锁synchronized关键字持有的对象。
- 本地方法调用时使用的全局对象。
10.jvm中都有哪些引用类型
-
强引用:jvm默认的引用关系,对象被局部变量、静态变量等GC Root关联的对象引用
-
软引用:一个对象只有软引用存在时,当程序内存不足时,就会将软引用中的数据进行回收softReference
-
弱引用:弱引用整体是和软引用差不多,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会被直接回收,主要在ThreadLocal中使用(WeakReference)
-
虚引用(幽灵引用/幻影引用):PhantomReference,当对象被垃圾回收器回收时可以接收到对应的通知,直接内存中为了及时知道直接内存对象不在使用了,从而回收内存,使用虚引用来实现
-
终结器引用:终结器引用会关联对象并放置在Finalizer类中的引用队列里,之后由一条线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。
11.常见的垃圾回收算法有哪些
-
标记-清除算法:
(1)标记阶段:将所有存活的对象进行标记,使用可达性分析算法,从GC Root开始通过引用链遍历出所有的存活对象
(2)清除阶段:从内存删除没有被标记的非存活对象
优点:实现简单,只需要维护一个标志位(存活还是非存活)
缺点:1.不连续的碎片问题,对象删除之后,内存会出现很多细小的碎片单元,大对象无法分配
2.分配速度慢,因为有内存碎片,需要维护一个空闲的链表,极有可能发生每次需要遍历到链表的最后才有合适的空间
-
复制算法:
(1)将堆内存分成From和To两个空间,每次对象在分配阶段,只能使用其中一块空间(From)
(2)在垃圾回收GC阶段,将From中存活对象复制到To空间
(3)将两块空间From和To名字互换
优点:吞吐量高,不会产生内存碎片,因为复制之后会将存活对象按顺序存放到To中
缺点:内存使用率低,每次只能使用一半的内存空间
-
标记-整理算法(标记压缩):
(1)标记阶段:和标记清除算法一样
(2)整理阶段:将所有的存活对象移动到堆的一端,清理掉非存活对象的内存空间
优点:1.内存使用率高,整个堆内存都能使用
2.没有内存碎片
缺点:整理阶段的效率不高
-
分代GC:
前面三种算法的组合
堆分成年轻代和老年代,放在年轻代的对象存活时间比较短,朝生夕死(变量啥的),放在老年代的对象存活时间比较长,比如spring的bean对象
年轻代:伊甸区、S0、S1(幸存区),幸存区采用的是复制算法
伊甸区满了或者新建的对象放不下了,就会出发Minor GC,将伊甸区和From区中需要回收对象对象进行回收,存活对象放入到To区,会记录一个对象年龄(最大值15),达到最大就会放到老年代,或者新建的对象太大了超过了伊甸区就会直接放到老年代
当老年代空间不足,无法放入新对象,先尝试minor gc,如果还是不足,就会触发full GC
12.有哪些常见的垃圾回收器
-
serial + serialOld:
都是单线程串行去执行,回收的时候,用户线程是不能工作的,STW等待时间很长,效率很低
优点:适合单cpu处理
缺点:多cpu下吞吐量不行
适用:一些机器性能要求不高的,比如儿童手表
-
ps +po:
JDK8默认的垃圾回收器,多线程并行回收,jvm可以自动调整堆内存大小
优点:吞吐量高
缺点:不能保证单次停止时间
适用:后台任务,比如大文件导出
-
parNew + CMS:
- parNew本质是对Serial在多cpu下的优化,使用多线程回收垃圾
缺点:吞吐量和停顿时间stw不如G1,JDK9之后就不建议使用了
适用:JDK8搭配CMS - CMS:采用标记清除算法,关注在系统暂停时间,允许用户线程和垃圾回收线程在并发标记和并发清理阶段同时执行
执行步骤:
1.初始标记,短时间标记出GC Root关联的对象,也即存活对象,用户线程暂停,STW
2.并发标记,标记所有对象,用户线程不暂停
3.重新标记:并发标记时候有些对象发生了变化,需要重新标记,用户线程暂停,STW
4.并发清理:清理非存活对象,用户线程不暂停
- parNew本质是对Serial在多cpu下的优化,使用多线程回收垃圾
缺点:内存碎片,退化问题,浮动垃圾问题
浮动垃圾问题:在并发清理的时候,可能有些未标记的对象已经不存活了,但是这轮没法清理了,只能等到下一轮来清理
退化问题:如果老年代满了,CMS就会退化成Serial Old单线程回收老年代
- G1:
年轻代和老年代全部用的是复制算法,内存划分是不连续的
JDK9之后默认的垃圾回收器,把内存划分为多个 Region,默认是堆空间大小/2048,必须是2的指数幂
年轻代回收Young GC:当G1判断年轻代区不足60%的时候,回收伊甸区,回收的时候会根据STW暂停时间进行判断当前轮数里面回收多少,哪个伊甸区,将存活的对象放到幸存区,幸存区互换,年龄超过15,放到老年代中;
如果一个大存活对象超过了region大小,就把这个大对象直接放到Humongous区中,占用连续的几个region区作为Humongous区
老年代回收Mixed GC,当总堆的占有率达到45%时候触发Mixed GC,会回收所有的年轻代和部分的老年代的对象以及大对象,采用复制算法
优点,没有内存碎片,并发标记效率比CMS高,支持巨大的堆空间回收,允许用户设置最大暂停时间