zoukankan      html  css  js  c++  java
  • Java内存模型与垃圾回收

     一、Java内存模型

    Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如下图所示:

    1. 程序计数器

    一块较小内存区域,指向下一条要执行的指令。如果线程正在执行一个Java方法,这个计数器记录将要执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空。

    2. Java虚拟机栈

    线程私有的,其生命周期和线程一致,存储着一个一个的栈帧,每个方法执行时都会创建一个栈帧用于存储局部变量表(对象的引用,基本类型数据)、操作数栈(表达式栈)、动态链接(指向运行时常量池中方法的引用)、返回地址,一些附加信息

    JVM Java虚拟机栈(栈帧:局部变量表)_寒青~的博客

    2.1局部变量表


     

     

     

      public int add(){
        int a = 15;
        int b = 8;
        int s = a+b;
        return s;
      }

     

     

    2.2方法返回地址(return address)

    • 存放调用该方法的PC寄存器的值
    • 一个方法的结束,有两种方式:

       正常执行完成:返回PC寄存器的值

       出现未处理的异常,非正常退出:出现异常后会交给异常处理器去处理,如果有异常处理器(比如try-catch)且异常处理器可以处理该异常,则进行处理,如果没有异常处理器或处理不了则异常退出

    非虚方法:如果这个方法在编译器就确定了具体的调用版本,在运行时是不可以变的,称之为非虚方法:静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法

    虚方法:  除了非虚方法的其它方法都为虚方法。

    interface Friendly {
            void sayHello();
            void sayGoodbye();
         }
        
    class Cat implements Friendly {
           public void eat() {
             }
           @Override
           public void sayHello() {
             }
     
           @Override
           public void sayGoodbye() { 
            }
    
            @Override
           public void finalize() {
            }
    
            @Override
            public String toString() {
               return "Cat{}";
            }
        }

    Cat类的方法表

    3. 本地方法栈

    功能与虚拟机栈类似,只不过虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的Native方法服务。

    4. 堆

    堆是虚拟机管理内存中最大的一块,被所有线程共享,该区域用于存放对象实例和数组,几乎所有的对象都在该区域分配。Java堆是内存回收的主要区域,从内存回收角度看,由于现在的收集器大都采用分代收集算法,所以Java堆还可以细分为:新生代(1/3)和老年代(2/3),新生代再细分一点的话可以分为Eden空间(8/10)、From Survivor空间(1/10)、To Survivor(1/10)空间,80%以上的对象在Eden区就被回收了。根据Java虚拟机规范规定,Java堆可以处于物理上不连续的空间,只要逻辑上是连续的就行。

    TLAB:JVM为每个线程在Eden区分配了一个私有缓存区,可以避免多线程同时分配造成的线程安全问题,会优先在TLAB分配。

    逃逸分析,栈上分配,标量替换,同步消除,锁消除之间的区别  栈上分配由于实现起来需要更改大量假设了“对象只能堆分配”的代码,因此 Hotspot虚拟机并没有采用栈上分配,而是使用了标量替换这一技术

    5. 元空间

    与Java一样,是各个线程所共享的,用于存储已被虚拟机加载类信息、常量、即时编译器编译后的代码等数据。Java8将方法区改为元空间,不在使用虚拟机内存,而是直接使用本地内存。对元空间的GC主要是回收不在使用的类信息和运行时常量池中的常量。

     JVM8-方法区 - 掘金 (juejin.cn)

    6. 运行时常量池

    运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。运行期间可以将新的常量放入常量池中,用得比较多的就是String类的intern()方法,当一个String实例调用intern()时,Java查找常量池中是否有相同的Unicode的字符串常量,若有,则返回其引用;若没有,则在常量池中增加一个Unicode等于该实例字符串并返回它的引用。

     7. 字符串常量池 

    Java为String在开辟的一块内存缓冲区,为了提高性能同时减少内存开销。在JVM中,字符串常量池由一个hash表实现,java7默认容量为60013(底层设计其实就是hash表+链表)。JDK8将字符串常量池静态变量存储在堆内存

     

     二、对象分配内存策略

     

     一篇文章搞懂逃逸分析,栈上分配,标量替换,同步消除,锁消除之间的区别_Shockang的博客-CSDN博客

    三、垃圾对象如何确定

    Java堆中存放着几所所有的对象实例,垃圾收集器在对堆进行回收前,首先需要确定哪些对象还"活着",哪些已经"死亡",也就是不会被任何途径使用的对象。

      1. 引用计数法

    引用计数法实现简单,效率较高,在大部分情况下是一个不错的算法。其原理是:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加1,当引用失效时,计数器减1,当计数器值为0时表示该对象不再被使用。需要注意的是:引用计数法很难解决对象之间相互循环引用的问题,主流Java虚拟机没有选用引用计数法来管理内存。

      2. 可达性分析算法

    这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

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

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    2. 方法区中类静态属性引用的对象。
    3. 方法区中常量引用的对象。
    4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

    现在问题来了,可达性分析算法会不会出现对象间循环引用问题呢?答案是肯定的,那就是不会出现对象间循环引用问题。GC Root在对象图之外,是特别定义的“起点”,不可能被对象图内的对象所引用。

    3. 对象生存还是死亡(To Die Or Not To Die)

    对象有三个状态,可触及可复活(调用finalize()方法后有可能复活),不可触及(调用finalize()方法后没有复活)

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。程序中可以通过覆盖finalize()来一场"惊心动魄"的自我拯救过程,但是,这个机会集合只有一次呦。只有不可触及状态的对象才可以被回收

    4. 引用

    无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

    • 强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。“不回收
    • 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。“内存不足回收
    • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。“发现即回收
    • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。“回收跟踪”(主要是知道它被回收了,用于追踪垃圾回收过程)
    • 终结器引用:用以实现对象的finalize()方法,无需手动编码。在GC时,终结器引用入队列。由Finalizer线程通过终结器引用找到被引用的对象并调用finalize()方法,第二次GC时才能回收被引用对象

    软引用使用示例:

    package jvm;
    
    import java.lang.ref.SoftReference;
    
    class Node {
        public String msg = “”;
    }
    
    public class Hello {
        public static void main(String[] args) {
            Node node1 = new Node(); // 强引用
            node1.msg = “node1”;
            SoftReference node2 = new SoftReference(node1); // 软引用
            node2.get().msg = “node2”;
    
            System.out.println(node1.msg);
            System.out.println(node2.get().msg);
        }
    }

    输出结果为:

    node2
    node2

    四、典型的垃圾回收算法

      1. Mark-Sweep(标记-清除)算法

    这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。

    标记阶段:从根节点出发深度优先遍历,标记出所有可达的对象

    清除阶段:回收没有被标记的对象所占用的空间。

    标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间。标记清除算法需要遍历两次,所以效率叶不太高

      2. Copying( 复制)算法

    为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块(A,B),每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

    这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半(有没有联想到s0和s1呢?)。

    很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

      3. Mark-Compact(标记-整理)算法

    为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存(old区在使用该算法)。具体过程如下图所示:

    优点:解决了清除标记算法的内存碎片化问题和复制算法内存利用率低的问题

    缺点:对象移动的同时,需要调整引用的地址;移动过程中需要暂停应用程序,即:STW

    4. Generational Collection(分代 收集)算法

    分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

    目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(一般为8:1:1),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

    而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

     以上所有算法在垃圾回收的时候,应用软件都处于一种Stop the Word状态,如果长时间回收垃圾,应用程序将被挂起很久,严重影响用户体验和系统的稳定性

    5. 增量收集算法

    让垃圾收集线程和应用程序线程交替执行,每次,逻辑回收线程值收集一小块区域,接着切换到应用程序线程。以此反复,直到垃圾收集完成。(基础还是传统的标记清除和复制算法)

    因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本下降,造成系统吞吐量下降

    6. 分区算法

    分代算法按照对象的生命周期划分了新生代和老年代,分区算法(G1)将整个堆空间划分成连续的雄安区间(Region),每一个小区间都独立使用,独立回收

  • 相关阅读:
    STL中队列queue的常见用法
    牛客网剑指offer第17题——树的子结构
    《剑指offer》网络大神所有习题题解
    牛客网剑指offer第4题——重建二叉树
    牛客网剑指offer第56题——删除链表中重复的节点
    图像处理中的求导问题
    hash_set和hash_map
    hashtable初步——一文初探哈希表
    数据结构-链表的那些事(下)(二)
    数据结构-链表的那些事(上)(一)
  • 原文地址:https://www.cnblogs.com/isalo/p/13095228.html
Copyright © 2011-2022 走看看