三、垃圾收集器与内存分配策略-个人总结

终于看完第三章了真不容易,看这一章的时候就能碰上很多之前遇到过关于JVM的面试题,收获还是挺多的,垃圾收集器那几节笔记还没有做,等到以后再补

点击查看深入理解Java虚拟机-汇总

一、概述

当需要排查各种内存溢出,内存泄漏时,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要去了解垃圾收集和内存分配。

需要关注哪些内存回收

对于Java内存运行时的区域中,Java堆和方法区是需要关注垃圾回收的区域,因为具有很显著的不确定性:一个接口的多个实现类需要内存可能会不一样;一个方法执行不同条件分支所需要内存也可能不一样,只有再运行期间,才能知道程序创建了哪些对象,创建了多少。所以,这部分内存的分配和回收是动态的。

对于程序计数器、虚拟机栈、本地方法栈,生命周期基本上和线程生命周期一致,在编译阶段就已经确定下来每个栈帧需要分配多少内存,在方法或线程结束后,这部分内存就自动的就会被回收,所以不需要关心这部分内存回收问题。

二、对象已死

1、引用计数法

在对象中添加一个引用计数器,当这个对象增加一个引用时,计数器就加一,相反减少一个引用计数器就减一,当计数器为0时就说明该对象不会再被使用,就需要被清除。

优点:原理简单,并且效率比较高

缺点:会额外占用一些内存空间,并且如果存在两个对象相互引用,除此之外,没有其他任何引用,虽然这两个对象也不会再被访问,但因为计数器不为0,所以无法回收它们。

2、可达性分析算法

基本思路:通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径成为“引用链”,如果某个对象到"GC Roots"间没有任何引用链相连(从GC Roots到这个对象不可达),证明该对象不可能再被使用,将会被判为可回收对象。

可以作为GC Roots对象的不是一种,包含了好几种,例如:虚拟机栈,栈帧中本地变量表中引用的对象(例如:方法参数、局部变量、临时变量等)、方法区中类静态属性和常量引用的对象等等,具体参考书。

在局部垃圾回收的时候,例如只针对Java堆中某块区域(新生代)垃圾回收,需要考虑到这部分区域中的某些对象可能被其他区域的对象引用,所以需要将关联区域的对象也加入到GC Roots集合中,才能保证可达性分析算法的正确性。

3、再谈引用

之前引用定义:一个reference类型的数据中存储的是另一块内存的起始地址,则这个reference数据就代表这块内存的引用。

JDK1.2之后,对应用概念进行扩充:

  • 强引用(Strongly Reference):最传统最普遍的引用定义,通过new关键字创建对象进行引用赋值的,都是强引用。只要还存在强引用,垃圾收集器就不会回收掉被引用的对象。
  • 软引用(Soft Reference):用来描述一些还有用,但是非必须的的对象。在系统即将发生内存溢出异常之前,垃圾收集器会将软引用关联的对象进行二次回收,如果回收之后还没有足够的内存,才会抛出内存溢出的异常。提供SoftReference类来实现软引用。
  • 弱引用(Weak Reference):弱引用也用来描述非必须对象,但它的强度比软引用更低,被弱引用关联的对象,只能存活到下次垃圾回收之前,等到下次垃圾回收时无论内存是否足够,都会回收掉被弱引用关联的对象
  • 虚引用(Phantom Reference):也成为”幽灵引用“或”幻影引用“,是最弱的一种引用关系。一个对象是否有虚引用完全不会对其生存时间产生影响,也无法通过虚引用取得对象实例。存在的唯一目的:在这个对象被垃圾收集器回收时,收到一个系统通知。

4、生存还是死亡

