第九周作业

用户头像
AIK
关注
发布于: 2020 年 08 月 05 日

垃圾收集器和java的内存分布有着紧密的联系,因此我们要对jvm的内存布局回顾一下,jvm的内存布局大致分为如下:





JVM运行时数据区域-例子



public void method(){
Object obj = new Object();
}



  • 生成了2个部分的内存区域,1)obj这个引用变量,因为是方法内的变量,放到JVM Stack里面,2)真正Object class的实例对象,放到Heap里面。

  • 上述的new 语句一共消耗12个bytes,JVM规定引用占用4个bytes(在JVM stack),而空对象是8个bytes(在 Heap)

  • 方法结束后,对应Stack中的变量马上回收,但是Heap中的对象要等到GC来回收。

垃圾判断算法

引用计数算法(Reference Counting)

给对象添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减一,任何时刻计数器为0的对象就是不可能再被使用的。

引用计数器算法无法解决对象循环引用的问题。

上图所示,A引用B,B引用A,但是外界引用都断了,不存在对“环”的引用,但是他们的引用计数器都是1,引用计数算法就不会对他们进行回收,会一直在内存中。

根搜索算法(Root Tracing)

在实际的生产语言中(java、C#等)都是使用根搜索算法判定对象是否存活

算法基本思路 就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(

Reference Chain)相连,则证明此对象是不可用的

在java语言中,GCRoots 包括

  • 在JVM栈(帧中的本地变量)中的引用

  • 方法区中的静态引用

  • JNI(即一般说的Native方法)中的引用

方法区

java虚拟机规范表示可以不要求虚拟机在这区实现GC,这区GC的“性价比”一般比较低。

在堆中,尤其是在新生代,常规应用进行一次GC一般可以回收70%-95%的空间,而方法区的FC效率远小于此。

当前的商业JVM都有实现方法区的GC,主要回收两部分内容:废弃常量与无用类。

类回收需要满足如下三个条件

该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例

加载该类的ClassLoader已经被GC

该类对应的java.lang.Class对象没有在任何地方呗引用,如不能在任何地方通过反射访问该类的方法。

在大量使用反射、冬天代理、CGlib等字节码框架、动态生成jsp以及OSGI这类频繁自定义一ClassLoader的场景都需要

JVM具备类卸载的支持以保证方法区不会溢出。

GC算法

  • 标记 -清楚算法(Mark-Sweep)

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

  • 复制算法(Copying)

  • 分代算法(Generational)

标记 -清楚算法(Mark-Sweep)

算法分为“标记”和”清除” 两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象。

缺点

效率问题,标记和清理两个过程效率都不高

空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前

触发另一次的垃圾搜索集动作。









最后阴影部分被回收掉,F、J、M不能被root trace到,因此也会被回收。

效率不高,需要扫描所有对象,堆越大,GC越大。碎片越严重。

存在内存碎片问题,GC次数越多,

复制(Coping)搜集算法

将可用内存划分为两块,每次只是用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来的整块内存空间一次性清理掉

这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针、按顺序分配内存

就可以了,实现简单,运行效率高。

只是这种算法的代价是将内存缩小为原来的一半,代价昂贵。

现在的商业虚拟机中都是用了这一种收集算法回收新生代。

将内存分为一块较大的eden空间和快较少的survival空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次性

拷贝到另外一块survival空间上,然后清理掉eden和用过的survivor:

Oracle Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10% 的内存是”浪费”的。

复制收集算法在对象存活率高的时候,效率有所下降。

如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都有100%存活的极端情况,所以在老年代一般不能直接选用这种算法。







只需要扫描存活的对象,效率更高

不会产生碎片

需要浪费额外的内存作为复制区

复制的算法非常适合生命周期比较短的对象,每次GC总能回收大部分的对象,复制的开销比较小

根据IBM的专门研究,98%的java对象只会存活一个GC周期,对这些对象很适合用复制算法。而且不用1:1的划分工作区和复制区的空间

标记整理算法(Mark-Compact)

标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。



没有内存碎片

比Mark-Sweep耗费更多的时间进行compact

分代收集算法(Genaratinal Collecting)

当前商业虚拟机的垃圾收集都是采用“分代收集”算法根据对象不同存活周期将内存划分为几块

一般是把java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批量对象死去,

只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。

综合前面几种GC算法的优缺点,针对不同生命周期的对象采用不同的GC算法。





Hotspot JVM 6(jdk8之前)中共划分为三个代:年轻代(Ypung Generation)\老年代(Old Generation)和永久代(Permanent Generation)





年轻代

新生代的对象都放在新生代,年轻代用复制算法进行GC(理论上年轻代的对象的生命周期非常短,所以适合复制算法)

年轻代分三个区。一个Eden区,两个Survivor区(可以通过参数设置Survivor个数)。对象在Eden区中生成,当Eden区满时,

此区的存活对象被复制到另外一个survivor区,当第二个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代,2个Survivor

是完全对称,轮流替换。

Eden和2个Survivor的缺省比例是8:1:1,也就是10%的空间会被浪费,可以根据GC log的信息调整大小的比例。

老年代

存放了经过一次或多次GC还存活的对象

一般采用Mark-Sweep或者Mark-Compact算法进行GC

有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个GC算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器

(追求吞吐量?追求最短的响应时间?)

永久代

  • 比不属于堆(Heap)但是GC也会涉及到这个区域

  • 存放了每个Class的结构信息,包括常量池、字段描述、方法描述、与垃圾收集要收集的对象关系不大。



内存分配

堆上分配

大多数情况在eden上分配,偶尔会直接在old上分配

栈上分配

原子类型的局部变量

内存回收

  • GC要做的是将那些dead的对象所占用的内存回收掉hotspot认为没有引用的对象是dead的hotspot将引用分为四种:Strong、Soft、Week、PhantomStrong即默认通过Object o = newObject()这种方式赋值的引用‘Soft、Week、Phantom这三种则都是继承Reference

  • 在Full GC时会对Reference类型的引用进行特殊处理Soft:内存不够时一定会被GC、长期不用也会被GCWeek:一定会被GC,当被mark为dead,会在ReferenceQueue中通知Phantom:贝莱就没有引用,当jvm heap中释放时会通知

###垃圾收集算法

GC的时机

  • 在分代模型的基础上,GC从时机上分为两种:scavenge GC和Full GC

  • Scavenge GC(Minor GC)触发时机:新对象生成时,Eden空间满了理论上Eden区大多数对象会在Scavenge GC回收,复制算法的执行效率会很高,Scavenge GC时间比较短。

  • Full GC对整个JVM进行整理,包括Young、Old和Perm主要的触发时机:1)、old满了 2)Perm满了 3)system.gc()效率很低,尽量减少Full GC。



用户头像

AIK

关注

还未添加个人签名 2019.01.17 加入

还未添加个人简介

评论

发布
暂无评论
第九周作业