垃圾回收(GC) 很干,很全

说一下垃圾回收机制

垃圾回收主要针对的是JVM的堆内存,我讲一下我了解的hotspot实现的jvm, 分为新生代和老年代,按照以前的说法,还有一个永久代,永久代在方法区里(物理上跟老年代相连),保存了class信息,静态变量,常量池等, jdk1.8之后,方法区实现发生了变化,取消了永久代的概念(hotspot特有的概念),使用了元空间的概念, 跟老年代分开,直接使用了jvm的内存(会使得内存溢出的可能性减小,空间也更容易扩展),同时方法区也转移一部分数据出去,例如类的静态变量和字符串常量池都放到了堆内存当中.在堆内存中,根据垃圾回收的范围,一般有两种,一种是minor GC(Young GC)针对新生代,一种是majorGC针对老年代,一般majorGC动作伴随着minorGC所以也可叫做fullGC.然后我还知道垃圾回收常见的搜集算法和回收算法.搜集算法包括:引用计数法(缺点是两个垃圾循环引用就没法标记),解决方法:根可达分析算法(GC root包括:栈中引用的对象(局部变量表中引用的对象), 类静态属性引用的对象, 常量引用的对象, Native方法引用的对象),标记之后就是涉及一个回收算法:包括标记清除法(会产生内存碎片,所以有些公司维护就重启服务器),复制算法(拷贝同时也清空,既做到垃圾回收,还做到了碎片整理,但是缺点内存空间浪费一倍,年轻代中S1和S0中就是使用这个算法),标记整理算法(就是在清理垃圾基础上多了一步碎片整理,一般在老年代内存不足时,触发fullGC,做碎片整理工作).jvm具体的垃圾回收器包括最早期的(Serial和Serial Old),中期的(Parallel Scanvenge和Parallel Old)和过渡期的(ParNew和CMS)和G1以及将来的ZGC,我简单说一下CMS(老年代回收器),CMS,初始标记的时候只标记根对象的第一层,然后在用户线程执行期间并发标记,然后再并发期间产生的错标和漏标会在下一阶段重新标记(并发标记,也触发stw)
G1(1.9以后才是默认垃圾回收器),G1允许用户手动设置一个期望的STW时间,G1并不会准确符合要求,只会尽可能缩短时间,G1将整个堆内存划分成若干相等大小的区域,默认2048,依然保留了伊甸区,幸存者区和老年代概念,但不是物理上连续,一块区域可能是年轻代也可能是老年代,空间大小不绝对固定.GC扫描内存时,无需扫描整块区域,扫描特定区域即可,增大支持的堆内存的大小,它回收时,会根据设定的STW调整策略,将要进行扫描的区域进行价值排序,如果设置的stw时间短,就优先回收一部分区域(如果设置时间过低会导致回收不完全,经常GC,导致吞吐量下降),他也使用复制算法,减少了碎片.大对象也单独存放了(humongous)

垃圾回收过程

  • 大部分情况下,对象会被分配在Eden区, 如果Eden区满了会进行young GC(Minor GC), 将存活的对象放入s0或者s1, 同时对象的年龄会加1, 当年龄增加到一定程度(大于15)时候晋升老年代;
  • 进行Minor GC时, Eden区域的存活对象还有survivor的from区会被复制到"to"区, "from区"中的对象也可能达到阈值晋升老年代, 同时Eden与"From"区会被清空."from"区和"to"区交换角色;
  • young GC 会一直重复,如果"From"区域或者"to"区域的空间不够, 会提前把对象放到老年代中(分配担保机制)

新生代到老年的不同情况:

  • 分配担保: 当Eden区满, Minor GC, Eden区和From区存活的对象无法装入到to区,就会通过分配担保机制提前放到老年代中;
  • 超大对象: 新生代无法容纳这个对象,直接到达老年代;
  • 长期存活的对象: 对象年龄达到15(15是因为对象Mark Word中用4位存储对象分代年龄)
  • 动态对象年龄判定: 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

为什么堆这么设计(包括分代, Survivor2个)?

