美文网首页
java内存分区及垃圾回收

java内存分区及垃圾回收

作者: 蒸汽飞船 | 来源:发表于2018-07-10 23:20 被阅读6次

    Android垃圾回收:
    分配内存-GC不回收软引用(GC_FOR_MALLOC)-增长到最大堆-(GC回收软引用(GC_BEFORE_OOM)+增长到最大堆)

    虚拟机栈:Java虚拟机栈描述的是Java方法执行的内存模型:Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法。

    1.java内存


    java内存模型

    参考java内存区域

    程序计数器

    虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。

    由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
      在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
      由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

    Java虚拟机栈

    1. Java虚拟机栈描述的是Java方法执行的内存模型:Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。
    2. Java虚拟机栈是线程私有的,它的生命周期与线程相同。
    3. 程序员主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。
      局部变量表存放了编译时期可知的各种基本数据类型对象引用
      局部变量表所需的内存空间在编译时期完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
    4. Java虚拟机规范对这个区域规定了两种异常情况:
    • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
    • 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

    本地方法栈

    本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务(也就是字节码),而本地方法栈为虚拟机使用到的Native方法服务。

    Java堆

    Java堆是被所有的线程共享的一块内存区域,在虚拟机启动时创建。
    Java堆的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
    Java堆是垃圾回收器管理的主要区域,因此也被称为"GC堆"。
    从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代、老生代、持久带;

    方法区

    方法区也是被所有的线程共享的一块内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码和运行时常量池等数据。
    Java虚拟机规范对方法区的限制非常宽松,可以选择不实现垃圾回收。
    这区域的内存回收目标主要是针对常量池的回收和类型的卸载。
    Java虚拟机规范规定,当方法区无法满足内存分配的需求时,将抛出OutOfMemoryError异常。
    运行时常量池:
    运行时常量池是方法区的一部分。CLass文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放,如:String类的intern()方法。

    方法区1.8之前算在持久代,后来换成了元数据区MetaspaceJava 虚拟机16:Metaspace
    Metaspace是方法区在HotSpot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存。因此Metaspace具体大小理论上取决于32位/64位系统可用内存的大小,可见也不是无限制的,需要配置参数。

    三个问题:参考

    • 那些内存需要回收?(对象是否可以被回收的两种经典算法: 引用计数法 和 可达性分析算法)
    • 什么时候回收? (堆的新生代、老年代、永久代的垃圾回收时机,MinorGC 和 FullGC)
    • 如何回收?(三种经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法 和 七种垃圾收集器)

    一. 如何确定一个对象是否可以被回收?

    1.引用计数法(Reference Counting Collector)

    优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
    缺点: 无法检测出循环引用。

    2. 可达性分析算法:判断对象的引用链是否可达

    程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
    java中可作为GC Root的对象有
      1.虚拟机栈中引用的对象(局部变量表)
      2.方法区中静态属性引用的对象
      3. 方法区中常量引用的对象
      4.本地方法栈中引用的对象(Native对象)

    二. 垃圾收集算法

    1. 标记-清除算法分析
      标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
    2. 标记-整理算法
      标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。该垃圾回收算法适用于对象存活率高的场景(老年代)
    3. 复制算法
      复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
      事实上,现在商用的虚拟机都采用这种算法来回收新生代。
    4. 分代算法:
      不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三个模块
      堆空间分代

    分代:

    年轻代(Young Generation)
      1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
      2.新生代内存按照8:1:1的比例分为一个Eden区,两个 Survivor区0、1(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
      3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
      4.新生代发生的GC也叫做Minor GC(未成年GC),MinorGC发生频率比较高(不一定等Eden区满了才触发)

    年老代(Old Generation)
      1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
      2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC(成年GC)即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

    持久代(Permanent Generation)
      用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

    三.垃圾收集器

    如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
    新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
    老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
    略。。。

    四. 内存分配与回收策略

    1. 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。
    2. 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
    3. 多次Minor GC后存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认MaxTenuringThreshold为15)的Minor GC后,就会被晋升到老年代中。
    4. 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

    相关文章

      网友评论

          本文标题:java内存分区及垃圾回收

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