一个对象真正被回收,至少要经历两次标记过程。第一次是经过可达性分析被标记为不可达对象,之后会进行第二次标记筛选,依据就是该对象是否覆盖了finalize方法,以及finalize方法是否已经执行过了,如果有必要执行则会将该对象放置在一个叫F-Queue的队列中,并稍后虚拟机会自动创建一条低优先级的Finalizer线程去执行finalize方法,finalize方法只会被执行一次,且并不保证一定会执行完(如果该方法执行缓慢,或者发生死循环,会导致队列中其他对象永久等待,可能会导致回收系统崩溃),对象最后一次逃脱被回收的机会就是在执行finalize方法时,将自己(this关键字)赋值给某个类的变量或对象的成员变量。并不推荐使用finalize方法,因为运行代价高昂,不确定性大,且无法保证调用顺序,关闭资源的事情可以使用try,finally来完成。

5、回收方法区

方法区回收的效果同城并不理想,在方法区中主要回收:废弃常量和不再使用的类型

废弃常量

不再使用的类型

三、垃圾收集算法

1、分代收集理论

分代收集是一种理论,符合大多数程序实际运行情况的经验法则,现在商业虚拟机的垃圾收集器大多数都遵循分代收集理论

先关注两个分代假说:

  • 弱分代假说:绝大多数对象都是朝生夕死。
  • 强分代假说:经过多次垃圾收集过程依旧存活下来的对象,越难以死亡被回收

根据这两个假说总结出设计原则:应该将Java堆划分成不同区域,根据对象的年龄分配到不同的区域中,并在不同区域实行不同的垃圾收集算法

新生代、老年代,Minor GC、Major GC、Full GC

根据不足新增一条假说:

  • 跨代引用假说:跨代引用相对于同代引用仅占极少数

在新生代中建立一个全局的数据结构(记忆集)来解决跨代引用

2、标记-清除算法

根据标记判定算法,先标记出所有需要回收的对象,之后统一回收掉所有被标记的对象;也可以反过来,标记出所有存活对象,之后再将未标记的对象统一清除。

缺点:

  • 执行效率不稳定
  • 内存空间碎片化

3、标记-复制算法

基本思想就是将内存分为大小相等的两块,每次只使用其中的一块,当这块内存使用完后,就将还存活的对象复制到另一块内存上,然后将使用过的内存一次性全部清空。这样做的好处就是可以减少内存空间的碎片化,但这种垃圾回收算法只适用于每次只有少量对象存活,大多数对象都需要被回收的情况,只有这样才复制最少对象,实现最优化。但缺点就是可用内存减少为原来的一般,比较浪费空间。

新生代为什么分为一块Eden空间和两块Survivor空间:在新生代,大多数对象存活时间较短都是朝生夕死,存活下来的对象就比较少。所以将新生代就划分为一块较大的Eden空间和两块较小的Survivor空间,HotSpot虚拟机默认两块空间大小是8:1,每次使用都只使用一块Eden空间和一块Survivor空间,当发生垃圾收集时,就将这两块空间上存活的对象复制转移到另一块未使用的Survivor空间上,也就是可用内存空间占总内存的90%,只有10%会被浪费掉。

**如果将存活对象赋值到Survivor空间时,超过10%容量大小怎么办:**当Survivor空间不足以容纳垃圾收集(Minor GC)后存活对象,即超过了10%,就需要其他内存区域(大多数是老年代)进行分配担保,这些对象通过分配担保机制可以直接进入老年代,后面还会有详细的分配担保…

**缺点:**1.在对象存活率较高时,需要复制的对象就比较多,会导致效率降低;2.浪费空间,总会有内存空间会被浪费掉,并且还需要额外的内存空间为其进行分配担保,以应对所以对象100%存活的极端情况。

4、标记-整理算法

由于标记-复制算法的缺点,所以在老年代一般是不会选中复制算法的。不过有一种新的算法就是标记-整理,基本思想就是,也先对内存空间对象进行标记,之后并不是直接清除,而是将所有存活对象都向内存空间一端移动,然后直接清理掉边界以外的内存。与标记清除算法区别就在于是否是移动式的回收算法。

