垃圾回收机制(四)

1. 垃圾回收算法
1.1 复制算法(Copying)

​ 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

​ 专家经过大量实验发现:新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

​ HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

1.2 标记-清除算法(Mark-Sweep)

​ 标记-清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
​ 它的主要不足空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

1.3 标记-整理算法(Mark-Compact)

​ 首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。

2. JVM垃圾回收器

​ 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,因此在新生代通常使用的是复制算法。

​ 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

2.1 垃圾回收器分类

​ JVM常见的垃圾回收器分为三类:单线程垃圾回收器、多线程并行垃圾回收器和多线程并发垃圾回收器。

垃圾回收器.png

3 各类垃圾收集器的介绍
3.1 单线程垃圾回收器

​ 在JDK1.3.1之前,单线程回收器是唯一的选择。它的单线程意义不仅仅是说它只会使用一个CPU或一个收集线程去完成垃圾收集工作。而且它进行垃圾回收的时候,必须暂停其他所有的工作线程(Stop The World,STW),直到它收集完成。它适合Client模式的应用,在单CPU环境下,它简单高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。

​ 串行的垃圾收集器有两种,Serial与Serial Old,一般两者搭配使用。新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。Client应用或者命令行程序可以,通过-XX:+UseSerialGC可以开启上述回收模式。下图是其运行过程示意图。

单线程垃圾回收器.png

3.2 多线程并行垃圾回收器

​ ParNew和Parallel Scavenge都属于并行垃圾回收器,使用多线程并行处理的方式来减少垃圾收集时间,让用户代码获得更长的运行时间。并行垃圾回收器最终的关注点是高吞吐量

​ ParNew和Parallel Scavenge都是新生代的回收器,采用的是复制算法来多线程回收对象。

​ 垃圾回收过程和单线程回收比较类似,只是在上图回收阶段JVM开启多线程来收集待回收的对象。

多线程并行垃圾回收器.png

3.3 多线程并发垃圾回收器

​ CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。通过垃圾回收线程和用户线程并发的方式,来实现及时响应的需求。

CMS.png

​ 从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对复杂一些,整个过程分为4个步骤,包含:

  • 初始标记:耗时短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记:和用户的应用程序同时进行,进行GC Roots追踪的过程。
  • 重新标记:短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  • 并发清除:和用户线程一起请求待回收对象。

​ 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

​ **浮动垃圾:**由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

3.4 G1回收器

​ G1垃圾回收器是在Java7 update 4之后引入的一个新的垃圾回收器。G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。

3.4.1 G1回收器的内存分块

本质上来说,G1垃圾回收器依然是一个分代垃圾回收器。但是它与一般的回收器所不同的是,它引入了额外的概念-Region。G1垃圾回收器把堆划分成一个个大小相同的Region。在HotSpot的实现中,整个堆被划分成2048左右个Region。每个Region的大小在1-32MB之间,具体多大取决于堆的大小。

G1垃圾回收器的分代也是建立在这些Region的基础上的。对于Region来说,它会有一个分代的类型,并且是唯一一个。即每一个Region,它要么是young的,要么是old的。

还有一类十分特殊的Humongous。所谓的Humongous,就是一个对象的大小超过了某一个阈值——HotSpot中是Region的1/2,那么它会被标记为Humongous。如果我们审视HotSpot的其余的垃圾回收器,可以发现这种对象以前被称为大对象,会被直接分配老年代。而在G1回收器中,则是做了特殊的处理。
G1并不要求相同类型的region要相邻。换言之,就是G1回收器不要求它们连续。当然在逻辑上,分代依旧是连续的。因此,G1回收器的堆区内存划分如下图:

G1内存划分.png

**注:**其中E代表的是Eden,S代表的是Survivor,H代表的是Humongous,剩余的深蓝色代表的是Old(或者Tenured),灰色的代表的是空闲的region

