JVM的内存区域和垃圾回收机制

JVM的内存区域

java 虚拟机在执行java程序的过程中,会把它管理的内存划分为若干那个内存区域,分为以下几种
这里写图片描述

程序计数器

程序计数器是一个较小的内存空间,他可以看做当前线程所执行的字节码的行号指示器,由于java虚拟机的多线程是通过线程轮流切换并分配处理器的执行时间来实现的,任何一个确定的时刻,一个处理器(对应于多核中的一个内核)都只会执行一个线程中的指令,因此为了线程切换后内回到正确的位置,每个线程都有一个独立的程序计数器,各线程之间的程序计数器互不影响,我们称为线程私有内存 唯一一个没有内存溢出的区域

Java虚拟机栈

Java虚拟机栈也是线程私有的,他描述的是java方法执行的内存模型,每个方法执行的同时都要创建一个栈帧,用于储存局部变量表,操作数栈,动态链接,方法出口,等信息,每一个方法从调用到执行完成,都对应一个栈帧在虚拟机栈中从入栈到出栈的过程

局部变量表放置了,编译器可知的各种基本数据类型,和对象引用

本地方法栈

一般认为他和虚拟栈为一体

Java堆

java堆是java虚拟机中占用内存最大的一块,他是线程共享的一块内存,在虚拟机启动时创建,此区域唯一的目的就是放置对象的实例,几乎所有的对象实例都是在这里分配内存,java堆是垃圾回收器的主要区域,也被称作为GC堆,可处于物理内存不了连续的空间

方法区

和java堆是一样,是各个线程共享的内存区域,它用于储存已经被虚拟机加载的,类信息,常量,静态变量,即时编译器编译后的代码

运行时常量池

是方法区的一部分,class文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用

栈和堆

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。

对象的创建

  • (1)当虚拟机遇到一个new指令时,首先去检查这个指令的参数是否能在常量池定位到一个类的引用符号,并且检查引用符号代表的类是否已经被加载,解析,初始化过,如果没有必须执行相应的类加载过程。

  • (2)当类加载检查通过后,虚拟机为为新生对象分配内存,有俩种方法分配内存

    • 指针碰撞 : java堆在内存中绝对工整,所有的空闲内存放在一边,用过的内存放在另一边,中间一个指针作为分界点的指示器,那分配内存仅仅只是,吧指针象空闲移动一个对象的大小而已。
    • 空闲列表: java堆在内存中是不连续的内存块,虚拟机维护一个列表,来记录那些内存块是可用的,然后找到一个对象大小的内存块使用。
  • (3) 当内存分配完成后,虚拟机需要将分配的内存,都初始化为零值(不包括对象头),这一步保证了,对象的实例字段在java代码中可以不赋初始值就可以使用,程序能访问到,这些字段数据类型对应的零值

  • (4)接下来,虚拟机要对对象进行必要的设置,比如,这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息,这些信息存放在对象的对象头中。

  • (5)上面的工作完成后,从虚拟机的角度,一个对象已经产生,单从java的角度来看,对象的创建才刚刚开始,- 方法还没没有执行,所有字段都为零,所以来说,new之后执行,按照程序员的意愿初始化对象,这样一个完整可用的对象才算创建出来

对象的内存布局

对象在内存中存储的布局分为3部分,对象头,实例数据和对齐填充

  • 对象头包括俩部分:第一部分用于储存对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向县能成ID,偏向时间戳等,另外一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过指针来确定他是那个类的实例
  • 实例数据部分是对象真正储存有效信息,也是在程序代码中定义的各种类型字段内容
  • 第三部分对其填充,并不是必然存在的,也没有实际意义

垃圾回收机制

哪里回收

上面介绍了java运行时内存区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈,这三个区域随线程而生,随线程而死,这几个区域内存分配和回收都具有确定性,这几个区域不用过多的考虑回收,因为线程结束,或者方法结束时,内存自然就回收了,而java的堆和方法区不一样,一个接口中多个实现类所需要的内存可能不一样,一个方法中的多个分支,需要的内存也可能不一样,我们只有在程序运行期间,才知道会创建那些对象,这部分的内存分配和回收都是动态的,垃圾回收器主要关注这个部分。

如何知道对象是否可回收

  • 引用计数法
    给对象加一个引用计数器,每当有地方引用他时就加一,引用失效时就减一,任何时刻,引用计数为0 就是没有被引用的,但他很难解决,对象互相引用的情况,所以虚拟机并没有使用这种方法。
  • 可达性算法
    在主流的实现中都是采用可达性算法,来判定对象是否存活,这个算法的基本思路是通过一个“GC Root” 的对象作为起点,从这些起点向下搜索,搜索做过的路径叫做引用链,当一个对象到“GC Root” 没有任何引用链时,说明此对象是不可用的
    这里写图片描述

在java语言中,可作为GC Root 的对象包括以下几种

  • 虚拟机栈中的引用对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法占中jin引用的对象