而这种算法也有令人左右为难的地方,如果移动存活对象,特别是在老年代每次回收都会有大量对象存活,进行对象的移动并更新引用这些对象的地方操作时,必须暂停用户应用程序才能进行(Stop The World),将会是一种代价沉重的操作,会造成一些延迟。但如果不进行移动直接清除,会使内存空间碎片化,就必须依赖更复杂的内存分配器和内存访问器来解决,虽然不会造成延迟,但相对吞吐量会降低(内存分配器和内存访问器的访问频率要比垃圾收集器的访问频率高)。

hotspot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法实现的,而关注低延迟的CMS收集器则是基于标记-清除算法实现的。标记清除会造成内存空间碎片化,但也有一种"和稀泥"的解决办法,大多数时间都是采用标记清除算法,暂时容忍内存碎片化,当内存碎片化影响到对象分配时,再采用标记整理算法收集一次,整理内存空间。

四、HotSpot的算法实现细节

1、根节点枚举

可固定作为GC Roots的结点主要是:全局性引用,如常量和类型静态属性;执行上下文,如栈帧中的本地变量表。

在枚举根节点的时候,需要注意几点:首先,在枚举过程中,必须暂停用户所有线程,也就是“Stop The World”,因为这个过程必须要在可以保持一致性的快照中进行,如果出现在分析过程中,根节点的对象引用关系还在不停发生变化,这样就无法保障分析结果的准确性,所以暂停是必须的。其次,现在虚拟机使用的都是精准式垃圾收集,并不需要逐个检查每个根节点,虚拟机可以直接知道哪些地方存放着对象的引用,HotSpot虚拟机使用称为OopMap的数据结构来达到目的的,一旦完成类加载,虚拟机就会把对象内什么偏移量上是什么类型数据计算出来,在即时编译过程中,也会在特定位置记录下栈里和寄存器里哪些位置是引用,这样在扫描的时候就可以直接得知这些信息了

2、安全点

记录引用位置,生成对应的OopMap的时候只有在特定位置才可以记录,这些位置就是安全点,目的主要是防止为每一条指定都生成对应的OopMap,占用过多的额外存储空间。有了安全点的限制,也就决定的不能在代码任意位置都能停顿下来进行垃圾收集,必须强制执行达到安全点后才能暂停进行垃圾收集,安全点设置数量不能太少(垃圾收集等待时间过长)也不能太过频繁(增加运行负担),一般会在方法调用、循环跳转、异常跳转设置安全点,因为这些代码符合设置安全点的条件:是否具有让程序长时间执行的特征

如何让所有的线程都跑到最近的安全点,然后停顿下来也是一个问题?有两种解决方案:一、抢先式中断,发生垃圾收集时,先将用户所有线程全部中断,如果发现有线程不在安全点上,则恢复这个线程,让它跑到最近的安全点上后在中断;二、主动式中断,在垃圾收集的时候不需要对线程直接操作,而是设置一个标志位,标志位与安全点是重合的,所有线程在执行的时候会不断轮询标志位,当标志位为真表明要进行垃圾收集了,自己就在最近的安全点主动挂起。因为轮询标志位操作会比较频繁,所以必须要十分高效,HotSpot虚拟机采用内存保护陷阱的方法,精简到一条汇编指令就可以,当需要挂起线程时,就将标志位的内存页设为不可读,线程执行到标志位的时候会产生抛出一个自陷异常信号,然后再预先注册好的异常处理器中挂起线程实现等待。

3、安全区域

安全区域是为了解决,用户线程处于sleep或Blocked状态,无法自己走到安全点挂起的问题。安全区域可以看成是被拉长的安全点,在安全区域被内任意位置进行垃圾收集都是安全的,如果线程执行带安全区域的代码,就会标识自己已经进入了安全区域,如果这是发生垃圾收集,则垃圾收集器就不用去管处于安全区域的线程,处于安全区域的线程如果要离开安全区域,则首先会检查虚拟机是否已经完成根节点的枚举(或者是垃圾收集过程中其他需要暂停用户线程的行为),如果完成了,则离开,否则会一直等待直到完成。

4、记忆集与卡表

记忆集就是为了解决跨代引用问题,将非收集区域中跨代引用的引用记录下来直接加入到GCRoots中,减少GC Roots的扫描范围。