一个Region是G1回收器一次回收的最小单元。即每一次回收都是回收N个Region。这个N是多少,主要受到G1回收的效率和用户设置的软实时目标有关。每一次的回收,G1会选择可能回收最多垃圾的Region进行回收。与此同时,G1回收器会维护一个空间Region的链表。每次回收之后的Region都会被加入到这个链表中。

3.4.2 G1的回收流程

​ G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

​ 在初始标记阶段,G1会STW标记GC-Roots,在程序运行期间不断监控GC-Roots,并且标记要回收的对象。在最终标记阶段,会多线程并行标记新产生的待回收对象,最后根据用户配置,筛选可能回收最多垃圾的Region进行回收。

G1处理流程.png

3.5 垃圾回收器对比

新生代垃圾回收器对比:

回收器回收算法回收器类型回收对象
Serial复制算法单线程新生代
ParNew复制算法并行的多线程收集器新生代
Parallel Scavenge (侧重吞吐量)复制算法并行的多线程收集器新生代

老年代垃圾回收器对比:

回收器回收算法收集器类型回收对象
Serial Old标记整理算法单线程老年代
Parallel Old标记整理算法并行的多线程收集器老年代
CMS (侧重及时响应)标记清除算法并行与并发收集器老年代
G1标记整理 + 化整为零并行与并发收集器跨新生代和老年代

注:吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间)

垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间

4. 垃圾回收器的配合使用场景
  • 场景1:服务器在client模式下,限定单个CPU的环境中使用Serial收集器
    • 指令:-XX:+UseSerialGC
  • 场景2:服务器在Server模式多核处理器,对用户响应速度有要求的情况下,使用ParNew+CMS组合
    • 指令:-XX:+UseConcMarkSweepGC 指定使用CMS后,会默认使用ParNew作为新生代收集器
  • 场景3:服务器在高吞吐量为目标,应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互的场景中,使用Parallel Scavenge收集器。
    • 指令:-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间,大于0的毫秒数
    • 指令:-XX:GCTimeRatio 设置垃圾收集时间占总时间的比率,0<n<100的整数,默认值是1%–1/(1+99),即n=99
    • 指令:-XX:+UseParallelGC强制使用该收集器,打开该收集器后,将使用Parallel Scavenge(年轻代)+Serial Old(老年代)的组合进行GC
    • 指令:-XX:+UseParallelOldGC,打开该收集器后,将使用Parallel Scavenge(年轻代)+Parallel Old(老年代)的组合进行GC
  • 场景4:面向服务端应用,针对具有大内存、多处理器的机器的情况,可以考虑使用G1垃圾回收器。
    • 指令:-XX:+UseG1GC 指定使用G1收集器;
    • 指令:-XX:InitiatingHeapOccupancyPercent 当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
    • 指令:-XX:MaxGCPauseMillis 为G1设置暂停时间目标,默认值为200毫秒;
    • 指令:-XX:G1HeapRegionSize 设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region
5. 常量池与String

常量池有很多概念,包括运行时常量池、class常量池、字符串常量池。虚拟机规范只规定以上区域属于方法区,并没有规定虚拟机厂商的实现。

严格来说是静态常量池和运行时常量池,静态常量池是存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。

  • 运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。这两个常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。

字面量
给基本类型变量赋值的方式就叫做字面量或者字面值。比如:int i=120; long j=10L;

符号引用:包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。

**直接引用:**具体对象的索引值。

6.String类的实现

​ String 类被 final 关键字修饰,而且变量 char 数组也被 final 修饰。我们知道类被 final 修饰代表该类不可继承,而 char[]被 private+final 修饰,代表了 String 对象不可被更改。Java 实现的这个特性叫作 String 对象的不可变性,即 String 对象一旦创建成功,就不能再对它进行改变。

​ 在 Java 中,通常有两种创建字符串对象的方式,

  • 一种是通过字符串常量的方式创建,如 String str=“abc”;

    • 这种方式首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。
  • 另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”)。

    • 这种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象;最后,str 将引用 String 对象。

​ 如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串的引用,如果没有会把首次遇到的字符串的引用添加到常量池中;如果有,就返回常量池中的字符串引用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值