11、什么情况下会发生内存泄露?
- 静态集合类引起内存泄漏
- 静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。
- 单例模式
- 单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。
- 数据连接、IO、Socket等连接
- 创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。
- 变量不合理的作用域
- 一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。
- hash值发生变化
- 对象Hash值改变,使用HashMap、HashSet等容器的时候,由于对象修改之后的Hash值和存储进容器时的Hash值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么String类型被设置成了不可变类型。
- ThreadLocal使用不当
- ThreadLocal的弱引用导致内存泄漏也是个老生常谈的话题了,使用完ThreadLocal一定要记得使用remove方法来进行清除。
12、谈谈OOM的认识?如何排查 OOM 的问题?
除了程序计数器,其他内存区域都有 OOM 的风险,发生的地方如下:
- Java虚拟机栈+本地方法栈,扩展时无法申请到足够内存,OOM((线程请求深度超过JVM允许栈深度,StackOverflowException) 。
- 栈帧过多导致栈内存溢出,方法递归调用时,没有设置正确的递归结束条件,导致无限递归。
- 栈帧过大导致栈内存溢出
- 无限递归,第三方代码中出现两个类中循环引用的问题,导致无限递归。
- 堆内存溢出:不断产生新的对象,且对象都被引用了,无法触发垃圾回收机制,长时间就会导致堆内存溢出。
- 方法区: 方法区如果申请内存时发现内存不足,方法区也会出现内存溢出的问题,经常会遇到的是动态生成大量的类,无法满足内存分配需求时。
- 1.8以前会导致永久内存溢出: java.lang.OutOfMemoryError:PermGen space。
- 1.8之后会导致元空间内存溢出:元空间默认使用系统内存,且不会设置内存的上限,所以我们要手动设置。
- 直接内存 OOM:直接内存的回收和释放是通过unsafe 对象实现的,不是java 的垃圾回收机制。
排查 OOM 的方法:
- 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
- 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
- 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把它改为弱引用 。
13、如何判断一个对象是否存活或者是垃圾?
判断一个对象是否存活,分为两种算法,引用计数法和可达性分析算法:
- 引用计数法: 给每一个对象设置一个引用计数器,当有一个地方引用该对象的时候,引用计数器就+1,引用失效时,引用计数器就-1;当引用计数器为0的时候,就说明这个对象没有被引用,也就是垃圾对象,等待回收;
- 缺点:无法解决循环引用的问题,当A引用B,B也引用A的时候,此时AB对象的引用都不为0,此时也就无法垃圾回收,所以一般主流虚拟机都不采用这个方法;
- 可达性分析法:从一个被称为GC Roots的对象向下搜索,如果一个对象到GC Roots没有任何引用链相连接时,说明此对象不可用,在java中可以作为GC Roots的对象有以下几种:
- 虚拟机栈中引用的对象;
- 方法区类静态属性引用的变量;
- 方法区常量池引用的对象;
- 本地方法栈JNI引用的对象。
14、强引用、软引用、弱引用、虚引用是什么,有什么区别?
强引用:
- 概念:强引用,就是普通的对象引用关系,如 String s = new String("ConstXiong")
- 回收:只有所有GC Root对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
软引用:
- 概念:软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
- 回收:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象,可以配合引用队列来释放软引用自身。
弱引用:
- 概念:弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期。
- 回收:仅有弱引用引用该对象,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身。
虚引用:
- 概念:虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。
- 回收:必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。
15、被引用的对象就一定能存活吗?
不一定,需要看引用类型:
- 仅有弱引用引用该对象,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
- 但是仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象。
16、finalize()方法了解吗?有什么作用?
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
- 如果对象在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己 (this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它就"逃过一劫";但是如果没有抓住这个机会,那么对象就真的要被回收了。
17、方法区的垃圾回收?
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
废弃的常量:
- 只要常量池中的常量没有被任何地方引用,就可以被回收。
- 例如:一个字符串"abc”放入常量池,现在没有一个值为"abc"的字符串对象,也就是没有任何字符串对象引用常量池中“abc”的常量,且虚拟机其他地方没有引用这个字面量,如果发生垃圾回收且有必要时,常量会被系统清理出常量池。
不再使用的类型,需要同时满足下面3个条件才可以被允许回收:
- 该类的所有实例被回收,堆中不存在该类及其派生子类的实例。
- 加载该类的ClassLoader被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用
满足了上面的3个条件,只是被允许回收,不是一定会被回收,还需要设置一些参数进行控制。 虚拟机提供了-Xnoclassgc参数进行控制。
18、Java堆的内存分区了解吗?
- 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。
- 新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8 : 1 : 1。
- 新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
- 老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。
19、 垃圾回收算法有哪些? ⚡
Java中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法。
- 标记清除:
- 步骤:第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记; 第二步:再遍历一遍,将所有标记的对象回收掉。
- 优点:垃圾回收速度快。
- 缺点:空间不连续,容易产生内存碎片。
- 标记整理:
- 步骤:第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;第二步:在清理垃圾的过程中,会将可用的对象向前移动,保证内存的紧凑,连续的内存增多了。
- 优点:没有内存碎片。。
- 缺点:移动内存,改变地址,速度较慢 。
- 复制算法:
- 将内存划分为大小相等的两块区域:From和To,To区域空闲
- 步骤:
- 标记;
- 把From上存活的对象复制到To区域中,并在复制过程中进行整理;
- 此时From上剩余的都是垃圾了,直接清空From;
- 交换From和To。
- 优点: 不会产生内存碎片。
- 缺点: 占用双倍的内存空间。
- 分代收集算法:
- 根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收。