引用还分为 强引用 软引用 弱引用 虚引用##

  • 强引用(StrongReference)
    强引用是指在代码中普遍存在的Object obj =new Object(),这类的引用有引用变量指向时,永远不会被垃圾回收,JVM宁可抛出OutofMemory也不会回收这种对象,如果想中断某个强引用和对象的之间的联系,可以将引用赋值为null
  • 软引用(SoftReference)
    用来描述一些有用但非必须的对象,对于软引用关联着的对象,在系统发生内存溢出之前,会把这些对象列入回收范围之内,进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出异常,软引用经常用于内存敏感的高速缓存,比如,网页缓存,图片缓存,防止内存泄漏,增强代码的健壮性
 Object o = new Object();
 SoftReference<Object> softReference = new SoftReference<Object>(o);
 //如果设置 o=null,如果软引用没有被回收依然可以通过获取object对象
 Object o1 = softReference.get();
  • 弱引用(WeakReference)
    表示非必须的对象,不管内存是否充足,都会回收该对象
    只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。
  • 虚引用(PhontomReference
    虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

生存还是死亡

即使在可达性算法中,不可达对象,也不是非死不可,这时他们处于“缓刑”阶段,要宣告一个对象真正死亡需要至少俩个标记阶段, 如果发现对象没有引用链,则会进行第一次标记,并进行一次筛选,筛选的条件是此对象是否有必要进行finalize()方法,当对象没有覆盖finalize(),或者finalize()已经调用过,这俩种都视为“没有必要执行”
若果这个对象被视为 有必要执行,则会放入到一个F-Queue队列 中,并稍后有虚拟机建立一个低优先级的Finalizer线程去执行它,所谓的执行,是指会触发这个方法,但并不承诺等他执行完成,并不建议大家用这个方法做回收,try-finally 更加靠谱

回收方法区

主要回收两类

  • 废弃常量

    • 假如一个abd字符串 已经进入常量池,而当前程序没有任何一个 string 叫做adc ,也没有其他地方引用这个常量,就是废弃常量
  • 无用的类

    • 判断一个无用的类条件更加的苛刻
    • 所有类的实例都被回收,也就是java堆中不存在该类的任何实例
    • 加载该类的classloader 被回收
    • 该类对应的java.lang.class 对象没有任何地方被引用,无法在任何地方通过反 射,访问该类的方法
    • 满足上面那三个条件,就可以被回收

垃圾回收算法

  • 标记-清除算法
    该算法分为标记和清除俩个阶段,首先要标记出需要回收的各个对象,在标记完成后统一回收被标记的对象, 他有俩个不足
    1 效率问题:标记清除俩个过程效率都不是很高
    2 空间问题:会产生大量的不连续空间
    这里写图片描述
  • 复制算法
    为了解决效率问题,他可以将内存划分为完全相同的俩块,每次只使用其中的一块,当这块用完了,就把还存活的复制到另一块上去,然后把已经使用过的内存空间一次清理掉,不足,会把内存缩小为原来的一半
    这里写图片描述
  • 标记整理算法
    复制算法在存活较多的情况下,效率较低,而且会浪费掉50%的空间 ,所以老年代不能选择这种算法,根据老年代存活率特别高的特点,又提出一种 标记整理的算法,标记过程和“标记清除” 一样,但后续步骤不是对可回收对象进行清理,而是让所有存活的对象,向一端移动,然后直接清理掉端以外的内存
    这里写图片描述
  • 分代收集算法
    当前虚拟机大部分采用,分代收集算法,这种算法并没有特别思想,只是根据对象的存活周期不同把内存划分为几块,一般是吧java堆分为新生代和老年代,这样就可以根据年代的特点采用不同的算法,提高效率,新生代每次垃圾回收都会有大量的对象死去,少量存活,那就用复制算法,老年代存活率较低,那就使用标记-清除,或标记-整理法

在这里插入图片描述

  • 新生代(Young Generation
    大多数对象在新生代中创建,其中很多对象的生命周期很短,每次新生代的垃圾回收(又称 Minor GC),只有少量对象存活,所以选择复制算法,因为少量的复制成本就可以完成
    新生代又分为三个区,一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成,当Eden区满了之后,还存活的对象复制到Survivor区中的一个,当这个Survivor区满了之后,此区存活但不满足晋升条件的对象,复制到另一个Survivor区,对象每一次Minor GC年龄加一,达到年龄的阈值后,晋升老年区,默认的阈值为15岁

  • 老年代(Old Generation)
    新生代经历n次垃圾回收,还存活的对象就会被放到老年代,此区域中对象存活率高,老年代的垃圾回收,通常用标记清理和标记整理的方法,整堆包括新生代和老年代的垃圾回收称为Full GC

  • 永久代(Perm Generation)
    主要存放元数据,如Class何Method的元数据,与垃圾回收对象的关系不大,相对于新生代和老年代来说,该区划分对垃圾回收影响较小

什么时候回收

GC的类型:

  • 分配内存不够引起的GC,会stop world,是并发GC,其他线程都会停止,直到GC完成

    • 当你new一个新的对象时,如果内存不够会进行Full GC
  • 内存达到一定的阈值,进行的GC,是一个后台GC,不会stop world

    • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

    • Minor GC触发条件:当Eden区满时,触发Minor GC。

    • 老年代 GC(Full GC ):指发生在老年代的 GC,经常会伴随至少一次的 Minor GC 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。

    • Full GC触发条件:(1)老年代空间不足(2)升到老年代的对象大于老年代剩余空间

  • 显示调用时进行的GC,显式调用System.gc时会调用Full GC

参考:深入理解java虚拟机

https://tech.meituan.com/2017/12/29/jvm-optimize.html

https://jsonchao.github.io/2019/08/18/Android%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96/

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值