java中的【垃圾回收】

栈帧(Stack Frame):java栈是线程私有的,是java方法的执行模型,每个线程对应一个栈,每个线程在执行一个方法时会创建一个对应的栈帧,栈帧负责存储局部变量变量表、操作数栈
动态链接和方法返回地址等信息,每个方法的调用过程相当于栈帧在java栈的入栈和出栈过程,随方法调用创建和销毁,同时可能存在多个,但只有栈顶的当前栈帧是活动的。
栈帧是栈中的一个栈元素,是一种用于帮助虚拟机执行方法调用与方法执行的数据结构,当前线程中,每执行一个方法就会往栈中插入一个栈帧,当一个方法调用结束的时候,其对应的栈帧也会被丢弃。
栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法返回地址(即返回到方法的调用者)以及操作数栈。java虚拟机栈(java virtual machine stacks)是线程私有的,换句话说,每个线程都会有一个栈,所以对于栈帧来说不存在并发调用的情况。栈帧是由动态链接、方法返回地址、操作数栈、局部变量表组成。其中局部变量表是以数组的形式存的,而且当前栈帧的方法所需要的分配的最大长度是在编译时就确定了,局部变量表通过index来寻址,变量从index[0]k开始传递。对于54位的数据类型,假如其占用了数组中的index[n]和index[n+1]两个位置,那么不允许单独访问其中的某一个位置,java虚拟机规范中规定,如果出现一个64位的数据被单独访问某一部分时,则在类加载机制中的校验阶段就应该抛出异常。

概述

Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。
java一般内存申请有两种:静态内存和动态内存。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般所说的垃圾回收也是针对的这一部分。
总之,Stack(栈)的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap(堆)则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题。

垃圾检测、回收算法

垃圾收集器一般必须完成两件事:
检测出垃圾、回收垃圾

检测算法

怎么检测出垃圾?
引用计数法:给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。
但是,如果有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:
可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集
一般包括java栈中引用的对象、方法区常量池中引用的对象,本地方法中引用的对象等。
该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
在Java中,可作为GC Root的对象包括以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(即一般说的Native方法)引用的对象。
总之,JVM在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器回收

回收算法

1、标记-清除(Mark-sweep)

算法和名字一样,分为两个阶段:标记和清除。标记所有需要回收的对象,然后统一回收。这是最基础的算法,后续的收集算法都是基于这个算法扩展的。
不足:效率低;标记清除之后会产生大量碎片

2、复制(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间

3、标记-整理(Mark-Compact)

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,
第一阶段从根节点开始标记所有被引用对象,
第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。
此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题

4、分代收集算法

这是当前商业虚拟机常用的垃圾收集算法。分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。
因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
为什么要运用分代垃圾回收策略?
在java程序运行的过程中,会产生大量的对象,因每个对象所能承担的职责不同所具有的功能不同所以也有着不一样的生命周期,有的对象生命周期较长,
比如Http请求中的Session对象,线程,Socket连接等;有的对象生命周期较短,比如String对象,由于其不变类的特性,有的在使用一次后即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对会很长,而且对于存活时间较长的对象进行的扫描工作等都是徒劳
因此就需要引入分治的思想,所谓分治的思想就是因地制宜,将对象进行代的划分,把不同生命周期的对象放在不同的代上使用不同的垃圾回收方式
如何划分?
将对象按其生命周期的不同划分成:年轻代(Young Generation)、年老代(Old Generation)、持久代
(Permanent Generation)
。其中持久代主要存放的是类信息,所以与java对象的回收关系不大,与回收息息相关的是年轻代和年老代。

年轻代

是所有新对象产生的地方。
年轻代被分为3个部分——Enden区和两个Survivor区(From和to)。
当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区
(假设为from区)。
Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。
这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。
通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。

年老代

在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算
法,因为那些对于这些回收战场上的老兵来说是小儿科。通常会在老年代内存被占满时将会触发Full GC,
回收整个堆内存。

持久代

用于存放静态文件,比如java类、方法等。
持久代对垃圾回收没有显著的影响。
这里之所以最后讲分代,是因为分代里涉及了前面几种算法:
①年轻代:涉及了复制算法;
②年老代:涉及了“标记-整理(Mark-Sweep)”的算法。

垃圾收集器

Serial收集器:是最基本、历史最久的收集器,单线程,并且在收集是必须暂停所有的工作线程。
ParNew收集器:是Serial收集器的多线程版本。
Parallel Scavenge收集器:新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量,这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算。
Serial Old收集器:Serial 收集器的老年代版本,单线程,主要是标记—整理算法来收集垃圾。
Parallel Old收集器:Parallel Scavenge的老年代版本,多线程,主要是标记—整理算法来收集垃圾。
注意:
Parallel Old 和 Serial Old 不能同时搭配使用,后者性能较差发挥不出前者的作用
CMS收集器:一种以获取最短回收停顿时间为目标的收集器;基于标记清除算法,并发收集、低停顿、运作过程复杂(初始标记、并发标记、重新标记、并发清除)。
缺点
1。对CPU资源非常敏感(占用资源);
2。无法处理浮动垃圾(在并发清除时,用户线程新产生的垃圾叫浮动垃圾),可能出现“Concurrent Mode Failure”失败;
3。产生大量内存碎片。
G1收集器具有以下特性:
分代收集:G1可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生对象和已经存活一段时间的对象;
空间整合:采用标记整理算法,局部采用复制算法(Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC;
可预测的停顿:能够让使用者明确指定一个时间片段内,消耗在垃圾收集上的时间不超过时间范围内。

总结

对新生代的对象的收集称为minor GC
对老年代的对象的收集称为full GC;
程序中主动调用System.gc()强制执行的GC为full GC
强引用:默认情况下,对象采用的均为强引用。
软引用:适用于缓存场景(只有在内存不够用的情况下才会被回收)。
弱引用:在GC时一定会被GC回收。
虚引用:用于判断对象是否被GC。
垃圾收集算法
标记—清除算法:有两点不足,一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除后会产生大量不连续的内存碎片;
复制算法:解决了内存碎片问题,但是内存利用率低;
标记整理算法:解决了内存碎片问题。
分代收集算法
新生代
每次GC时都会有大量对象死去,少量存活,使用复制算法;
新生代又分为Eden区、Survivor(Survivor from、Survivor to)大小比例默认为8:1:1;
JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一个Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。每熬过一次Minor GC,年龄就加1,当它的年龄到了一定程度(默认15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代;如果Survivor相同年龄所有对象大小的总和大
于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。
老年代
老年代中的对象存活率高、没有额外空间进行分配,就是用标记—清除或标记—整理算法;
大对象可以直接进入老年代,JVM可以配置对象达到阈值后进入老年代的大小(XX:PretenureSizeThreshold)
总之,只需记住以下几点
1、年轻代
①、eden 伊甸园区:存放刚刚创建出来的对象
②、survivor 幸存者区:存入通过GC后还幸存的对象,分为了s0 ,s1两块空间
当触发Young GC了,先去标记-清除 eden区的对象和s0中的对象,将幸存对象使用复制算法到s1空间,每次Young GC都会形成类似的操作。
③、当一个对象在survivor存活了15轮后,会进入到老年代。
2、老年代
老年代快满了,会触发full gc(整体GC)
有两种方法进入老年代:
①、年轻代对象活过15轮。
②、在刚创建对象的时候,占用的空间很大。

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值