image frame

GC—全流程

1、minorGC 和 Full GC 区别

     新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。

     老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

2、minorGC 过程详解

     在初始阶段,新创建的对象被分配到 Eden 区,Survivor 的两块空间都为空。

图一

     当Eden区满了的时候,minor garbage 被触发。

图二

     经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收, 并且存活的对象年龄都增大一岁。

图三

     在下一次的 Minor GC 中,Eden 区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到 Survivor区。当 Eden 和 s0区空间满了,S0 的所有的数据都被复制到S1,需要注意的是,在上次 Minor GC 过程中移动到S0 中的两个对象在复制到 S1 后其年龄要加1。此时 Eden 区 S0 区被清空,所有存活的数据都复制到了 S1 区,并且 S1 区存在着年龄不一样的对象,过程如下图所示:

图四

     再下一次 Minor GC 则重复这个过程,这一次 Survivor 的两个区对换,存活的对象被复制到 S0,存活的对象年龄加1,Eden 区和另一个 Survivor 区被清空。

图五

     再经过几次 Minor GC 之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold 默认是15),就会被从年轻代 Promotion 到老年代。

图六

     随着 MinorGC 一次又一次的进行,不断会有新的对象被 Promote 到老年代。

图七

     上面基本上覆盖了整个年轻代所有的回收过程。最终,MajorGC将会在老年代发生,老年代的空间将会被清除和压缩(标记-清除或者标记-整理)。从上面的过程可以看出,Eden 区是连续的空间,且 Survivor 总有一个为空。经过一次 GC 和复制,一个 Survivor 中保存着当前还活着的对象,而 Eden 区和另一个 Survivor 区的内容都不再需要了,可以直接清空,到下一次 GC 时,两个 Survivor 的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将 Eden 区和一个 Survivor 中仍然存活的对象拷贝到另一个 Survivor 中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下(基于大部分对象存活周期很短的事实)高效,如果在老年代采用停止复制,则是非常不合适的。

     老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-压缩算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。在发生 Minor GC 时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次 Full GC,否则,就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行 MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC( 这代表着如果设置-
XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。

3、整体描述

     大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1(“To”),并且对象的年龄还会加 1( Eden 区 -> Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,From 和 To 会交换他们的角色,也就是新的 To 就是上次 GC 前的 From ,新的 From 就是上次 GC 前的 To。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到 To 区被填满,To 区被填满之后,会将所有对象移动到年老代中。

4、GC 触发条件

     Minor GC 触发条件:Eden 区满时。Full GC 触发条件:

  • 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行;
  • 老年代空间不足;
  • 方法去空间不足;
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
  • 由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

5、对象进入老年代的四种情况

     假如进行Minor GC时发现,存活的对象在ToSpace区中存不下,那么把存活的对象存入老年代。

图八

     大对象直接进入老年代:假设新创建的对象很大,比如为5M(这个值可以通过PretenureSizeThreshold这个参数进行设置,默认3M),那么即使Eden区有足够的空间来存放,也不会存放在Eden区,而是直接存入老年代。

图九

     长期存活的对象将进入老年代:此外,如果对象在Eden出生并且经过1次Minor GC后仍然存活,并且能被To区容纳,那么将被移动到To区,并且把对象的年龄设置为1,对象没"熬过"一次Minor GC(没有被回收,也没有因为To区没有空间而被移动到老年代中),年龄就增加一岁,当它的年龄增加到一定程度(默认15岁,配置参数-XX:MaxTenuringThreshold),就会被晋升到老年代中。

     动态对象年龄判定:还有一种情况,如果在From空间中,相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁(默认)。

图十

6、空间分配担保

     在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlerPromotionFailure 这个参数设置的值( true 或 flase )是否允许担保失败(如果这个值为 true,代表着 JVM 说,我允许在这种条件下尝试执行 Minor GC,出了事我负责)。

     如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlerPromotionFailure 为 false,那么这次 Minor GC 将升级为 Full GC。如果老年代最大可用的连续空间大于历次晋升到老年代对象的平均大小,那么 HandlerPromotionFailure 为 true 的情况下,可以尝试进行一次 Minor GC,但这是有风险的,如果本次将要晋升到老年代的对象很多,那么 Minor GC 还是无法执行,此时还得改为 Full GC。

     注意:JDK 6Update 24 之后,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大 小就会进行 Minor GC,否则进行 Full GC。

GC—基础知识

     JVM 的 GC 是指垃圾回收,主要是对堆内存的回收。本文将介绍 JVM 中一次完整的 GC 流程是怎样
的,首先抛出第一个问题,什么样的对象会是 JVM 回收的目标?

1、可达性分析算法(GC Roots)

     有一种引用计数法,可以用来判断对象被引用的次数,如果引用次数为0,则代表可以被回收。这种实现方式比较简单,但对于循环引用的情况束手无策,所以 Java 采用了可达性分析算法。即判断某个对象是否与 GC Roots 的这类对象之间的路径可达,若不可达,则有可能成为回收对象,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。在 Java 中,可作为 GC Roots 的对象包括以下几种:

  • 虚拟机栈(本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

2、JVM中的堆结构

     JVM 中的堆可划分为两大部分,新生代和老年代,大小比例为1:2,如下:

JVM 分代比例

     其中,新生代分为 Eden 区和 Survivor 区, Survivor 幸存者区又分为大小相等的两块 from 和 to
区。这便是 JVM 中堆的结构和各部分默认的比例,当然这些比例都可通过对应 JVM 参数来调整。完整的 JMM 如下:

JVM 内存模型全景

2.1、为何新生代要分为三个区

     这里需要介绍新生代的垃圾回收算法——复制算法。该算法的核心是将可用内存按容量划分为大小
相等的两块,每次回收周期只用其中一块,当这一块的内存用完,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间清理掉。

     优点:不必考虑内存碎片问题;效率高。

     缺点:可用容量减少为原来的一半,比较浪费。

     最优设置:根据权威数据分析,90%的对象都是朝生夕死的,所以采用10%的空间用作交换区,因为交换区必须要有等量的两个,所以采用复制算法中新生代中三个区默认分配比例为8:1:1。

2.2、新生代对象的分配和回收

     基本上新的对象优先在 Eden 区分配;

     当 Eden 区没有足够空间时,会发起一次 Minor GC;

     Minor GC 回收新生代采用复制回收算法的改进版本。即:
from 区和 to 区的两个交换区,这两个区只有一个区有数据。采用8:1:1的默认分配比例(-XX:SurvivorRatio默认为8,代表 Eden 区与 Survivor 区的大小比例)

2.3、老年代对象的分配和回收

     老年代的对象一般来自于新生代中的长期存活对象。这里有一概念叫做年龄阈值,每个对象定义了年龄计数器,经
过一次 Minor GC (在交换区)后年龄加1,对象年龄达到15次后将会晋升到老年代,老年代空间不够时进行 Full GC。当然这个参数仍是可以通过 JVM 参数(-XX:MaxTenuringThreshold,默认15)来调整。

     大对象直接进入老年代。即超过 Eden 区空间,或超过一个参数值(-
XX:PretenureSizeThreshold=30m,无默认值)。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

     对象提前晋升到老年代(组团)。动态年龄判定:如果在 Survivor 区中相同年龄所有对象大小总和
大于 Survivor 区大小的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无须等到自己
的晋升年龄。

3、JVM完整的GC流程

     对象的正常流程:Eden 区 -> Survivor 区 -> 老年代。

     新生代GC:Minor GC;老年代GC:Full GC,比 Minor GC 慢10倍,JVM 会“stop the world”,严重
影响性能。

     总结:内存区域不够用了,就会引发GC。Minor GC 避免不了,Full GC 尽量避免。
处理方式:保存堆栈快照日志、分析内存泄漏、调整内存设置控制垃圾回收频率,选择合适的垃圾
回收器等。

三段式国密

三段式国密通讯

     三段式国密的问题

     1、三段式国密一次一密,每次通信都需要进行 SM2 运算,SM2 运算是对性能的极大损耗;

     2、签名不能代替摘要,签名说的是不可抵赖性,摘要说的是防篡改性;

     3、私钥放在了客户端。

兜底在系统设计中的重要性

系统调用关系

     生产环境存在如上图的调用关系,应用 AB 在往下游发起 RPC 调用时,都先去向全局的序列服务申请序列号,序列号用来作为全局流水号或者追踪号。TDSQL 架构如图:

TDSQL示意图

     三周前,TDSQL 做了序列服务数据库的机房迁移,从甲机房迁移到了乙机房,当晚验证没有问题。但是三周过后,硬件负载 F5 出了问题,导致 B 获取序列失败。由于 B 在实现上强依赖于序列服务,序列服务,整个 B 系统的所有交易都走不通了。但是 A 仍然可以正常发起 RPC 调用,因为 A 在实现上,如果调用失败,兜底的方案是会在本地生成序列。

     由于 A 应用依赖于 B 应用,而 B 缺少类似于 A 的兜底策略,导致整体对客的服务全都不可用。

     事实上,序列号只不过是用来追踪程序的一个功能,A 应用的情况,就算不走全局序列服务来生成,本地生成一个序列号也没有任何影响,只不过序列服务那边溯源的时候查不到这笔交易。但是总好过 B 应用,在序列服务出问题的时候,连核心的交易都走不通要好。

String引用传值问题

     Java 中是没有引用传递的,Java 中只有值传递。Java 中所谓的引用传递,也只是传递了"引用变量保存的地址值"。 Java 中判断"引用传递"有没有达到你预期的效果,前提要基于内存模型,并结合你的上下文,根据有没有利用引用变量"传递"的这个地址值去修改实际内存对象的数据来判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package edu.zjnu;

/**
* @author 杨海波
* @date 2024/2/29 20:49
* @description String 引用传值
*/
public class Main {

public static void main(String[] args) {
test01();
test02();
}

private static void test01() {
String str = "out";
// str = "out2";
f01(str);
System.out.println(str);
}

/**
* 工程实践表明,java 中没有引用传递,只有值传递。
* <p>
* Java 中所谓的引用传递,也只是传递了"引用变量保存的地址值",如果无法做到
* 根据这个引用指向的地址去修改实际对象的值,那么函数外部的实际对象无法被修改。
* 特别的,String 是不可变对象:
* 在 test01 这个例子中, 外部的 ' str = "out2"; 这行代码只是在方法区的
* 字符串常量池新建了一个字符串常量对象 "out2",并将该对象的地址赋值给引用变
* 量 str。内部的 str = "in"; 也是同理,但是当 f01 函数执行 str = "in";
* 这行代码时,引用变量保存的值(该值是尊从值传递的)是新的字符串常量对象 "in"
* 的地址,当 f01 执行完毕,这个值尊从函数作用域规则消失了。
*
* @param str
*/
private static void f01(String str) {
str = "in";
}


/**
* 解决办法如下
*/
private static void test02() {
StringWrapper str = new StringWrapper("out");
f02(str);
System.out.println(str.value);
}

private static void f02(StringWrapper str) {
str.value = "in";
}


static class StringWrapper {
// 实际值
public String value;

public StringWrapper(String value) {
this.value = value;
}
}
}
  • Copyrights © 2017 - 2025 杨海波
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信