新老年代

  • 如果没有新老年代, 每次扫描都需要所有区域全部扫描一遍
  • 分代之后, 主要扫描年轻代, 降低gc时间, 并且young GC 速度快.

Survivor区

  • 为了减少送到老年代的对象, 不然对象总能进入老年代, 有了Survivor区可以设置对象年龄, 达到指标才可以进入老年代, 保证老年代中的对象真正的老.
  • 能做到减少Old GC, 因为老年代内存空间大, 一般比例是 2: 1, 所以Old gc 消耗时间长. 尽量避免

为什么需要两个Survivor区

  • 解决碎片化, 没有Survivor 区如果回收或者进入老年区, survivor区就会产生碎片

增加Eden区,Minor GC的间隔变长了,会不会导致Minor GC的时间增加?

  • 单次Minor GC包含两部分:扫描新生代时间 + 复制存活对象
  • 增加Eden区,相当于增加了扫描新生代的时间,减少了复制存活对象的时间;而一般情况下,复制对象的成本要远高于扫描成本;
  • 如果存在较多长期存活的对象,增加Eden空间,反而会增加Minor GC的时间;
  • 如果存在较多短期存活的对象,增加Eden空间,Minor GC时间不会显著增加;
  • 总结:Minor GC的时间取决于 GC 后存活对象的数量,而不是Eden区的大小。

所有对象都在堆上分配空间吗?

逃逸: 方法中对象的引用被外面使用或者没有被返回则会在栈中开辟空间),对象会在栈中开辟空间

垃圾回收(GC) 很干,很全_第1张图片
垃圾回收(GC) 很干,很全_第2张图片

垃圾收集特点

  • 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(比如数据库连接,网络,IO等)
  • 程序无法精确的控制垃圾回收的运行,垃圾回收会在合适的时候进行,当对象永久性的失去引用,系统会在合适的时候回收它所占的内存。
  • 在垃圾回收机制回收任何对象之前,都会调用它的finalize方法,该方法可能使该对象重新复活,从而导致垃圾回收机制的取消。

对象在堆内存中的状态

可达状态:当创建一个对象后,如果有一个以上的引用变量引用它,则这个对象处于可达状态,程序可以通过对象的实例变量和方法进行调用。
可恢复状态:如果程序中的某个对象不再有任何引用变量引用它,它就进入到了可恢复状态,在这个状态下,系统的垃圾回收机制准备回收这该对象所占的内存。,在回收该对象之前会调用该对象的finalize方法,看是否还有其他变量引用,如果有可以由可恢复状态进入到可达状态。
不可达状态: 当对象与所有引用变量的关联都被切断,并且系统已经调用该对象的finalize方法也无法进入到可达状态, 从而导致进入不可达状态, 系统才会进行垃圾回收。

强制垃圾回收

程序员无法精准的控制系统何时进行垃圾回收,也无法明确在什么时间进行垃圾回收,程序只能控制一个对象不再被任何变量引用,但不能控制它何时被垃圾回收。
虽然程序无法精准的控制垃圾回收,但是依然可以通过代码指定系统进行强制垃圾回收。但是系统执行垃圾回收仍然是不确定的,大部分时候程序员只能指定强制垃圾回收,但是在何时进行还是由jvm自身决定。
两种方式进行强制垃圾回收:

  • 调用System类的gc方法
  • 调用Runtime类的gc方法

System和Runtime类的gc两者区别:
相同点: 都会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
System.gc()调用附带一个免责声明, 无法保证立即触发gc
不同点: Runtime.gc() 是 native method而System.gc() 是非 native method,它依次调用 Runtime.gc();
额外补充: 如果每次调用gc方法后想让gc必须执行,可以追加调用system. runFinalization方法

Finalize方法

在垃圾回收机制回收某个对象所占用的内存,会调用这个对象的finalize方法。

Finalize方法有4个特点:

  • 不要主动调用某个对象的finalize方法,把该方法交给垃圾回收机制自动执行
  • Finalize方法何时被调用,是否被调用 都是不确定的,不要把finalize方法当做一定会执行的方法。
  • 在jvm执行可恢复对象的finalize方法 可以使该对象或者系统中的其他对象重新进行到可达状态
  • 当jvm执行finalize 如果出现异常, 垃圾回收并不会报告异常,程序可以继续执行。