卡表(卡精度)只是记忆集进行记录的其中一种精度。最简单的一种形式就是字节数组,数组中每个元素大小就是1Byte,每个元素对应非收集区域内存中一块指定大小的内存块,这个内存块被称为“卡页”,大小为512Byte,卡页中只要有一个对象存在跨代引用,就将字节数组置1,说明这个元素变脏了。

5、写屏障

修改卡表中元素的时机,是在对象赋值那一刻。Hotspot虚拟机采用“写屏障”在对象赋值的时候更新卡表,写屏障有些类似于Spring中的AOP,在对象赋值前叫做“写前屏障”,对象赋值后叫做“写后屏障”,在G1之前一直都是用写后屏障。写屏障在面临高平发的时候还会出现伪共享问题,一种简单解决方案就是,在更新卡表之前,先进行判断是否更新过,如果没有再更新,再jdk7之后,提供-XX:+UserCondCardMark参数,决定是否开启更新条件判断。

6、并发的可达性分析

垃圾收集中暂定用户线程的这段时间,枚举根节点所占用的时间短且相对固定,但从根节点往下,遍历对象图时所占用时间就和Java堆大小成正比。之所以再垃圾收集时需要暂停用户线程,是因为用户线程会使对象引用关系不停发生变化,会影响垃圾收集的分析和收集工作,如果要解决并发修改的问题,则有两种解决方案:增量更新,和原始快照。增量更新:已经扫描过的存活对象如果新增引用关系,则标记这个已经存活读写,当扫描完成之后,在对这些标记的对象再扫描一次。原始快照:无论在扫描过程中是否删除引用关系,都会按照刚开始扫描那一刻的对象图快照进行扫描。

五、经典垃圾收集器

1、Serial收集器

2、ParNew收集器

3、Parallel Scavenge收集器

4、Serial Old收集器

5、Parallel Old收集器

6、CMS收集器

7、Garbage First收集器

六、低延迟收集器

1、Shenandoah收集器

2、ZGC收集器

七、选择合适的垃圾收集器

1、Epsilon收集器

Epsilon收集器与其他收集器有很大区别,就是他并不能进行垃圾收集,在应对短时间、小规模的服务形式,如仅仅需要运行数分钟或数秒,能够保证Java虚拟机正常分配内存即可,不需要进行垃圾收集的这种情况下,Epsilon收集器就比较适合,因为传统的垃圾收集器有着占用内存大、在容器中启动时间长、即时编译需要缓慢优化的特点。

其实垃圾收集器的功能职责并不仅仅是垃圾收集,可以将他换一个更为贴切的名字:自动内存管理子系统,应为除了垃圾收集之外,他还需要堆的管理与布局、对象的分配、与解释器协作、与编译器协作、与监控子系统协作等职责。这些功能中,其余的都可以舍弃,但其中堆的管理与布局、对象的分配是必须要实现的,因为这两个不仅是垃圾收集器最小化实现功能模块,也是Java虚拟机能正常运行的必要支持。

2、收集器的权衡

如果选择垃圾收集器,主要从三方面权衡:

  1. 应用程序的主要关注点是什么。如果是数据分析等需要快速计算出结果的情况,则需要关注吞吐量;如果系统的停顿时间直接影响服务质量,甚至会导致事务超时(SLA应用),则需要主要关注延迟停顿时间;如果是客户端应用或嵌入式应用,则需要关注垃圾收集的内存占用。
  2. 运行应用的硬件怎么样。如:运行程序的硬件处理器数量是多少?分配内存大小?选择的操作系统是Windows还是Linux?
  3. 使用JDK的发行商、版本号是什么?该JDK对应的《Java虚拟机规范》是哪个版本的?

3、虚拟机及垃圾收集器日志

从JDK9开始,才提供了统一日志处理框架,所以JDK9前后,查看日志的命令有所区别。日志级别从低到高,分别为:Trace、Debug、Info、Warn、Error、Off六种级别,JDK9后所有的日志都归收到"-Xlog"参数上,垃圾收集器日志只是Hotspot众多日志中的其中一个。

