• JVM:区域划分和垃圾回收


    目录

    JVM的运行时数据区

    为什么要划分区域?区域是干什么的?

    方法区

     程序计数器(PC)

    执行引擎

    即时编译JIT Complier

    JVM的大体启动过程

    垃圾回收(Garbage Collector)

    根据运行时数据区,哪些区域是GC的重点?

    垃圾回收算法

    1、引用计数法

    2、可达性分析法

    上述算法找到垃圾对象,如何进行垃圾回收(内存空间回收)?

    1、标记-清除算法

    2、标记-复制算法

    3、标记整理算法

    大部分对象的一生


    JVM的运行时数据区

    为什么要划分区域?区域是干什么的?

    逻辑上划分区域,便于为不同区域指定专门的用途方便人类理解。

    方法区

    存放方法,主要是指令数据,以字节码为代表的指令数据。线程共享区。也会有部分附属的方法基本信息。扩展起来认为,就是保存类的信息。逻辑上,认为类的相关数据存放在这,静态属性放在方法区。方法区以类为单位,类中还能以方法为基本单位。

    一个JVM实例只有一个堆内存,堆也是Java内存管理的核心区域。堆在JVM启动的时候创建,其空间大小也被创建,是JVM中最大的一块内存空间,所有线程共享堆,物理上不连续但逻辑上连续的内存空间,几乎所有的实例都在这里分配内存,在方法结束后,堆中的对象不会马上删除,仅仅在垃圾收集的时候被删除,堆是GC(垃圾收集器)执行垃圾回收的重点区域。属性空间是随着对象走的,所以逻辑上认为,属性是保存在堆区的。

    以栈帧为基本单位。栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。

    一个方法要执行的时候分配给属于这个方法本次执行的栈帧(至于栈帧分配多少内存空间,不会受运行期间变量数量的影响,而是取决于具体虚拟机的实现),方法执行结束之后,栈帧空间被回收。

    栈帧中保存的就是该方法本次执行需要的数据。栈帧随着方法本次执行出现,本次执行结束消亡。

     程序计数器(PC)

    保存下一条要执行的指令的位置。详细的说PC寄存器是用来存储指向下一条指令的地址,也就是即将将要执行的指令代码。由执行引擎读取下一条指令。

    执行引擎

    就是对CPU的模拟。执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。每当执行一项指令操作后,PC寄存器就会更新下一条需要被执行的指令的地址。

    当然方法在执行过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据执行定位到目标对象的类型信息。

    即时编译JIT Complier

     JIT(Just in time ):即时编译,提高执行效率的一套机制。

    JVM在执行过程中,有些字节码执行的比其他字节码执行更频繁,所以如果都按照字节码翻译的模式,效率就低了,因此,会在运行期间,即时地把这些热点字节码直接编译成本地的机器码(就像C语言一样),速度就能提升。

    JVM的执行,有个预热阶段,就像运动前热身一样,让自己的状态达到最好,效率才最高。

    JVM的大体启动过程

    (控制权是如何交到我们手里的(main方法的第一条语句(字节码)是如何被执行起来的))?

    运行java代码是:Java.exe -classpath指定类加载路径 -启动类名称;比如 java.exe -com.demo.Main -以这个类的main方法作为程序的启动入口:

    • OS收集要启动的进程的信息:程序是C:/program Files/java/jdk/bin/java.exe,参数是com.demo.Main
    • OS根据程序,启动进程,执行java.exe当时写的程序入口(C语言里的main函数)
    • JVM读取参数,找OS申请必要的内存,创建必要的执行引用和类加载器
    • JVM执行引擎,要求类加载器进行com.demo.Main类的加载
    • JVM创建主线程,把PC的值设置成com.demo.Main类下的static main的第一条指令的地址
    • JVM开启执行引擎的指令执行循环,执行第一条语句
    • 然后开始执行我们的代码,直到所有的前台线程退出
    • JVM进行必要的资源回收
    • JVM进程退出

    大致就是控制权由OS交给JVM,然后交给我,完了再交给JVM,在还给OS。

    垃圾回收(Garbage Collector)

    有了GC之后,相对的解放了开发人员的心智,让开发人员只需要考虑什么时候需要一块内存,而不需要考虑什么内存不再被需要了。

    从权利和义务的角度来说:

    逻辑上把内存的使用权和所有权分离了,我们只享受一段内存的使用权,没有所有权。

    好处:不需要不考虑内存的释放问题;

    坏处:内存的直接管理和我们没有关系;

    根据运行时数据区,哪些区域是GC的重点?

    1、PC区域(线程私有)

    PC一定是和有个线程关联着的,只要线程活着,PC 就一定需要。

    分配时机:创建一个新线程的时候;

    回收时机:这个PC对应线程的最后一条指令执行结束之后。

    PC区域的分配和回收时机都很明确,所以不需要GC做过多参与。

    2、栈区域(线程私有)

    每个线程有自己的栈区。

    分配时机:创建线程时为其分配栈空间。

    回收时机:线程执行结束后回收栈空间。

    当执行一个方法的调用时,分配栈帧。

    当该方法return时。回收栈帧。

    栈区域和栈帧分配和回收时机明确,不需要GC做过多参与。

    3、方法区(含运行时常量池)(线程共享)

    分配时机:类的加载时

    回收时机:类的卸载时

    一般来说这个区域的回收效果比较难令人满意,尤其是类的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。

    方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

    回收废弃常量与回收Java堆中的对象非常类似,只要常量池中的常量没有被任何地方引用,就可以被回收。(关于常量的回收比较简单,重点是类的回收)

    判定一个类是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足:

    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    上述区域,初学GC时不需要过多讨论。

    ,是GC ( Garbage Collection, 垃圾收集器)执行垃圾回收的重点区域

    现代JVM由于GC性能问题,把堆空间再次分区域分别进行归纳和管理----分代(Generation);

    堆区:以对象为基本单位进行组织的(线程共享区)

    分配时机:实例化一个对象的时候(时机明确,new语句只会出现在一个明确的地方)

    回收时机:该对象一定没有在被使用的时候。

    在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

    堆空间逻辑上分为三部分,但实际上还是新生区和老年区两部分,元空间(永久代)属于方法区。(java 8之前,永久区;java 8和8之后,更名为元空间)

            永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

             在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。

            在JVM中,Java对象:有一类属于生命周期较短的瞬时对象,这种对象的创建和消亡都很迅速;有一类对象生命周期很长,甚至可以与JVM生命周期一样。

    新生代:(伊甸园区(Eden):幸存者0区(Survivor0区/From区):幸存者1区(Survivor1区/To区)=8:1:1)

      >新生代主要存放新创建的对象,几乎所有的Java对象都是在Eden区被new出来的。

      >当Eden区满时,JVM垃圾回收器将对Eden垃圾回收(Minor GC),将Eden区不再被其他对象引用的对象销毁,在加载新的对象。扫描 Eden Space 和 From Suvivor Space,如果对象仍然存活,则复制到 To Suvivor Space,如果 To Suvivor Space 已经满,则复制到老年区。

      >扫描完毕后,JVM会将 Eden Space 和 From Suvivor Space 清空,交换 From 和 To 区,即下次回收扫描的是 Eden Space 和 To Suvivor Space。(主要是为了减少内存碎片的产生。)

      >在扫描幸存者区几次(默认15次)后对象依然能存活,JVM认为这是一个持久化对象,将其移到老年区。

    老年区:主要存放JVM认为生命周期比较长的对象,内存大小相对会比较大,垃圾回收也相对没有那么频繁。

     图片来自:JVM-堆空间内存分配和各区垃圾回收_gougege0514的博客-CSDN博客

    垃圾回收算法

    如何找到垃圾?

    1、引用计数法

    引用计数法是在对象创建时额外用一块内存区域,存储这个对象被引用的次数(ref_Count)。每当有一个地方引用这个对象,ref_Count++。当引用这个对象失效时,ref_Count--。当ref_Count==0时,一定没有引用指向该对象了。

    Java中的对象,不能被Java应用直接访问,必须通过引用访问,没有引用指向该对象,意味着没有引用这个对象的引用了,以后也 不会有人使用这个对象了。

    这种方法能快速直观的定位到可回收的对象进行垃圾回收,但是引用计数法最大的问题:循环引用问题,没有好的解决方法,导致不少空间被浪费。

    优点:简单,判定效率高;

    缺点:无法解决循环引用问题;

    2、可达性分析法

     

    可达性分析法是将所有的对象看作一张图,以GC Roots(进行GC时出发的根)作为起始节点集,从这些节点开始,根据引用关系向下搜索(图的遍历),遍历完成后,有不可达的对象,则证明此对象是不可能在被使用的。

    注意:一个对象被判定为不可达的对象不一定就会立即成为可回收对象。被判定为不可达的对象要成为可回收对象至少需要经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

    上述算法找到垃圾对象,如何进行垃圾回收(内存空间回收)?

    1、标记-清除算法

    最基础的垃圾回收算法,分为标记和清除两个阶段,先标记出所有需要回收的对象,在标记完成后,统一回收被标记的对象所占用的空间。(或者反过来,标记存活的对象,统一回收未标记的所有对象)。

    优点:算法简单容易实现;与保守式GC算法兼容(清楚算法不会移动对象);

    缺点:

    1、执行效率不稳定。如果存活对象多,标记可回收对象清楚算法执行效率较高。相反执行效率低一点。而且需要扫描两遍,第一遍标记有用的对象,第二遍扫描把可回收的找出来清理。

    2、容易产生内存碎片。标记清除之后会产生不连续的内存碎片,内存碎片太多可能会导致,后续过程中要给大对象分配空间时无法找到足够的连续内存空间,再次触发新的垃圾回收动作。

    2、标记-复制算法

    为了弥补标记清除算法提出的。

    将内存分成两个部分,例如分为A,B两部分,分配内存先往A区分配。将A区有用的对象拷贝到B区,完成后将A区清除(内存全部释放)。

    下次再分配内存的时候先往B区分配,将B中有用的对象拷贝到A区,清除B区后再往A区分配。

    这样一来就不容易出现内存碎片的问题。

     

    优点:标记复制算法适用于存活对象较少的情况,只要扫描一次,效率提高,而且不产生内存碎片。但移动复制对象时须调整对象引用。

    缺点:能使用的内存就剩原来的一半,而且如果存活对象多,标记复制算法效率就会大大降低。

    3、标记整理算法

    对象分配好空间之后,需要回收的时候,先把没有任何引用的可回收对象标记成垃圾,然后把后面存活的对象拷贝到标记的地方,最后凡是有用的对象全部移到前面无论这个内存有没有使用都给他整理到前面,最后就剩下一大块内存。

    不会产生碎片化内存(标记清除),也不会让可用的内存只有一半(标记复制)。

     该算法的实现思路:

    1、通过GC Roots找到有用的对象;

    2、把有用的对象往前移动;

    优点:标记整理算法不会产生内存碎片,空间连续,也不会让可用内存只有一半。

    缺点:需要扫描两次内存,还要移动对象,第一次扫描找有用对象,第二次扫描移动对象。

    移动时,如果是多线程要考虑线程同步,所有效率低一些。

    大部分对象的一生

    • 诞生在伊甸园(Eden)
    • 年关以至(GC),Eden诞生的大部分对象(99%)死去(没有被引用 清理),活下来的被放进幸存区(Survivor0区/From区),由于活下来的很少,所以复制代价小。同时伊甸园没有活对象。
    • 下次年关以至(GC),Eden活下来的对象和From区活下来的对象放进幸存区(Survivor0区/To区)(都活下来的很少),同时伊甸园和幸存1区没有活对象。
    • 直到一个对象成年之前,一直持续这个操作(GC,From区可用,To不可用。GC,To可用,From不可用)前提:大部分对象会死去。
    • 当一个对象14岁时(14次),在遇到GC变成15岁,视为成年,还活着的话从幸存区转移到老年区。
    • 成年之后不用再记年龄。如果在遇到GC,老年代的GC按照垃圾回收算法进行处理。(老年区对象一般不多,所以相对可以接受),
    • 剩下的对象会在老年区直到死去(GC被回收)。

    如果对象本身就比较大,复制起来成本很高,一般直接进入老年代。 


    大部分情况下,只进行Minor GC。随着Minor GC的进行,越来愈多的对象进入老年代。当老年代

    对象占存达到某个阈值,进行Major GC。(老年代GC成本较大,一般会尽量减少老年代GC)

    由于大部分Major GC是由某一次Minor GC引起的,所以Major GC发生时,一般也就代表类Full GC。

    小结:

    为啥对象15岁"成年"?

    HotSpot内部实现对象的时候,用了4个bit记录年龄。年龄就0-15。

    为啥两个幸存区?

    为了解决内存碎片所带来的麻烦,再划分一个幸存者区,将gc回收之后的from幸存区倒腾到to幸存者区,就可以避免这种问题,是一种空间换时间的思路。

  • 相关阅读:
    前端blob数据
    三、组件与数据交互
    单目标分割标签图叠加代码
    第09章 文本特征向量化
    【面试题精讲】Java包装类缓存机制
    【分享】5G+北斗RTK高精度人员定位解决方案
    Mac安装Mysql,并启动
    支持多模多态 GBase 8c数据库持续创新重磅升级
    饿了么官宣合作抖音后,美团的失意是什么?
    DJango 学习(2)—— django引入:借助于wsgiref模块(web服务网关接口)搭建简易 web 框架
  • 原文地址:https://blog.csdn.net/qq_51263139/article/details/125983404