百度面试题:一个线程 OOM 后,其他线程还能运行吗?(下)

简介: 百度面试题:一个线程 OOM 后,其他线程还能运行吗?
  • 示例


1.png

  • 结果
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread

出现SOF时,会有明确错误堆栈可供分析,相对容易定位问题。如果使用HotSpot虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的)到达1000~2000没有问题,对于正常的方法调用(包括不能做尾递归优化的递归调用),这个深度应该完全够用。但如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量换取更多的线程。这种通过“减少内存”手段解决内存溢出的方式,如果没有这方面处理经验,一般比较难以想到。也是由于这种问题较为隐蔽,从 JDK 7起,以上提示信息中“unable to create native thread”后面,虚拟机会特别注明原因可能是“possibly


//

#define OS_NATIVE_THREAD_CREATION_FAILED_MSG  
  "unable to create native thread: possibly out of memory or process/resource limits reached"

3 方法区和运行时常量池溢出

运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起。


HotSpot从JDK 7开始逐步“去永久代”,在JDK 8中完全使用元空间代替永久代,那么方法区使用“永久代”还是“元空间”来实现,对程序有何影响呢。


String::intern()是一个本地方法:若字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池,并且返回此String对象的引用。


在JDK6或之前HotSpot虚拟机,常量池都是分配在永久代,可以通过如下两个参数:

image.png

限制永久代的大小,即可间接限制其中常量池的容量,

  • 实例

1.png

  • 结果
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 
  at java.lang.String.intern(Native Method) 
  at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)

可见,运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“PermGen space”,说明运行时常量池的确是属于方法区(即JDK 6的HotSpot虚拟机中的永久代)的 一部分。


而使用JDK 7或更高版本的JDK来运行这段程序并不会得到相同的结果,无论是在JDK 7中继续使 用-XX:MaxPermSize参数或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数把方法区容量同样限制在6MB,也都不会重现JDK 6中的溢出异常,循环将一直进行下去,永不停歇。

这种变化是因为自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆,所以在JDK 7及以上版 本,限制方法区的容量对该测试用例来说是毫无意义。


这时候使用-Xmx参数限制最大堆到6MB就能看到以下两种运行结果之一,具体取决于哪里的对象分配时产生了溢出:

// OOM异常一: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 
at java.base/java.lang.Integer.toString(Integer.java:440) 
at java.base/java.lang.String.valueOf(String.java:3058) 
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12) 
// OOM异常二: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/java.util.HashMap.resize(HashMap.java:699) 
at java.base/java.util.HashMap.putVal(HashMap.java:658) 
at java.base/java.util.HashMap.put(HashMap.java:607) 
at java.base/java.util.HashSet.add(HashSet.java:220) 
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)

字符串常量池的实现位置还有很多趣事:

image.png

JDK 6中运行,结果是两个false

JDK 7中运行,一个true和一个false

image.png

因为JDK6的intern()会把首次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代里这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在 Java 堆,所以不可能是同一个引用,结果将返回false。


JDK 7及以后的intern()无需再拷贝字符串的实例到永久代,字符串常量池已移到Java堆,只需在常量池里记录一下首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。


str2比较返回false,这是因为“java”这个字符串在执行String-Builder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,而“计算机软件”这个字符串则是首次 出现的,因此结果返回true!


对于方法区的测试,基本的思路是运行时产生大量类去填满方法区,直到溢出。虽然直接使用Java SE API也可动态产生类(如反射时的 GeneratedConstructorAccessor和动态代理),但操作麻烦。

借助了CGLib直接操作字节码运行时生成大量动态类。 当前的很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到 CGLib字节码增强,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。

很多运行于JVM的动态语言(例如Groovy)通常都会持续创建新类型来支撑语言的动态性,随着这类动态语言的流行,与如下代码相似的溢出场景也越来越容易遇到


在JDK 7中的运行结果:

Caused by: java.lang.OutOfMemoryError: PermGen space 
  at java.lang.ClassLoader.defineClass1(Native Method) 
  at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) 
  at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

JDK8及以后:可以使用

-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M

设置元空间初始大小以及最大可分配大小。


1.如果不指定元空间的大小,默认情况下,元空间最大的大小是系统内存的大小,元空间一直扩大,虚拟机可能会消耗完所有的可用系统内存。

2.如果元空间内存不够用,就会报OOM。

3.默认情况下,对应一个64位的服务端JVM来说,其默认的-XX:MetaspaceSize值为21MB,这就是初始的高水位线,一旦元空间的大小触及这个高水位线,就会触发Full GC并会卸载没有用的类,然后高水位线的值将会被重置。