-Xlog[:[selector][:[output][:[decorators][:output-options]]]]

垃圾收集器日志的常用命令:

  1. 查看GC基本信息

    -XX:+PrintGC

    -Xlog:gc:

  2. 查看GC详细信息

    -XX:+PrintGCDetails

    -Xlog:gc*

  3. 查看GC前后的堆、方法区可用容量变化

    -XX:+PrintHeapAtGC

    -Xlog:gc+heap=debug

  4. 查看GC过程中用户线程并发时间及停顿时间

    -XX:+PrintGCApplicationConcurrentTime

    -XX:+PrintGCApplicationStoppedTime

    -Xlog:safepoint

  5. 查看收集器Ergonomics机制(自动设置堆空间各代分区大小,收集目标,从parallel收集器开始支持)

    -XX:+PrintAdaptiveSizePolicy

    -Xlog:gc+ergo*=trace

  6. 查看熬过收集后剩余对象的年龄分布信息

    -XX:+PrintTenuringDistribution

    -Xlog:gc+age=trace

其余参考《深入理解Java虚拟机》第三版中第三章对应目录,P126

4、垃圾收集器参数总结

参考《深入理解Java虚拟机》第三版中第三章对应目录,P128

八、内存分配与回收策略

1、对象优先在Eden分配

大多数情况下,对象会首先在Eden区进行分配,如果分配的时候空间不够,则会进行一次Minor GC。在MinorGC的时候,如果Survivor区没有足够空间容纳存活对象,则存活对象会通过分配担保机制提前进入老年代

2、大对象直接进入老年代

大对象通常是指很长的字符串或是数量很大的数组。大对象直接进入老年代是因为,在为大对象分配空间的时候,虽然可用空间足够,但是因为找不到足够的连续空间而提前触发Minor GC,以获得足够连续空间去存储,而复制的时候,大对象的复制也会产生高额的开销。HotSpot虚拟机可以通过-XX:PretenureSizeThreshold参数,指定大于该值的对象直接进入老年代。注意:这个参数在赋值的时候单位为字节(Byte),无法写为MB,且该参数只对SerialParNew两款新生代收集器有效。

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

虚拟机会为每个对象定义记录一个对象年龄(Age),存储在对象头,用4位存储,所以最大只能是15,默认也是15。对象被分配在Eden区,经过一次垃圾收集后仍存活,就会被分配到Survivor区,且设置对象年龄为1,之后每熬过一次垃圾收集,对象年龄就加1,达到设定值即可进入老年代。可以通过-XX:MaxTenuringThreshold进行设置年龄阈值。

4、动态对象的年龄判定

上面说对象年龄必须达到阈值才能进入老年代,也并不一定。如果在Survivor空间中相同年龄的所有对象大小总和达到Survivor空间的一半,则年龄大于或等于该年龄的对象都可以直接进入老年代,不用等到达到年龄阈值才进入。

5、空间分配担保

分配担保:新生代为了内存使用率,只使用一个Survivor空间来存储MinorGC后存活对象,如果存活对象比较多,极端情况下新生代中对象全部存活,则Survivor内存空间会不够用,这时候就需要老年代进行分配担保,将Survivor中无法存储的对象直接送入老年代。

在发生MinorGC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果大于则认为是安全的会进行MinorGC,如果小于,在JDK6 Update24之后,会再将老年代最大可以连续空间和历代晋升平均大小进行比较,如果大于则进行MinorGC,否则就进行Full GC。再JDK6 Update24之前,虚拟机会先查看-XX:HandlePromotionFailure是否允许担保失败参数,如果允许则会继续查看老年代最大可用连续空间是否大于历代晋升的平均大小,如果大于则进行MinorGC,否则进行FullGC。检查是为了确保老年代有足够空间进行为新生代进行分配担保,如果在MinorGC过程中发生担保失败,则虚拟机就会发起一次FullGC,这样停顿时间就会很长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值