常见的垃圾收集器

  • 新生代:Serial收集器、ParNew收集器、Parallel Scavenge收集器;
  • 老年代:CMS收集器;
  • both:G1收集器;
  • 默认的垃圾回收器:Parallel Scavenge + Parallel Old。

Serial收集器:

  • 采用标记-复制算法, 串行回收和stop the world(stw)机制的方式执行内存回收.

Serial收集器还提供用于执行老年代垃圾收集的Serial old收集器。Serial old收集器同样也采用了串行回收和"stop the World"机制,只不过内存回收算法使用的是标记-压缩算法

  • 单线程收集器
  • 新生代采用标记-复制算法
  • 新生代收集器

优点: 简单高效
缺点: 如果垃圾回收时间过长, 就会造成应用卡顿的情况


ParNew收集器

  • 多线程垃圾收集器, serial收集器的多线程版本.
  • 还是一样标记-复制算法和stop the world机制
  • 新生代收集器

Parrallel Scavenge收集器

  • 多线程垃圾收集器
  • 同样也是基于并行回收和"stop-the-World"机制
  • 关注点是吞吐量(高效利用CPU), 提供了很多参数供用户找到最合适的停顿时间或者最大吞吐量
  • 自适应调节策略: 年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,在手动调优比较困难的场合,可以直接使用这种自适应的方式
  • 新生代收集器, 采用标记复制算法

CMS 并发标记清除
目的最短停顿时间, 为了注重用户体验
采用标记清除算法实现
老年代收集器
步骤:

  1. 初始标记: 暂停所有其他的线程, 并记录下直接与root相连的对象
  2. 并发标记: 同时开启GC和用户线程, 用一个闭包结构去记录可达对象.但这个阶段结束, 这个闭包结构并不能保证包含所有的可达对象. 因为用户线程可能会不断地更新引用域, 所以GC线程无法保证根可达性分析的实时性. 所以这个算法会跟踪记录这些发生引用更新的地方.
  3. 重新标记: 为了修正并发标记期间因为用户线程继续运行而导致标记产生变动的那一部分对象的标记记录
  4. 并发清除: 开始用户线程, 同时GC线程开始对未标记的区域做清扫.

初始标记和重新标记还是需要stop the world

优点:并发收集、低延迟。
缺点

  • 对CPU资源敏感
  • 无法处理浮动垃圾
  • 使用标记-清除算法会导致收集结束时会有大量的空间碎片产生。导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发FullGC。

G1收集器

面向服务器的垃圾收集器,主要针对多颗处理器即大容量内存的机器。以极高概率满足GC停顿时间要求的同时还具备高吞吐量性能。它同时兼顾年轻代和老年代。

  • 使用标记-整理算法 --> 不会产生碎片
  • 非常精确控制停顿, G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型),能让使用者明确指定在一个长度为M毫秒的时间片段内。

G1将整个Java堆(新生代、老年代)划分为多个大小固定的独立区域,并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。

**优点:**与其他GC收集器相比,G1使用了全新的分区算法,还在并发的基础上并行, 大内存应用上则发挥其优势。平衡点在6-8GB之间
**缺点: **G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高。经验上讲在小内存应用上CMS的表现大概率会优于G1


借用下硕的博客的图片:
硕的博客地址
垃圾回收(GC) 很干,很全_第3张图片

jdk1.8 默认垃圾收集器: 新生代(Parallel Scavenge) + 老年代(Parallel Old)
jdk1.9 默认G1
jdk11 使用的是实验性质的ZGC, 也是低停顿, 几乎所有地方并发执行, 除了初始标记是STW, 几乎所有停顿时间都在初始标记上.
ZGC技术: 着色指针Colored Pointer和 读屏障Load Barrier.

垃圾收集算法

包括: 标记-清除算法、标记-复制算法、标记-整理算法、分代收集算法。

分代收集算法: 新生代(标记-复制算法), 老年代(标记-整理算法)
标记整理比标记清除强在了不会产生内存碎片.

你可能感兴趣的:(JVM,jvm,java,算法)