相信看了本文后,对你的面试是有一定帮助的!
?点赞?收藏?不迷路!?
Heap (堆区)
堆是 OOM 故障最主要的发生区域。它是内存区域中最大的一块区域,被所有线程共享,存储着几乎所有的实例对象、数组。
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代。再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
堆区的调整:通过设置如下参数,可以设定堆区的初始值和最大值,比如 -Xms256M -Xmx 1024M,其中 -X 这个字母代表它是 JVM 运行时参数,ms 是 memory start 的简称,中文意思就是内存初始值,mx 是 memory max 的简称,意思就是最大内存。
在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力所以在线上生产环境中 JVM 的 Xms 和 Xmx 会设置成同样大小,避免在 GC 后调整堆大小时带来的额外压力。
堆的默认空间分配:查看当前 JDK 版本所有默认的 JVM 参数:=======》java -XX:+PrintFlagsFinal -version
Java 虚拟机栈:
对于每一个线程,JVM 都会在线程被创建的时候,创建一个单独的栈。也就是说虚拟机栈的生命周期和线程是一致,并且是线程私有的。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表(局部变量表:定义为一个数字数组,用于方法参数和方法内部的局部变量,包括三种数据类型:8种基本数据类型+引用数据类型地址+returnAddress类型(指向一条字节码指令的地址) )、操作数栈(操作数栈主要用于保存运算过程中的的中间结果,同时作为计算过程中变量的临时的存储空间.)、动态链接(动态链接就是讲指令中的符号引用转化为真实的方法地址(直接引用)和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
本地方法栈:
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
拓展:
程序计数器:
程序计数器(Program Counter Register)是一块较小的内存空间。是线程私有的。它可以看作是当前线程所执行的字节码的行号指示器
直接内存:
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现
Code Cache:
JVM 代码缓存是 JVM 将其字节码存储为本机代码的区域 。我们将可执行本机代码的每个块称为 nmethod。该 nmethod 可能是一个完整的或内联 Java 方法。
Java中的垃圾回收是通过标记-清除算法实现的。当一个对象不再被任何引用指向时,它就可以被垃圾回收器回收。
在Java中,垃圾回收器通过可达性分析算法来标记垃圾对象。可达性分析算法是从一组被称为“GC Roots”的根对象开始遍历内存中的所有对象,任何能够被遍历到的对象都被认为是“存活”的对象,而未被遍历到的对象则被认为是“垃圾”对象。
Java中的GC Roots包括以下几种类型的对象:
垃圾回收器会从GC Roots开始遍历内存中的所有对象,标记出所有被引用的对象,然后将没有被标记的对象视为垃圾对象进行回收。这个过程中,需要注意的是,垃圾回收器不能回收互相引用的对象,即使这些对象已经没有任何被引用的路径,因为它们之间仍然存在一条循环引用的路径,所以它们仍然被视为“存活”的对象。
需要注意的是,Java虚拟机的垃圾回收机制是自动的,程序员无法直接干预。但是,可以通过一些手段来优化垃圾回收的效率,比如尽量避免创建大量的临时对象,避免使用过多的finalize()方法等。
因为虚拟机栈、本地方法栈、程序计数器是线程私有的,随着线程的消亡而消亡,方法结束或者线程结束时,内存自然就跟随着回收了。 GC回收的区域是堆和方法区,为什么回收这两个区域那,因为他们是线程共享的,即java程序中所有的线程都可以访问。
**分代收集算法:**根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;
**标记清除法:**第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:在遍历一遍,将所有标记的对象回收掉;
特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间分片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC(垃圾回收);
**标记整理法:**第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉;
特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;
**复制算法:**将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除;特点:不会产生空间碎片;内存使用率极低;
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足的常见场景比如大对象、大数组直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。
除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。
还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
在执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也
会执行 Full GC。
如果经过 Full GC 仍然回收不了,那么虚拟机会抛出java.lang.OutOfMemoryError PermGen space
为避免以上原因引起的 Full GC,可采用的方法为增大Perm Gen或转为使用 CMS GC。
空间担保,下面两种情况是空间担保失败:
1、每次晋升的对象的平均大小 > 老年代剩余空间
2、Minor GC后存活的对象超过了老年代剩余空间
注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当出现这两种状况的时候就有可能会触发Full GC。
promotion failed 是在进行 Minor GC时候,survivor space空间放不下只能晋升老年代,而此时老年代也空间不足时发生的。
concurrent mode failure 是在进行CMS GC过程,此时有对象要放入老年代而空间不足造成的,这种情况下会退化使用Serial Old收集器变成单线程的,此时是相当的慢的。