天天看点

Java 基础——垃圾回收器与内存分配策略

一、简述

java 和C++ 之间隔着一堵墙,就是内存动态分配和垃圾回收,墙里的人想出来,墙外人想进去。

垃圾回收需要考虑三个问题?

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

分析java 运行时内存区域,知道 程序计数器、虚拟机栈、本地方法栈 3个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作。这三个区域会随着方法结束或者线程结束时,内存自然就跟着回收了。

        程序计数器只是记录一个代码执行位置,不会占用多少内存,而且不会内存溢出。现在就只剩下堆和方法区了,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分的内存的分配和回收都是动态的,垃圾收集器关注的是这部分的内存。

二 、对象已死吗?

GC对那些死去的对象进行回收?所以要进行对象的死活进行判定,如何进行判定?

1 引用计数法

算法描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器值为0的对象就是不可能再被使用的

缺点: 很难解决对象之间的相互循环引用的问题。

2.可达性分析法

  通过一系列的名为“GC ROOTS”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。 

Java 基础——垃圾回收器与内存分配策略

对象object5、object6和object7虽然互相关联,但是它们到GC Roots 是不可达的,所以它们被判定是可回收的

在Java语言中可作为GC Roots的对象包括下面几种:

  • 虚拟机(栈帧中的本地变量表)中的引用对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般所说的Native方法)的引用的对象。

3.再谈引用

  在JDK1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用强度依次逐渐减弱。

  • 强引用 ——类似“Object object = new Object()”这类的引用,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用——内存不够时触发GC ,会把软引用的对象尝试回收。
  • 弱引用——下一次GC时,会强制把若引用的对象进行回收。
  • 虚引用——对象被回收时,会收到一个通知。

4.回收方法区

根据Java虚拟机规范的规定,方法区无法满足内存分配需求时,也会抛出OutOfMemoryError异常,虽然规范规定虚拟机可以不实现垃圾收集,因为和堆的垃圾回收效率相比,方法区的回收效率实在太低,但是此部分内存区域也是可以被回收的。

方法区的垃圾回收主要有两种,分别是对废弃常量的回收和对无用类的回收。当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。

方法区中的类需要同时满足以下三个条件才能被标记为无用的类:

1.Java堆中不存在该类的任何实例对象;

2.加载该类的类加载器已经被回收;

3.该类对应的java.lang.Class对象不在任何地方被引用,且无法在任何地方通过反射访问该类的方法。

当满足上述三个条件的类才可以被回收,但是并不是一定会被回收,需要参数进行控制,例如HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收。

三 垃圾收集算法

3.1 标记——清除算法

分为两个阶段,标记、清除。

思路:标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

不足:一个是效率问题,两个阶段的效率都不高,另一个时空间问题,标记清除后悔留下大量的不连续的内存碎片,空间碎片太多,可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

3.2 复制算法

将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块内存中,然后再把已使用过的内存一起清空,这样使得每次都是对整个半区就行内存回收。内存分配时也不需要考虑内存碎片等复杂问题,只需要移动堆顶指针,按顺序分配内存即可,实现简单,云心高效。这种算法的代价是将内存缩小为原来的一半。

3.3标记-整理算法

标记(同标记-清除)后让所有的存活对象都移向一端,然后直接清理掉端边界以外的内存。

3.4分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新的意思,只是根据对象存活周期的不同将内存划分为几块。一般是将Java堆分为新生代和老年代,这样根据各个年代的特点采用适当的收集算法。在新生代由于每次都有大批的对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理“算法进行回收。

四、垃圾收集器类型

类型:

Serial 收集器:单线程收集,工作的时候其他线程必须停止;

ParNew收集器:就是Serial的多线程版本。

Parallel Scavenge 收集器:达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)。虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

CMS收集器:包括初始标记、并发标记、重新标记、并发清除四步;

G1收集器:具备并发和并行的特点。最新最好的收集器。

总结:这些收集器的性能指标围绕在 并发、并行、收集时间上一些指标进行优化,理解这些就行了,这些细节实在太专业繁琐,搞不太明白。

并行和并发不是一回事 
  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
  • 并发 (Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上

 看一下G1的图示:

Java 基础——垃圾回收器与内存分配策略

 几个常用的概念:

新生代Eden :对象在新生代Eden区分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

大对象进入老年代:需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(例子中的byte[]数组就是典型的大对象)。大对象堆虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

长期存活的对象将进入老年代。

堆参数设置 ——在androidStudio 这里IDE的工具中可以设置相关参数。

  -Xms:初始堆大小 

  -Xmx:最大堆大小 

  -XX:NewSize=n:设置年轻代初始值大小 

  -XX:MaxnewSize:表示新生代可被分配的内存的最大上限;当然这个值应该小于-Xmx的值; 

  -Xmn:至于这个参数则是对 -XX:newSize、-XX:MaxnewSize两个参数的同时配置,也就是说如果通过-Xmn来配置新生代的内存大小,那么-XX:newSize = -XX:MaxnewSize = -Xmn,虽然会很方便,但需要注意的是这个参数是在JDK1.4版本以后才使用的 

  -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 

  -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 

  -XX:MaxPermSize=n:设置持久代大小

文章参考:深入理解JVM虚拟机 、https://blog.csdn.net/naonao2014/article/details/82115914

继续阅读