美文网首页安卓面试Java基础相关
JAVA基础-虚拟机和内存回收

JAVA基础-虚拟机和内存回收

作者: 来lol里 | 来源:发表于2021-01-07 23:25 被阅读0次

    虚拟机的组成

    主要是由所有左边线程共享的方法区、堆,和线程单独所在的虚拟机栈、本地方法栈、程序计数器两部分组成。

    image.png
    • 虚拟机栈
      存储当前线程运行方法所需的数据,指令、返回地址
      Java虚拟机栈是线程私有的,即生命周期和线程相同。Java虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表(线程方法里的局部参数变量的引用)、操作数栈(具体参数变量操作)、动态链接(多态相关)、返回地址(方法return到哪里)等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 本地方法栈
      本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法。(HotSpot虚拟机将虚拟机栈和本地方法栈合二为一)
    • 程序计数器
      指向当前线程正在运行的字节码指令的地址(行数),其实就是保证多线程切换下线程的正常执行,当在同一时刻一个处理器内核只会执行一条线程,处理器切换线程时并不会记录上一个线程执行到哪个位置,所以为了线程切换后依然能恢复到原位,每条线程都需要有各自独立的程序计数器。
    • 方法区
      属于共享内存区域,存储已被虚拟机加载的类信息(各种编译后的class)、常量(可以理解为类中的全局变量)、静态变量(static修饰的)、即时编译器编译后的代码等数据。

    • 前面所说的程序计数器、Java虚拟机栈、本地方法栈通常只占很小一部分的内存空间,对与大多数应用来说,Java堆(Java Heap)才是JVM管理的内存空间中最大的一块。此区域存在的唯一目的就是存放对象实例和数组,几乎所有的对象实例都会在这被分配内存,而且Java堆是被所有线程共享的一块内存区域

    再来看张图,其实内存的回收大部分是在共享内存区这一块的。


    image.png

    内存回收检查方法

    想要回收垃圾需要先统计哪些是垃圾,现在主要的两种方式分别是 引数计数法和可达性分析,目前虚拟机基本都是采用可达性算法。

    • 引数计数法 每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。此方法主要是无法解决两个对象相互引用无法释放的问题。
    • 可达性分析 从GC Roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC Roots无法到达的对象便成了垃圾回收的对象,随时可被GC回收。
      可以作为GC Roots的对象(见下图):
      1.虚拟机栈的栈帧的局部变量表所引用的对象;
      2.本地方法栈的JNI所引用的对象;
      3.方法区的静态变量和(运行时的)常量所引用的对象;
      从GC Roots开始遍历所有可达的对象,则认定为该存活的对象,其他则为可回收内存。
    image.png

    内存回收的算法

    • 标记清除算法
      根据被标记的垃圾对象,逐个进行清理。效率高,但是清理回收后,导致内存不连续,形成内存碎片。此时如果有新对象需要消耗更大的内存,虽然总空闲内存足够,由于内存不连续,会导致创建失败。利用率高,但是有内存碎片。

      image.png
    • 标记整理算法
      同上个方法相比,在清除之后多了一个整理的功能,因为需要内存移动,导致效率一般。以上两个标记类的算法更适合老年代。

      image.png
    • 复制算法
      将可用的内存按容量划分为大小相等的两块(from,to)。每次把没有被标志的,即幸存的对象复制到一边去(to),然后把(from)这块内存格式化的清理。这样子就能保证被清理的内存总是连续可用的。然而,每次只是用其中一块(总有一块是空的【to区域】),造成了内存的使用率折半。实现简单,运行快,利用率低,回收新生代中。新生代中(eden、from、to的比例是8:1:1)90%的对象是不需要回收的,所以按照这个比例大部分空间比例会放在新生代中的Eden区,剩下的from和to用在了回收复制。空间担保,当新生代放不下后会放到老年代。

      image.png
    • 分代收集法
      其实分代收集法并不是一个新的算法,只是整合了复制算法和标记整理算法,管理它们。针对不同的内存区域,根据这些内存区域的特性使用不同的算法(选择复制法还是标记整理法)。例如,在新生代中的Eden区,对象的存活率是非常低的,这里被采用复制算法;在老年代内存区,存活率超级高,这里会采用标记整理算法。

    堆对象的的内存分配策略

    image.png

    对象优先在Eden分配,大对象直接进入老年代,长期存活的对象将进入老年代
    动态对象年龄判定是否需要放入老年代。
    新生代出发的GC叫做Minor GC,老年代触发的GC叫做Full GC。

    四种引用方式

    • 强引用
      一般new出来的都是。只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收
    • 软引用(SoftReference)
      软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常
    • 弱引用(WeakReference)
      弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收
    • 虚引用(PhantomReference)

    内存泄露、内存溢出、内存抖动

    • 内存溢出
      即为out of memory, 当你要求分配的内存超过了系统给你的内存时, 系统就会抛出out of memory的异常(每个Android能用的内存是有限的),简单理解就是装不下,流出来了,有个词比较合适精满自溢。
    • 内存泄露
      即为memory leak, 一个对象被创建后, 你不再使用它了, 但因为某种原因它又没有成为垃圾对象, 这块内存不能再被分配置使用,简单理解就是没用的你没有释放,结果越存越多,然后就顶破了泄露了。这两个的关系就是内存泄露不多时没有太大影响, 但积累得多了就会导致应用运动缓慢, 到最后就会内存溢出。所以平时我们还是要避免泄露。
    • 内存溢出
      即为memory churn,存抖动是指在短时间内有大量的对象被创建或者被回收的现象,内存抖动出现原因主要是频繁(很重要)在循环里创建对象(导致大量对象在短时间内被创建,由于新对象是要占用内存空间的而且是频繁,如果一次或者两次在循环里创建对象对内存影响不大,不会造成严重内存抖动这样可以接受也不可避免,频繁的话就很内存抖动很严重),内存抖动的影响是如果抖动很频繁,会导致垃圾回收机制频繁运行。
      以上三者是需要我们尽量避免的,通过复用对象的方式减少产生的对象,大对象需要先压缩后创建,无用对象及时释放等方法,保证项目内存的合理使用。

    相关文章

      网友评论

        本文标题:JAVA基础-虚拟机和内存回收

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