百度面试题:一个线程 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),则该考虑直接内存了。

目录
相关文章
|
2天前
|
消息中间件 前端开发 Java
美团面试:如何实现线程任务编排?
线程任务编排指的是对多个线程任务按照一定的逻辑顺序或条件进行组织和安排,以实现协同工作、顺序执行或并行执行的一种机制。 ## 1.线程任务编排 VS 线程通讯 有同学可能会想:那线程的任务编排是不是问的就是线程间通讯啊? 线程间通讯我知道了,它的实现方式总共有以下几种方式: 1. Object 类下的 wait()、notify() 和 notifyAll() 方法; 2. Condition 类下的 await()、signal() 和 signalAll() 方法; 3. LockSupport 类下的 park() 和 unpark() 方法。 但是,**线程通讯和线程的任务编排是
|
2天前
|
定位技术 API
Angular 调用导入百度地图API接口,2024春招BAT面试真题详解
Angular 调用导入百度地图API接口,2024春招BAT面试真题详解
|
2天前
|
机器学习/深度学习 PyTorch 算法框架/工具
神经网络基本概念以及Pytorch实现,多线程编程面试题
神经网络基本概念以及Pytorch实现,多线程编程面试题
|
2天前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
4天前
|
缓存 算法 自动驾驶
百度Cyber框架面试总结
百度Cyber框架面试总结
9 0
|
4天前
|
消息中间件 监控 前端开发
面试官:核心线程数为0时,线程池如何执行?
线程池是 Java 中用于提升程序执行效率的主要手段,也是并发编程中的核心实现技术,并且它也被广泛的应用在日常项目的开发之中。那问题来了,如果把线程池中的核心线程数设置为 0 时,线程池是如何执行的? 要回答这个问题,我们首先要了解在正常情况下,线程池的执行流程,也就是说当有一个任务来了之后,线程池是如何运行的? ## 1.线程池的执行流程 正常情况下(核心线程数不为 0 的情况下)线程池的执行流程如下: 1. **判断核心线程数**:先判断当前工作线程数是否大于核心线程数,如果结果为 false,则新建线程并执行任务。 2. **判断任务队列**:如果大于核心线程数,则判断任务队列是否
23 1
面试官:核心线程数为0时,线程池如何执行?
|
4天前
|
监控 安全 Java
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
|
4天前
|
存储 安全 Java
这些年背过的面试题——多线程篇
本文是技术人面试系列多线程篇,面试中关于多线程都需要了解哪些基础?一文带你详细了解,欢迎收藏!
|
4天前
|
监控 安全
线程死循环是多线程应用程序开发过程中一个难以忽视的问题,它源于线程在执行过程中因逻辑错误或不可预见的竞争状态而陷入永久运行的状态,严重影响系统的稳定性和资源利用率。那么,如何精准定位并妥善处理线程死循环现象,并在编码阶段就规避潜在风险呢?谈谈你的看法~
避免线程死循环的关键策略包括使用同步机制(如锁和信号量)、减少共享可变状态、设置超时、利用监控工具、定期代码审查和测试、异常处理及设计简洁线程逻辑。通过这些方法,可降低竞态条件、死锁风险,提升程序稳定性和可靠性。
19 0
|
4天前
|
安全 Java
面试官:线程调用2次start会怎样?我支支吾吾没答上来
面试官:线程调用2次start会怎样?我支支吾吾没答上来
14 1

相关实验场景

更多