StackOverflowError和OOM+JVM垃圾回收机制

StackOverflowError
本地方法栈和虚拟机栈是线程所私有的,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;
产生原因:当前线程请求的栈深度超过了虚拟机所允许的栈深度,常发生在递归或者死循环当中。
可以通过修改-xss参数调整栈大小

OOM(Out Of Memory)
堆(heap)是被所有线程共享的一块区域,

内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
解决办法:通过内存监控软件,查找程序中的泄露代码

内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
解决办法:可以通过虚拟机VM参数进行修改

产生原因:

  • 当java的垃圾回收机制没有及时回收堆中的静态变量/类等,导致堆溢出。
  • 强引用太多,因为强引用不会被回收。

Java中的四种引用:强引用、软引用、弱引用、虚引用

强引用:
强引用是我们最常见,也是最熟悉的普通对象引用,如创建一个对象引用他(new 对象)。只要处于此种引用,该对象就表示永远不会处于不可及状态,GC就不会回收他,即使JVM内存空间不足,JVM宁愿抛OutOfMemoryError运行时错误(OOM),让程序异常终止,也不会靠回收强引用对象来解决内存不足的问题。

软引用:
(1)软引用在内存足够的时候,GC不会回收它。 只有当JVM认定内存空间不足时才会去回收软引用指向的对象。软引用通常可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,java虚拟机就会把这个软引用加入到与之关联的引用队列中(详见下面的示例)。
(2)JVM会确保在抛出内存溢出(OOM)前清理软引用指向的对象,他会尽可能优先回收长时间闲置不用的软引用指向的对象,对那些刚构建的或刚使用过的软引用指向的对象尽可能的保留。基于软引用的这些特性,软引用可以用来实现很多内存敏感点的缓存场景(如图片缓存和网页缓存),即如果内存还有空闲,可以暂时缓存一些业务场景所需的数据,当内存不足时就可以清理掉,等后面再需要时,可以重新获取并再次缓存。这样就确保在使用缓存提升性能的同时,不会导致耗尽内存。

弱引用:
(1)GC在扫描它所管辖的内存区域时,只要发现弱引用的对象,不管内存空间是否有空闲,都会立刻回收它。 是一种十分临近finalize状态的情况,当弱引用被清除的时候,就符合finalize的条件了。弱引用与软引用最大的区别就是弱引用比软引用的生命周期更短暂。
(2)具体的回收时机还是要看垃圾回收策略的,并且垃圾回收器是一个优先级很低的线程,因此那些弱引用的对象并不是说只要达到弱引用状态就会立马被回收。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用:
(1)虚引用在任何时候都可能被GC回收,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,并且虚引用并不会决定对象的生命周期。
(2)虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用,也有人利用虚引用监控对象的创建和销毁。幻象引用的get方法永远返回null,主要用于检查对象是否已经从内测中删除。

参考链接:https://blog.csdn.net/weixin_41133233/article/details/103789929

Java垃圾回收机制

对于程序计数器、虚拟机栈、本地方法栈这三个部分而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此以下有关内存分配和回收关注的是Java堆与方法区这两个区域。

1、如何判断对象已“死”
Java堆中存放着几乎所有的对象实例,垃圾回收器在堆进行垃圾回收前,首先要判断这些对象那些还存活,那些已经“死去”。判断对象是否已“死”有如下几种算法:

1.1 引用计数法
引用计数法描述的算法为:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个比较好的算法。比如Python语言就是采用的引用计数法来进行内存管理的。
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因是引用计数法无法解决对象的循环引用问题,容易产生内存泄漏,因此JVM不采用引用计数法。
1.2 可达性分析算法
在上面讲了,Java并不采用引用计数法来判断对象是否已“死”,而采用“可达性分析”来判断对象是否存活(同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言)。
此算法的核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。以下图为例:

对象Object5 —Object7之间虽然彼此还有联系,但是它们到 GC Roots 是不可达的,因此它们会被判定为可回收对象。

在Java语言中,可作为GC Roots的对象包含以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中(Native方法)引用的对象

即使在可达性分析算法中不可达的对象,也并非"非死不可"的,这时候他们暂时处在"缓刑"阶段。要宣告一个对象的真正死亡,至少要经历两次标记过程: 如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况都视为"没有必要执行",此时的对象才是真正"死"的对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。
任何一个对象的finalize()方法都只会被系统自动调用一次,如果相同的对象在逃脱一次后又面临一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败。

垃圾回收算法

1、标记-清除算法
“标记-清除”算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(标记过程参见1.2可达性分析)。后续的收集算法都是基于这种思路并对其不足加以改进而已。
“标记-清除”算法的不足主要有两个:

效率问题:标记和清除这两个过程的效率都不高
空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

3.2 复制算法(新生代回收算法)
“复制”算法是为了解决“标记-清除”的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图:

3.3 标记整理算法(老年代回收算法)
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为“标记-整理算法”。标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。流程图如下:

3.4 分代收集算法
当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

反射

反射即动态获取类中的所有属性和方法。要利用反射,首先要得到其.Class文件(字节码文件),再从Class字节码文件当中解析各种属性和方法。

1:获取类对象的三种方式

  • Class.forName()(常用)
  • Hero.class
  • new Hero().getClass()
    三种方式中,常用第一种,第二种需要导入类的包,依赖太强,不导包就抛编译错误。第三种对象都有了还要反射干什么。一般都第一种,一个字符串可以传入也可写在配置文件中等多种方法。
    在一个JVM中,一种类,只会有一个类对象存在。所以以上三种方式取出来的类对象,都是一样。(此处准确是在ClassLoader下,只有一个类对象)

2 利用反射机制创建对象
基本步骤
与传统的通过new 来获取对象的方式不同反射机制,反射会先拿到Hero的“类对象”,然后通过类对象获取“构造器对象”再通过构造器对象创建一个对象,具体步骤:

  • 1.获取类对象 Class class = Class.forName("pojo.Hero");
  • 2.获取构造器对象 Constructor con = clazz.getConstructor(形参.class);
  • 3 获取对象 Hero hero =con.newInstance(实参);

参考链接:https://blog.csdn.net/yubujian_l/article/details/80804708
https://blog.csdn.net/lililuni/article/details/83449088

posted @   Dreamer_szy  阅读(107)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示