4.从第3点可以知道,如果初始化的高水位线设置过低,会频繁的触发Full GC,高水位线会被多次调整。所以为了避免频繁GC以及调整高水位线,建议将-XX:MetaspaceSize设置为较高的值,而-XX:MaxMetaspaceSize不进行设置。


JDK8 运行结果:

image.png

一个类如果要被gc,要达成的条件比较苛刻。在经常运行时生成大量动态类的场景,就应该特别关注这些类的回收状况。


这类场景除了之前提到的程序使用了CGLib字节码增强和动态语言外,常见的还有:


  • 大量JSP或动态产生JSP 文件的应用(JSP第一次运行时需要编译为Java类)
  • 基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)


JDK8后,永久代完全废弃,而使用元空间作为其替代者。在默认设置下,前面列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区OOM。


为了让使用者有预防实际应用里出现类似于如上代码那样的破坏性操作,HotSpot还是提供了一些参数作为元空间的防御措施:

  • -XX:MetaspaceSize

指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整。如果释放了大量的空间,就适当降低该值,如果释放了很少空间,则在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值

  • -XX:MaxMetaspaceSize
    设置元空间最大值,默认-1,即不限制,或者说只受限于本地内存的大小
  • -XX:MinMetaspaceFreeRatio
    在GC后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的GC频率
  • -XX:Max-MetaspaceFreeRatio
    控制最大的元空间剩余容量的百分比


本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize指定,若不指定,则默认与Java堆最大值(-Xmx)一致。


这里越过DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配。

Unsafe类的getUnsafe()指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe,JDK10时才将Unsafe的部分功能通过VarHandle开放给外部。


因为虽然使用DirectByteBuffer分配内存也会抛OOM,但它抛异常时并未真正向os申请分配内存,而是通过计算得知内存无法分配,就在代码里手动抛了OOM,真正申请分配内存的方法是Unsafe::allocateMemory()


  • 使用unsafe分配本机内存

1.png

  • 结果

image.png

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显异常,若发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了 DirectMemory(比如使用NIO),则该考虑直接内存了。

目录
相关文章
|
21天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
23天前
|
消息中间件 前端开发 NoSQL
面试官:线程池遇到未处理的异常会崩溃吗?
面试官:线程池遇到未处理的异常会崩溃吗?
57 3
面试官:线程池遇到未处理的异常会崩溃吗?
|
23天前
|
消息中间件 存储 前端开发
面试官:说说停止线程池的执行流程?
面试官:说说停止线程池的执行流程?
34 2
面试官:说说停止线程池的执行流程?
|
27天前
|
消息中间件 前端开发 NoSQL
面试官:如何实现线程池任务编排?
面试官:如何实现线程池任务编排?
25 1
面试官:如何实现线程池任务编排?
|
1月前
|
Java Spring
运行@Async注解的方法的线程池
自定义@Async注解线程池
56 3
|
2月前
|
消息中间件 设计模式 安全
多线程魔法:揭秘一个JVM中如何同时运行多个消费者
【8月更文挑战第22天】在Java虚拟机(JVM)中探索多消费者模式,此模式解耦生产与消费过程,提升系统性能。通过`ExecutorService`和`BlockingQueue`构建含2个生产者及4个消费者的系统,实现实时消息处理。多消费者模式虽增强处理能力,但也引入线程安全与资源竞争等挑战,需谨慎设计以确保高效稳定运行。
69 2
【多线程面试题 一】、 创建线程有哪几种方式?
创建线程的三种方式包括继承Thread类、实现Runnable接口和实现Callable接口,其中Runnable和Callable接口方式更受推荐,因为它们允许多重继承并更好地体现面向对象思想。
|
2月前
|
Java 调度
【多线程面试题 五】、 介绍一下线程的生命周期
线程的生命周期包括新建、就绪、运行、阻塞和死亡状态,线程状态会根据线程的执行情况在这些状态之间转换。
【多线程面试题 五】、 介绍一下线程的生命周期
|
2月前
|
Java 编译器 开发工具
JDK vs JRE:面试大揭秘,一文让你彻底解锁Java开发和运行的秘密!
【8月更文挑战第24天】JDK(Java Development Kit)与JRE(Java Runtime Environment)是Java环境中两个核心概念。JDK作为开发工具包,不仅包含JRE,还提供编译器等开发工具,支持Java程序的开发与编译;而JRE仅包含运行Java程序所需的组件如JVM和核心类库。一个简单的"Hello, World!"示例展示了两者用途:需借助JDK编译程序,再利用JRE或JDK中的运行环境执行。因此,开发者应基于实际需求选择安装JDK或JRE。
41 0
|
2月前
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。

相关实验场景

更多
下一篇
无影云桌面