美文网首页
JVM内存分配与回收

JVM内存分配与回收

作者: NengLee | 来源:发表于2021-08-14 19:58 被阅读0次

    今天七夕节(单身汪节日快乐),一不小心new个对象,接下里在JVM会产生怎么操作,从创建到分配和执行以及回收等一系列的流程,是这篇文章的具体分析。

    JVM对象的创建

    Object obj = new Object();  //创建New对象
    

    大致分为4个步骤:

    1. 类加载器
    2. 检查加载
    3. 分配内存
    4. 内存空间初始化
    创建对象步骤

    类加载器

    分配当期这个Class是属于哪个加载器所加载,之前有简单写过介绍 Android中的类加载,以及双亲委派机制

    检查加载

    根据指定是否能够在常量池定位带一个类的符号引用,并且检查该类是否被加载,加载以及初始化

    内存分配

    划分内存的方式,并且初始一些内存并发安全机制CAS,下面会着重写这块

    • CAS(比较并替换):乐观锁 synchronized:悲观锁 。 之前有写过文章 Java并发同步锁

    内存空间的初始化

    执行init{}代码块,初始化对象属性、构造函数直到被创建。虚拟机栈中的Reference指向堆中的对象

    内存分配

    内存分配就是划出一些不等分区间数值,用来存储对象属性,那么对象内部又是如何分配

    对象内存的分布

    对象的组成大致分三大类:对象头实例数据对齐填充

    • 对象头:描述对象信息的作用,自身状态
      • 存储对象的运行时数据(Mark Word)
        • 哈希码
        • GC分代情况、年纪
        • 锁状态标识
        • 线程持有的锁
        • 偏向线程ID
        • 偏向时间戳
      • 类型指针
      • 若为对象数组,还应该有记录数组长度的数据
    • 实例数据:具体信息内容
    • 对齐填充:对象即使是没有内容,应该有对象头,所以最小是8字节,并且对象的内存大小是8的倍数,例如当前只有30个字节,那么就会补齐32字节,保证内存细分是等分

    对象的访问定位

    前面说在执行初始化后,因为栈与方法区的一些变量表只能存储基本数据类型,其对象与数组都是在堆内存放,那么就需要 句柄 方式访问对象

    对象的访问定位

    堆内存的划分

    堆内存划分分 新生代老年代 俩大块,为什么需要划分?主要是帮助提升GC垃圾回收器的效率与策略

    堆内存的划分

    一个新New的对象都是先进入新声代(End),通过多次GC递增后,对象依然存在,那么该对象就会晋级后面的区域,那么依据是是否被标记为 “垃圾对象”,在后面Gc回收机制在细分

    Eden --> From --> To (约15次gc)--> Old

    这样的好处,可以提升CG回收大量对Eden区域处理,减少全部对象的筛选,就好比如在Old内存的对象,是经过多次GC还存在,那么可以判断内部对象经常性被使用,就可以几乎完全没必要回收它。

    内存比例大小原则

    新生代 :老年代 (1/3:2/3)

    Eden:From:To (8:1:1)

    对象分配的一些原则

    • 对象优先分配在Eden
    • 空间分配担保
    • 大对象直接进入老年代
    • 长期存活的对象进入老年代
    • 动态对象年龄判断

    会走3个判断逻辑

    • 是否分配在栈上
    • 是否内存占用大对象
    • 是否分配Eden本地线程TlAB中
    New 一个对象:
    
    //栈判断
    if(是否分配在栈上){
        Yes - 分配在栈
    }else{
        //大对象
        if(是否内存占用大对象){
            Yes - 直接分配Old 老年代/元空间   
        }else{
            //是否本地线程缓冲区
            if(是否分配Eden本地线程TlAB中){
                Yes - Eden内部的TLAB线程中
            }else{
                独立的Eden中
            }
        }
    }
    

    GC回收机制

    GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。

    特别是当内存吃紧,不确定机制下以及 System.GC() 手动调用下触发GC回收器,释放堆中没有用 或是 “不重要”的内存空间。

    判断对象是否为垃圾,是否可存活的一些依据

    1. 引用计数算法
    2. 可达性分析(根可达 GCRoots)
    3. Class回收
    4. finalize()

    引用计数算法

    是在对象头维护了一个 counter标识,每次增加一次该对象的引用计数器自加,如果该对象的引用失联,则计数器自减,那么当counter为0的时候,表示该对象已经被放弃,不处于存活资格应该被视为垃圾对象。

    引用技计数算法在某些常见会失效,例如"强、软、弱、虚"引用下,还有就是俩个对象相互引用,造成死锁导致无法释放counter,无法GC。

    可达性分析算法

    通过一系列的 GC Root Set :顾名思义就是GC 会通过 根节点的集合来依次向下查询,与根对象链在一起的引用,就形成链路集合,那么这个查询结束后,那位孤立,并且未在链路上的对象便可以视为“垃圾对象”。

    可达性分析 GCRoots

    通过图表的形式,可以很清楚的观察到,经过可达性分析后,孤立的没有在链路上的对象视为“垃圾”

    还有就是在 “A/B”俩个对象在相互引用和指向对方,因为没被根集合所指向,当然也视为“垃圾”被回收

    Class回收条件

    在对象是可以频繁也是比较容易回收,而class如果是想被回收是非常的困难,

    1. 类中的所有实例都已经被回收
    2. 所属的ClassLoader加载器被回收
    3. 并且该为没有被如何地方所引用

    finalize

    该方法是Object类中的函数,其作用就是 垃圾对象第一次是被伪清除,可以通过 finalze()函数所抢救回来,防止Gc把某些对象给清除掉,所以是在万物祖宗Object类中,但是finalize()函数有效性只有一次,当被抢救回来,下次GC再次标注为垃圾所清除就不具备抢救性,即使被finalize调用N次也没有用。

    public class Presenter {
    
        public static Presenter instance = null;
    
        public void isAlive() {
            System.out.println("Presenter 还活着,不为nNULL ");
        }
    
        @Override
        protected void finalize() throws Throwable {//不推荐使用
            super.finalize();
            System.out.println("finalize 开启抢救模式");
            Presenter.instance = this;//把引用接上
        }
    
        public static void main(String[] args) throws Throwable {
            instance = new Presenter();
            //对象进行第1次GC
            instance = null;
            System.gc();
            System.out.println("instance = null  GC ");
    
            Thread.sleep(1000);//Finalizer方法优先级很低,需要等待
            if (instance != null) {
                instance.isAlive();
            } else {
                System.out.println("Presenter 挂了 NULL");
            }
            System.out.println("  ");
    
            //对象进行第2次GC
            instance = null;
            System.gc();
            System.out.println("instance = null  GC ");
    
            Thread.sleep(1000);
            if (instance != null) {
                instance.isAlive();
            } else {
                System.out.println("Presenter 挂了 NULL");
            }
        }
    }
    
    //日志输出
    finalize 开启抢救模式
    instance = null  GC 
    Presenter 还活着,不为nNULL 
      
    instance = null  GC 
    Presenter 挂了 NULL
        
    

    四种引用类型

    不同的引用类型,更好的管理对象生产时间,结合业务和内存达到最优状态

    强引用 Strong Reference

    如果一个对象是强引用,它就不会被垃圾回收器回收,即使当内存空间不足,JVM也不会进行回收,而且抛出相应的OutOfMemoryError错误,使得程序进行终止,如果想让强引用对象可以被GC所回收,那么在使用该对象后进行赋值null,这样GC在某次触发后便可以回调掉。

    //只要obj还指向Object对象,Object对象就不会被回收
    Object obj = new Object(); 
    //手动置null
    obj = null;  
    

    软引用 Soft Reference

    在软引用的情况下,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器所回收,只有当内存紧张下,系统就会回收软引用对象,如果回收之后依旧内存不足,就会抛出内存益处OutOfMemoryError

    弱引用 Weak Reference

    弱引用所拥有的生命周更加短暂,当JVM进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收,垃圾回收器是一个优先级别较低的线程,并不一定及时发现弱引用对象。

    虚引用 Phantom Reference

    如果一个对象持有了虚引用,那么它相当于没有被引用,在任务时候都可能被垃圾回收器回收,虚引用有个特点也是区别弱、软引用的。虚引用必须和引用队列(ReferenceQueue)结合使用,当垃圾回收期回收该虚引用对象之前,先会加到引用队列中。

    垃圾回收的基本回收算法

    复制算法

    执行步骤:

    1. 将可用的内存按照容量划分为大小相等的俩快
    2. 每次只使用其中的一块
    3. 当这块的内存使用完,就将还活着的对象复制到另一块上
    4. 最后把已经使用的内存空间一次全清理掉,
    复制算法

    具有的特点:

    • 实现简单,运行效率高效
    • 没有内存碎片
    • 利用率只有一半

    Eden区的来源(Apple式回收)

    为了避免内存空间浪费,提高空间利用率和空间分配担保,在复制算法基础上进行升级,也就是派上 “Eden、From、To” 内存区

    1. 首先把新生代内存空间分成3个区域,“Eden、From、To” 比例:8:1:1
    2. Eden把已经GCRoots链路上的对象,复制移动到 From上,清除Eden内存,此时Eden、To目前是干净内存
    3. 再次遇到GC回收,把Eden和From的链路,复制移到To内存,同时清除Eden、From内存,此时From为干净内存
    4. 又一次GC回收,把Eden和To内存的链路,复制移到From内存上,同时清除Eden和To,此时To目前还是干净内存【回到2步,来回交替复制,这样就提高空间利用率和空间分配担保
    5. 需要注意的当某个对象内存一直在From和To内存来回的复制移动,就需要进入老年代内存区
    Apple式回收

    标记清除算法

    标记清除算法是直接根据GCRoots上的链路,保留,其他的孤立、未在链路上的对象空间直接回收释放

    • 清除后会产生大碎片,位置不是连续
    • 效率比较低
    • 唯一特性优点就是不用阻塞线程

    为了提高碎片凌乱问题,就有了整理,毕竟内存不连续会导致大一点的内存对象就无法存储,特别是Android里面对Bitmap位图的内存声明,如果没有连续的内存就会出现OutOfMemoryError异常

    标记整理算法

    顾名思义,就是在标记算法上的改良

    1. 可达性分析后
    2. 把链路进行整理排序,会造成指针调整
    3. 最后一把清除孤立的未引用的内存对象

    综合这俩个算法

    • 复制算法效率高,会阻塞线程
    • 标记算法效率低,不会阻塞线程
    • 标记算法不整理的情况下,会产生大量的碎片

    常量的垃圾回收器

    1. 单线程回收器

    2. 多线程并行垃圾回收器

    3. 并发垃圾回收器

    回收器 回收对象和算法 回收器类型
    Serial 新生代、复制算法 单线程(串行)
    ParallelScavenge 新生代、复制算法 并行多线程回收器
    PaeNew 新生代、复制算法 并行多线程收集器
    Serial Old 老年代、标记整理算法 多线程(串行)
    Parallel Old 老年代、标记整理算法 并行多线程回收器
    CMS 老年代、标记清除算法 并行多线程回收器
    G1 跨新生代和老年代、标记整理+化整为零 并发多线程回收器

    相关文章

      网友评论

          本文标题:JVM内存分配与回收

          本文链接:https://www.haomeiwen.com/subject/qtbebltx.html