本博文主要分享在JVM中的大厂面试问题。总结相关面试问题与yu解答。帮助大家更好的学习与理解JVM的原理。同时也是分享一些有关于JVM实战的经验,帮助大家在工作中排查错误。
一、JVM的内存模型 1.1 JVM1.8内存模型线程独占:栈,本地方法栈,程序计数器,线程共享:堆,方法区
- 栈:又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法 出口等信息.调用方法时执行入栈,方法返回式执行出栈.
- 堆:JVM内存管理最大的一块,对被线程共享,目的是存放对象的实例,几乎所欲的对象实例都会放在这里,当堆没有可用空间时,抛出OOM异常.根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行和管理。
- 方法区: 又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据.1.7的永 久代和1.8的元空间都是方法区的一种实现。
- 本地方法栈: 与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈.
- 程序计数器: 保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行 Native方法时,程序计数器为空。
- 栈由操作系统自动分配释放 ,存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。
- 堆由程序员分配释放,若程序员不释放,程序结束时由OS回收,分配方式倒是类似于链表。
- 管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
- 空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小64bits的Windows默认1M,64bits的Linux默认10M;
- 生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
- 分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
- 分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
- 栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。 堆(heap):用于存储对象。
栈溢出原因是方法执行时创建的栈帧超过了栈的深度。可能的就是方法递归调用产生这种结果。
堆内存溢出(OOM)的常见原因有哪些?堆内存不足: 这种场景最为常见,报错信息:java.lang.OutOfMemoryError: Java heap space
- 原因:
- 代码中可能存在大对象分配
- 可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
- 解决方法
- 检查是否存在大对象的分配,最有可能的是大数组分配
- 通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
- 如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
- 还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
永久代/元空间溢出 java.lang.OutOfMemoryError: PermGen space java.lang.OutOfMemoryError: Metaspace
- 原因
- 永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。
- JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:字符串常量由永久代转移到堆中,和永久代相关的JVM参数已移除.
- 在Java7之前,频繁的错误使用String.intern方法.
- 生成了大量的代理类,导致方法区被撑爆,无法卸载.
- 应用长时间运行,没有重启.
- 永久代/元空间溢出的解决方法有如下几种:
- 检查是否永久代空间或者元空间设置的过小
- 检查代码中是否存在大量的反射操作
- dump之后通过mat检查是否存在大量由于反射生成的代理类
- 放大招,重启JVM
GC overhead limit exceeded java.lang.OutOfMemoryError:GC overhead limit exceeded
- 原因
- 这个是JDK6新加的错误类型,一般都是堆太小导致的。
- Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
- 解决方法
- 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
- 添加参数-XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
- dump内存,检查是否存在内存泄露,如果没有,加大内存。
方法栈溢出 java.lang.OutOfMemoryError : unable to create new native Thread
- 原因
- 出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。
- 解决方法
- 通过-Xss降低的每个线程栈大小的容量
- 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
- /proc/sys/kernel/pid_max
- /proc/sys/kernel/thread-max
- max_user_process(ulimit -u)
- /proc/sys/vm/max_map_count
分配超大数组 java.lang.OutOfMemoryError: Requested array size exceeds VM limit
- 这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable), 如果不能寻址(addressable)就会抛出这个错误。
- 解决方法就是检查你的代码中是否有创建超大数组的地方。
swap区溢出 java.lang.OutOfMemoryError: Out of swap space
- 这种情况一般是操作系统导致的,可能的原因有:
- swap 分区大小分配不足;
- 其他进程消耗了所有的内存。
- 解决方案:
- 其它服务进程可以选择性的拆分出去
- 加大swap分区大小,或者加大机器内存大小
本地方法溢出 java.lang.OutOfMemoryError: stack_trace_with_native_method
- 本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处。 这个异常出现的概率极低,就算出现,只能通过操作系统本地工具进行诊断,难度有点大,还是放弃为妙。
遇到过元空间溢出吗?
- 元空间(Metaspace)默认是没有上限的,不加限制比较危险。当应用中的Java类过多,比如Spring等一些使用动态代理的框架生成了很多类,如果占用空间超出了我们的设定值,就会发生元空间溢出。所以,默认风险大,但如果你不给足它空间,它也会溢出。
遇到过堆外内存溢出吗?
- 使用了Unsafe类申请内存,或者使用了JNI对内存进行操作。这部分内存是不受JVM控制的,不加限制的使用,容易发生内存溢出。
JVM的永久代中会发生垃圾回收么
- Full GC为一次特殊GC行为的描述,这次GC会回收整个堆的内存,包含老年代,新生代,metaspace等.
- 而1.7以前的jdk采用的是永久代作为方法区的实现,在1.7及以前的jdk版本,永久代的空间不足也会导致fullGC,1.7以前,永久代空间如果设小了,就会触发整个堆的一次full GC(注意是触发堆的full GC),经过这样的一次定位就初步定位到了是由于永久代空间不足导致了堆的full GC。
- 所以垃圾回收会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如 果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小 对避免Full GC是非常重要的原因。
- 但是在1.8以后由于改成了元空间,它的垃圾回收就不是由java来控制了,元空间的默认情况下内存空间是使用的操作系统的内存空间,所以空间的容量是比较充裕的,发生OOMM的概率较小,但是也有可能发生。
我们从编译原理讲起,不同的开发环境、开发语言都会有不同的策略。一般来说,程序运行时有三种内存分配策略:静态的、栈式的、堆式的
- 静态存储:是指在编译时就能够确定每个数据目标在运行时的存储空间需求,因而在编译时就可以给它们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间。
- 栈式存储:栈式存储分配是动态存储分配,由一个类似于堆栈的运行栈来实现,和静态存储的分配方式相反。 在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到了运行的时候才能知道,但是规定在运行中进入一个程序模块的时候,必须知道该程序模块所需要的数据区的大小才能分配其内存。和我们在数据结构中所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。
- 堆式存储:堆式存储分配专门负责在编译时或运行时,无法确定存储要求的数据结构的内存分配。比如可变长度串和对象实例,堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。
- JAVA 强引用:在Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一 个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永 远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
- JAVA软引用: 软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回 收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中 。
- JAVA弱引用:弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只 要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。在 Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
- JAVA虚引用:虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状态。
对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。
- 对象头占 12B,包括对象标记和类型指针。对象标记存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等,这部分占 8B,称为 Mark Word。Mark Word 被设计为动态数据结构,以便在极小的空间存储更多数据,根据对象状态复用存储空间。类型指针是对象指向它的类型元数据的指针,占 4B。JVM 通过该指针来确定对象是哪个类的实例。
- 实例数据是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。
- 对齐填充不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是 8B 的倍数,对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐,需要对齐填充补全。
对象创建过程分为以下几步:
- 检查类是否已经被加载:new关键字时创建对象时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程。类的加载过程需要经历:加载、链接、初始化三个阶段。
- 为对象分配内存空间: 此时,对象所属类已经加载,现在需要在堆内存中为该对象分配一定的空间,该空间的大小在类加载完成时就已经确定下来了。为对象分配内存空间有两种方式:
- 第一种jvm将堆区抽象为两块区域,一块是已经被其他对象占用的区域,另一块是空白区域,中间通过一个指针进行标注,这时只需要将指针向空白区域移动相应大小空间,就完成了内存的分配,当然这种划分的方式要求虚拟机的对内存是地址连续的,且虚拟机带有内存压缩机制,可以在内存分配完成时压缩内存,形成连续地址空间,这种分配内存方式成为“指针碰撞”,但是很明显,这种方式也存在一个比较严重的问题,那就是多线程创建对象时,会导致指针划分不一致的问题,例如A线程刚刚将指针移动到新位置,但是B线程之前读取到的是指针之前的位置,这样划分内存时就出现不一致的问题,解决这种问题,虚拟机采用了循环CAS操作来保证内存的正确划分。
- 第二种也是为了解决第一种分配方式的不足而创建的方式,多线程分配内存时,虚拟机为每个线程分配了不同的空间,这样每个线程在分配内存时只是在自己的空间中操作,从而避免了上述问题,不需要同步。当然,当线程自己的空间用完了才需要需申请空间,这时候需要进行同步锁定。为每个线程分配的空间称为“本地线程分配缓冲(TLAB)”,是否启用TLAB需要通过 -XX:+/-UseTLAB参数来设定。
- 为对象的字段赋默认值:分配完内存后,需要对对象的字段进行零值初始化(赋默认值),对象头除外。 值初始化意思就是对对象的字段赋0值,或者null值,这也就解释了为什么这些字段在不需要进程初始化时候就能直接使用。
- 设置对象头:对这个将要创建出来的对象,进行信息标记,包括是否为新生代/老年代,对象的哈希码,元数据信息,这些标记存放在对象头信息中。
- 执行实例的初始化方法lint:linit方法包含成员变量、构造代码块的初始化,按照声明的顺序执行。
- 执行构造方法:执行对象的构造方法。至此,对象创建成功。
- 父类静态成员和静态初始化块
- 子类静态成员和静态初始化块
- 父类实例成员和实例初始化块
- 父类构造方法
- 子类实例成员和实例初始化块
- 子类构造方法
- 父类普通函数
- 子类普通函数
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次 Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到 阀值对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一 半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如 果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设 置,如果true则只进行Monitor GC,如果false则进行Full GC。
对象的优先分配在年轻代? 不是。当新生代内存不够时,老年代分配担保。而大对象则是直接在老年代分配。
2.5 新生代和老年代jvm中的堆分为新生代和老年代
- 新生代用来存放新生的对象,新生代中的对象朝生夕死,所以会频繁的触发 minor (脉了)GC 进行垃圾回收。新生代分为 eden 区、survivor from 区和 survivor to 区。eden区是java新对象的出生地,如果新创建的对象占用内存很大的话就会直接分配到老年代。当eden区的内存不足时就会触发 minor gc 对新生代进行一次垃圾回收。survivor from 区存放的是上一次minor gc 的幸存者,它将作为这一次gc的被扫描者。survivor to 区会保留这一次gc的幸存者。
- 新生代 minor gc 的流程是:它采用的复制算法,首先eden区和survivor from区中存活的对象复制到survivor to区域,并将它们的年龄加一。然后清空eden区和survivor from区中的对象,接着将survivor from和survivor to互换,也就是原先的survivor to成为下一次gc时的survivor from。(这样要注意的是,如果有对象的年龄达到了老年代的标准,就放进老年代;如果survivor to区域的空间不够的话,就会通过分配担保机制,将多出来的对象提前转到老年代,但老年代要进行担保的前提是自己本身还有容纳这些对象的剩余空间,由于无法提前知道会有多少对象存活下来,所以这里是取之前每次晋升到老年代的对象的平均大小作为经验值,与老年代的剩余空间做比较)
- 老年代主要存放生命周期较长的内存对象,所以不会频繁的进行垃圾回收。老年代采用的是标记清除算法,也就是首先扫描一次老年代,标记出存活对象,然后回收没有标记的对象。
- java8之前,jvm堆中还有一块称作永久代的区域,主要存放class和元数据的信息,class被加载的时候就会被放入永久代,gc不会在主程序运行期间对永久代进行清理,这样会导致一个问题,就是永久代区域会随着加载的class的增多而胀满,最终抛出OOM异常。
- java8移除了永久代,取而代之的是一个叫做元数据区的概念,也叫做元空间。元空间和永久代是类似的,但它们最大的区别是元空间并不在虚拟机中,而是使用的本地内存,因此默认情况下,元空间的大小仅受本地内存的限制。也就是类的元数据放入本地内存中,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就由系统实际可用空间来控制了。
在Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永 久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此, 默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 nativememory, 字符串池和类的静态 变量放入 java 堆中, 这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用 空间来控制。
- 永久代和元空间的作用都是存储类的元数据,用来存储class相关信息,包括class对象的Method,Field等。
- 永久代和元空间的区别本质只有一个,就是永久代使用的是JVM内存存储,元空间使用的是本地内存存储。
什么要废除永久代
- 由于永久代内存经常不够用或者发生内存泄露,爆出异常 java.lang.OutOfMemoryError: PermGen 。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会位GC带来不必要的复杂度,而且回收效率偏低。
什么情况会造成元空间溢出?
- JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:字符串常量由永久代转移到堆中,和永久代相关的JVM参数已移除.
- 在Java7之前,频繁的错误使用String.intern方法.
- 生成了大量的代理类,导致方法区被撑爆,无法卸载.
- 应用长时间运行,没有重启.
- 永久代/元空间溢出的解决方法有如下几种:
- 检查是否永久代空间或者元空间设置的过小
- 检查代码中是否存在大量的反射操作
- dump之后通过mat检查是否存在大量由于反射生成的代理类
- 放大招,重启JVM
对象进入老年代,有三种情况:
- 占用内存较大的对象,直接进入老年代,这个“大”由参数-XX:PretenureSizeThreshold来决定,超过这个参数设置的值就直接进入老年代,例如很长的字符串、很大的数组。
- 正常创建一个对象,对象内存布局,包含三部分信息(对象头、实例数据、对齐数据),对象头中存储的就是两部分信息,一部分是对象的运行时数据(GC年龄、锁信息等),一部分是类型指针,GC年龄在对象初始化时为1,每经过一次minorGC年龄增1,达到系统设置XX:MaxTenuringThreshold年龄值之后,进入老年代。
- 当一个对象从Eden区到了Survivor区,当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而不需要达到默认的分代年龄
当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后 产生与所加载类对应的 Class 对象。
加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段 包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三 个步骤。最后 JVM 对 类进行初始化,包括:
- 如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
- 如果类中存在初始化语句,就依次执行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、 系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader 的子类)。类加载有几个过程:加载、验证、准备、解析、初始化。
- 启动类加载器(Bootstrp ClassLoader),加载 /lib/rt.jar、-Xbootclasspath。
- 扩展类加载器(Extension ClassLoader)sun.misc.Launcher$ExtClassLoader,加载 /lib/ext、java.ext.dirs。
- 应用程序类加载器(Application ClassLoader,sun.misc.Launcher$AppClassLoader),加载 CLASSPTH、-classpath、-cp、Manifest。
- 自定义类加载器(user ClassLoader)。
JVM类加载为什么要使用双亲委派模式?双亲委托模型的重要用途是为了解决类载入过程中的安全性问题。
- 假设有一个开发者自己编写了一个名为java.lang.Object的类,想借此欺骗JVM。现在他要使用自定义ClassLoader来加载自己编写的java.lang.Object类。
- 然而幸运的是,双亲委托模型不会让他成功。因为JVM会优先在Bootstrap ClassLoader的路径下找到java.lang.Object类,并载入它。
Java的类加载是否一定遵循双亲委托模型?
- 在实际开发中,我们可以通过自定义ClassLoader,并重写父类的loadClass方法,来打破这一机制。
- SPI就是打破了双亲委托机制的(SPI:服务提供发现)。
GC 是 JVM 内部的一个进程,回收无效对象的内存用于将来的分配。
3.2 判断一个对象是否为垃圾?- 引用计数器,也就是为每一个对象添加一个引用计数器,用来统计指向当前对象的引用次数,如果当前对象存在应用的更新,那么就对这个引用计数器进行增加,一旦这个引用计数器变成0,就意味着它可以被回收了。这种方法需要额外的空间来存储引用计数器,但是它的实现很简单,而且效率也比较高。不过主流的JVM都没有采用这种方式,因为引用计数器在处理一些复杂的循环引用或者相互依赖的情况时,可能会出现一些不再使用但是又无法回收的内存,造成内存泄露的问题。
- 可达性分析,它的主要思想是,首先确定一系列肯定不能回收的对象作为GC root,比如虚拟机栈里面的引用对象、本地方法栈引用的对象等,然后以GC ROOT作为起始节点,从这些节点开始向下搜索,去寻找它的直接和间接引用的对象,当遍历完之后如果发现有一些对象不可到达,那么就认为这些对象已经没有用了,需要被回收。在垃圾回收的时候,JVM会首先找到所有的GC root,这个过程会暂停所有用户线程,也就是stop the world,然后再从GC Roots这些根节点向下搜索,可达的对象保留,不可达的就会回收掉。
可达性分析是目前主流JVM使用的算法。
标记清除算法( Mark-Sweep): 最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段 回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
复制算法(copying):为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两 块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉 。
标记整理算法(Mark-Compact): 结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同, 标记后不是清理对 象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
分代收集算法:分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为 不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代 (YoungGeneration)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次 垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
新生代与复制算法:目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分 对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大 的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
老年代与标记复制算法:而老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。
- JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation), 它用来存储 class 类,常量, 方法描述等。对永生代的回收主要包括废弃常量和无用的类。
- 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存 放对象的那一块),少数情况会直接分配到老生代。
- 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后, EdenSpace 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 FromSpace 进行清理。
- 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
- 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
- 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。 默认情况下年龄到达 15 的对象会被移到 老生代中。
分为新生代和老年代,新生代默认占总空间的 1/3,老年代默认占 2/3。新生代使用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
当新生代中的 Eden 区内存不足时,就会触发 Minor GC,过程如下:
- 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
- Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
- 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代
- Survivor 区相同年龄所有对象大小的总和 (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%
- Survivor 区内存不足会发生担保分配
- 超过指定大小的对象可以直接进入老年代
Major GC,指的是老年代的垃圾清理,但并未找到明确说明何时在进行Major GC
FullGC,整个堆的垃圾收集,触发条件:
- 每次晋升到老年代的对象平均大小>老年代剩余空间。
- MinorGC后存活的对象超过了老年代剩余空间。
- 元空间不足。
- System.gc() 可能会引起。
- CMS GC异常,promotion failed:MinorGC时,survivor空间放不下,对象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC时,同时有对象要放入老年代,而老年代空间不足造成
- 堆内存分配很大的对象。
- Minor GC:发生在年轻代的 GC。当Eden区满时,触发Minor GC。
- Major GC:发生在老年代的 GC。
- Full GC:全堆垃圾回收。比如 Metaspace 区引起年轻代和老年代的回收。
执行的Full GC的场景包括了:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行。
- 老年代空间不足。
- 方法区空间不足。
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对巨日应用最合适的收集器,进行分代收集。
- 串行 GC(Serial GC):单线程执行,应用需要暂停;
- 并行 GC(ParNew、Parallel Scavenge、Parallel Old):多线程并行地执行垃圾回收,关注与高吞吐;
- CMS(Concurrent Mark-Sweep):多线程并发标记和清除,关注与降低延迟;
- G1(G First):通过划分多个内存区域做增量整理和回收,进一步降低延迟;
- ZGC(Z Garbage Collector):通过着色指针和读屏障,实现几乎全部的并发执行,几毫秒级别的延迟,线性可扩展;
- Epsilon:实验性的 GC,供性能分析使用;
- Shenandoah:G1 的改进版本,跟 ZGC 类似。
- JDK 8 的默认 GC 是什么?很多人或觉得是 CMS,甚至 G1,其实都不是。答案是:并行 GC 是 JDK8 里的默认 GC 策略。注意,G1 成为 JDK9 以后版本的默认 GC 策略,同时,ParNew + SerialOld 这种组合不被支持。
- 初始标记
- 并发标记
- 并发预清理
- 并发可取消的预清理
- 重新标记
- 并发清理
CMS的问题:
- 内存碎片问题。Full GC的整理阶段,会造成较长时间的停顿。
- 需要预留空间,用来分配收集阶段产生的“浮动垃圾“。
- 使用更多的CPU资源,在应用运行的同时进行堆扫描。
- 停顿时间是不可预期的。
JDK11 中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。
基于Region内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。ZGC的 Region 具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。
3.8 什么是分布式垃圾回收(DGC)?它是如何工作的?
四、JVM的内存问题 4.1 生产环境中的CPU过高的原因?如何排查?如何解决? 4.1.1 CPU过高原因- CAS修改值失败,没有控制自旋次数,导致一直自旋不断重试,非常消耗cpu资源。
- 程序死循环:控制循环次数。
- 系统频繁的进行Full GC。
- 云服务器被黑客攻击,植入了挖矿程序:端口不能够被外网访问。
- tomcat服务器的并发量很大。服务器被DDOS攻击。
先用top命令找出cpu占比最高的。
ps -ef或者jps进一步定位,得知是一个怎样的后台程序在给我们惹事。
ps -mp 进程 -o THREAD,tid,time 定位到具体的线程或者代码。
将需要的线程id转化成16进制格式(英文小写格式)。
jstack 进程id | grep tid(十六进制线程id英文小写) -A60
利用的Arthas工具排查
[xjl@56988]$ thread -n 3
4.1.3 CPU过高解决方案
- CAS:限制CAS的次数。
- 程序破除死循环。
- 系统修改频繁的进行Full GC。
- 云服务器被黑客攻击,端口不能够被外网访问,建议Redis部署在内网,不要公开在外网。
- 服务器被DDOS攻击:限流、ip黑名单、图形验证码。
oom就是我们常说的内存溢出,它是指需要的内存空间大于系统分配的内存空间,oom后果就是项目程序crash;
- 请求创建一个超大对象,通常是一个大数组。(所以尽量根据自己的实际需要去初始化数组大小)
- 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。当流程突然很高是,由于提前没有对堆的内存空间做合理的准备,所以短时间内线程会创建大量的对象,这些对象可能会短时间内迅速的占满堆内存。
- 用终结器(Finalizer),该对象没有立即被 GC。
- 内存泄漏,大量的对象引用没有释放,GC没有办法对这些内存空间进行回收导致了内存泄漏的问题。
- GC overhead limit exceeded
- 当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出 java.lang.OutOfMemoryError:GC overhead limit exceeded 错误。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。 此类问题的原因与解决方案跟 Javaheap space非常类似
- Permgen space
- 该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。
- 永久代存储对象主要包括以下几类:
- 加载/缓存到内存中的 class 定义,包括类的名称,字段,方法和字节码;
- 常量池;
- 对象数组/类型数组所关联的 class;
- JIT 编译器优化后的 class 信息。 PermGen 的使用量与加载到内存的 class 的数量/大小正相关。
-
- 根据 Permgen space 报错的时机,可以采用不同的解决方案,如下所示:
- 程序启动报错,修改 -XX:MaxPermSize 启动参数,调大永久代空间。
- 应用重新部署时报错,很可能是没有应用没有重启,导致加载了多份 class 信息,只需重启 JVM 即可解决。
- 运行时报错,应用程序可能会动态创建大量class,而这些 class 的生命周期很短暂,但是 JVM 默认不会卸载 class,可以设置 -XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC 这两个参数允许 JVM 卸载 class。 如果上述方法无法解决,可以通过 jmap 命令dump内存对象 jmap-dump:format=b,file=dump.hprof ,然后利用MAT功能逐一分析开销最大的classloader 和重复class。
- 根据 Permgen space 报错的时机,可以采用不同的解决方案,如下所示:
- Metaspace
- JDK 1.8 使用 Metaspace 替换了永久代(Permanent Generation),该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大。 此类问题的原因与解决方法跟 Permgenspace 非常类似,可以参考上文。需要特别注意的是调整 Metaspace 空间大小的启动参数为 -XX:MaxMetaspaceSize。
- Unable to create new native thread
- 每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会报此类错误。
- JVM 向 OS 请求创建 native 线程失败,就会抛出 Unableto createnewnativethread,常见的原因包括以下几类:
- 线程数超过操作系统最大线程数 ulimit 限制;
- 线程数超过 kernel.pid_max(只能重启);
- native 内存不足;
-
- 该问题发生的常见过程主要包括以下几步:
- JVM 内部的应用程序请求创建一个新的 Java 线程;
- JVM native 方法代理了该次请求,并向操作系统请求创建一个 native 线程;
- 操作系统尝试创建一个新的 native 线程,并为其分配内存;
- 如果操作系统的虚拟内存已耗尽,或是受到 32 位进程的地址空间限制,操作系统就会拒绝本次 native 内存分配;
- JVM 将抛出 java.lang.OutOfMemoryError:Unableto createnewnativethread错误。
- 该问题发生的常见过程主要包括以下几步:
-
- 解决方案
- 升级配置,为机器提供更多的内存;
- 降低 Java Heap Space 大小;
- 修复应用程序的线程泄漏问题;
- 限制线程池大小;
- 使用 -Xss 参数减少线程栈的大小;
- 调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制。
- 解决方案
- Out of swap space?
- 该错误表示所有可用的虚拟内存已被耗尽。虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成。 当运行时程序请求的虚拟内存溢出时就会报Outof swap space错误。
- 该错误出现的常见原因包括以下几类:
- 地址空间不足;
- 物理内存已耗光;
- 应用程序的本地内存泄漏(native leak),例如不断申请本地内存,却不释放。
- 执行 jmap-histo:live 命令,强制执行 Full GC;如果几次执行后内存明显下降,则基本确认为 Direct ByteBuffer 问题。
-
- 根据错误原因可以采取如下解决方案:
- 升级地址空间为 64 bit;
- 使用 Arthas 检查是否为 Inflater/Deflater 解压缩问题,如果是,则显式调用 end 方法。
- Direct ByteBuffer 问题可以通过启动参数 -XX:MaxDirectMemorySize 调低阈值。
- 升级服务器配置/隔离部署,避免争用。
- 根据错误原因可以采取如下解决方案:
- Kill process or sacrifice child
- 不同于其他的 OOM 错误, Killprocessorsacrifice child 错误不是由 JVM 层面触发的,而是由操作系统层面触发的。
- 默认情况下,Linux 内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。 然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活 OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源。
- 解决方案
- 升级服务器配置/隔离部署,避免争用。
- OOM Killer 调优。
- Requested array size exceeds VM limit
- JVM 限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制。 JVM 在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为Integer.MAX_VALUE-2。 此类问题比较罕见,通常需要检查代码,确认业务是否需要创建如此大的数组,是否可以拆分为多个块,分批执行。
- Direct buffer memory
- Direct ByteBuffer 的默认大小为 64 MB,一旦使用超出限制,就会抛出 Directbuffer memory 错误。
- 解决方案
- Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查。
- 检查是否直接或间接使用了 NIO,如 netty,jetty 等。
- 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值。
- 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效。
- 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间。
- 内存容量确实不足,升级配置。
使用 top 命令排查生产上 CPU 和内存占比比较高的进程:
使用 top -Hp PID 观察哪一个线程占比较高 :
使用 jstack PID 定位线程
使用 jstat -gc pid 打印 GC 信息(这个命令查看起来并不直观,不如使用 jconsole)
- 对大多数的内存溢出的情况,只需要调整jvm的堆内存空间就可以解决该问题,如果还没有解决可以根据以下几种情况进行排查
- 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制。
- 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。
- 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接。
- System.gc()方法的调用。此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。
- 老年代空间不足。 老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space 。为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
- Permanet Generation空间满了。Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出错误信息:java.lang.OutOfMemoryError: PermGen space 。为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代可用内存不足(老年代可用内存小于该对象)
查看堆内存各区域的使用率以及GC情况
[xjl@56988]$ jstat -gcutil -h20 pid 1000
查看堆内存中的存活对象,并按空间排序
[xjl@56988]$ jmap -histo pid | head -n20
dump堆内存文件
[xjl@56988]$ jmap -dump:format=b,file=heap pid
4.4.3 fullGC问题解决方案
4.5 JVM 配置参数有哪些?
4.5.1 日志
- -XX:+PrintFlagsFinal,打印JVM所有参数的值
- -XX:+PrintGC,打印GC信息
- -XX:+PrintGCDetails,打印GC详细信息
- -XX:+PrintGCTimeStamps,打印GC的时间戳
- -Xloggc:filename,设置GC log文件的位置
- -XX:+PrintTenuringDistribution,查看熬过收集后剩余对象的年龄分布信息
- -Xms,设置堆的初始化内存大小
- -Xmx,设置堆的最大内存
- -Xmn,设置新生代内存大小
- -Xss,设置线程栈大小
- -XX:NewRatio,新生代与老年代比值
- -XX:SurvivorRatio,新生代中Eden区与两个Survivor区的比值,默认为8,即Eden:Survivor:Survivor=8:1:1
- -XX:MaxTenuringThreshold,从年轻代到老年代,最大晋升年龄。CMS 下默认为 6,G1 下默认为 15
- -XX:MetaspaceSize,设置元空间的大小,第一次超过将触发 GC
- -XX:MaxMetaspaceSize,元空间最大值
- -XX:MaxDirectMemorySize,用于设置直接内存的最大值,限制通过 DirectByteBuffer 申请的内存
- -XX:ReservedCodeCacheSize,用于设置 JIT 编译后的代码存放区大小,如果观察到这个值有限制,可以适当调大,一般够用即可
- -XX:+UseSerialGC,设置串行收集器
- -XX:+UseParallelGC,设置并行收集器
- -XX:+UseConcMarkSweepGC,使用CMS收集器
- -XX:ParallelGCThreads,设置Parallel GC的线程数
- -XX:MaxGCPauseMillis,GC最大暂停时间 ms
- -XX:+UseG1GC,使用G1垃圾收集器
- -XX:+UseCMSInitiatingOccupancyOnly。
- -XX:CMSInitiatingOccupancyFraction,与前者配合使用,指定MajorGC的发生时机。
- -XX:+ExplicitGCInvokesConcurrent,代码调用 System.gc() 开始并行 FullGC,建议加上这个参数。
- -XX:+CMSScavengeBeforeRemark,表示开启或关闭在 CMS 重新标记阶段之前的清除(YGC)尝试,它可以降低 remark 时间,建议加上。
- -XX:+ParallelRefProcEnabled,可以用来并行处理 Reference,以加快处理速度,缩短耗时。
- -XX:MaxGCPauseMillis,用于设置目标停顿时间,G1 会尽力达成。
- -XX:G1HeapRegionSize,用于设置小堆区大小,建议保持默认。
- -XX:InitiatingHeapOccupancyPercent,表示当整个堆内存使用达到一定比例(默认是 45%),并发标记阶段就会被启动。
- -XX:ConcGCThreads,表示并发垃圾收集器使用的线程数量,默认值随JVM运行的平台不同而变动,不建议修改。
- jps:用来显示本地的 Java 进程,可以查看本地运行着几个 Java 程序,并显示他们的进程号。 命令格式:jps
- jinfo:运行环境参数:Java System 属性和 JVM 命令行参数,Java class path 等信息。 命令格式:jinfo 进程 pid
- jstat:监视虚拟机各种运行状态信息的命令行工具。 命令格式:jstat -gc 123 250 20
- jstack:可以观察到 JVM 中当前所有线程的运行情况和线程当前状态。 命令格式:jstack 进程 pid
- jmap:观察运行中的 JVM 物理内存的占用情况(如:产生哪些对象,及其数量)。 命令格式:jmap [option] pid
为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。
5.2 什么是指令重排序?指令重排:为了提高性能,编译器和和处理器通常会对指令进行指令重排序。
图中的三个重排位置可以调换的,根据系统优化需要进行重排。遵循的原则是单线程重排后的执行结果要与顺序执行结果相同。内存屏障指令:volatile在指令之间插入内存屏障,保证按照特定顺序执行和某些变量的可见性。volatile就是通过内存屏障通知cpu和编译器不做指令重排优化来维持有序性。
5.3 safepoint是什么?STW并不会只发生在内存回收的时候。现在程序员这么卷,碰到几次safepoint的问题几率也是比较大的。当发生GC时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为JVM是安全的(safe),整个堆的状态是稳定的。 如果在GC前,有线程迟迟进入不了safepoint,那么整个JVM都在等待这个阻塞的线程,造成了整体GC的时间变长。
5.4 SWAP会影响性能么?当操作系统内存不足的时候,会将部分数据写入到SWAP交换分中,但是SWAP的性能是比较低的。如果应用的访问量较大,需要频繁申请和销毁内存,就容易发生卡顿。一般高并发场景下,会禁用SWAP。
5.5 如何找到死锁的线程?通过jstack命令,可以获得线程的栈信息。死锁信息会在非常明显的位置(一般是最后)进行提示。
5.6 你能保证GC执行吗?不能,虽然你可以调用 System.gc() 或者 Runtime.gc(),但是没有办法保证 GC的执行。
5.7 生产上如何配置垃圾收集器的? 5.8 invokedynamic指令是干什么的?invokedynamic是Java7之后新加入的字节码指令,使用它可以实现一些动态类型语言的功能。我们使用的Lambda表达式,在字节码上就是invokedynamic指令实现的。它的功能有点类似反射,但它是使用方法句柄实现的,执行效率更高。
5.9 能够找到Reference Chain的对象,就一定会存活么?JVM判断对象是否是垃圾采用的是可达性分析算法,通过 GC Roots 来判定对象存活,从GC Roots向下追溯、搜索,会产生一个叫做 Reference Chain 的链条,但是能够找到 Reference Chain 的对象却不一定会存活,还得考虑到对象的引用类型,比如如果对象是软引用类型,那么在堆内存不足时,该对象就会在GC时被回收,而如果对象是弱引用类型,那么只要发生了GC,该对象就会被回收。 因此能够找到 Reference Chain 的对象,不一定会存活,但是找不到 Reference Chain 的对象,就一定会被回收。
5.10 HashMap中的key,可以是普通对象么?需要什么注意的地方?Map的key和value都可以是任何类型。但要注意的是,一定要重写它的equals和hashCode方法,否则容易发生内存泄漏。
Java中内存泄露问题
内存泄露的标准定义是: 尽管对象不再被程序所使用,但垃圾回收器却无法将其回收的情况——因为对象仍然处于被引用的状态。 久而久之,不能被回收的内存越来越多,最终导致内存溢出OOM(OutOfMemoryError)。
获取的线程栈信息
- jps:获得进程号。
- top -Hp pid:获取本进程中所有线程的CPU耗时性能
- jstack pid :命令查看当前java进程的堆栈状态 或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。可以使用fastthread 堆栈定位(fastthread.io)
堆外内存的排查思路? 进程占用的内存,可以使用top命令,看RES段占用的值。如果这个值大大超出我们设定的最大堆内存,则证明堆外内存占用了很大的区域。使用gdb可以将物理内存dump下来,通常能看到里面的内容。更加复杂的分析可以使用perf工具,或者谷歌开源的gperftools。那些申请内存最多的native函数,很容易就可以找到。
4.8 那些手段排除内存溢出问题?内存溢出包含很多种情况,我在平常工作中遇到最多的就是堆溢出。有一次线上遇到故障,重新启动后,使用jstat命令,发现Old区在一直增长。我使用jmap命令,导出了一份线上堆栈,然后使用MAT进行分析。通过对GC Roots的分析,我发现了一个非常大的HashMap对象,这个原本是有位同学做缓存用的,但是一个无界缓存,造成了堆内存占用一直上升。后来,将这个缓存改成 guava的Cache,并设置了弱引用,故障就消失了。
4.10 查询垃圾回收器的信息这通常会使用另外一个参数:-XX:+PrintCommandLineFlags可以打印所有的参数,包括使用的垃圾回收器。
5.20 如何写一段简单的死锁代码?public class DeadLockDemo {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (object1) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
}
}
}, "deadlock-demo-1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (object2) {
synchronized (object1) {
}
}
}, "deadlock-demo-2");
t2.start();
}
}
博文参考
《JVM虚拟机原理》
《程序员面试大全》