|
| 1 | + |
| 2 | +## 复习:GC分类 |
| 3 | + |
| 4 | +针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC) |
| 5 | + |
| 6 | +- 部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为: |
| 7 | + |
| 8 | + - 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集 |
| 9 | + - 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。<mark>注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。</mark> |
| 10 | +- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有 G1 GC 会有这种行为 |
| 11 | +- 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。 |
| 12 | + |
| 13 | +1. 新生代收集:只有当Eden区满的时候就会进行新生代收集,所以新生代收集和S0区域和S1区域情况无关 |
| 14 | + |
| 15 | +2. 老年代收集和新生代收集的关系:进行老年代收集之前会先进行一次年轻代的垃圾收集,原因如下:一个比较大的对象无法放入新生代,那它自然会往老年代去放,如果老年代也放不下,那会先进行一次新生代的垃圾收集,之后尝试往新生代放,如果还是放不下,才会进行老年代的垃圾收集,之后在往老年代去放,这是一个过程,我来说明一下为什么需要往老年代放,但是放不下,而进行新生代垃圾收集的原因,这是因为新生代垃圾收集比老年代垃圾收集更加简单,这样做可以节省性能 |
| 16 | + |
| 17 | +3. 进行垃圾收集的时候,堆包含新生代、老年代、元空间/永久代:可以看出Heap后面包含着新生代、老年代、元空间,但是我们设置堆空间大小的时候设置的只是新生代、老年代而已,元空间是分开设置的 |
| 18 | + |
| 19 | +4. 哪些情况会触发Full GC: |
| 20 | +- 老年代空间不足 |
| 21 | +- 方法区空间不足 |
| 22 | +- 显示调用System.gc() |
| 23 | +- Minior GC进入老年代的数据的平均大小 大于 老年代的可用内存 |
| 24 | +- 大对象直接进入老年代,而老年代的可用空间不足 |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +## 不同GC分类的GC细节 |
| 29 | + |
| 30 | +用例代码: |
| 31 | + |
| 32 | +```Java |
| 33 | +/** |
| 34 | + * -XX:+PrintCommandLineFlags |
| 35 | + * |
| 36 | + * -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC |
| 37 | + * |
| 38 | + * -XX:+UseParNewGC:标明新生代使用ParNew GC |
| 39 | + * |
| 40 | + * -XX:+UseParallelGC:表明新生代使用Parallel GC |
| 41 | + * -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC |
| 42 | + * 说明:二者可以相互激活 |
| 43 | + * |
| 44 | + * -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用 |
| 45 | + * @author shkstart |
| 46 | + * @create 17:19 |
| 47 | + */ |
| 48 | +public class GCUseTest { |
| 49 | + public static void main(String[] args) { |
| 50 | + ArrayList<byte[]> list = new ArrayList<>(); |
| 51 | + |
| 52 | + while(true){ |
| 53 | + byte[] arr = new byte[1024 * 10];//10kb |
| 54 | + list.add(arr); |
| 55 | +// try { |
| 56 | +// Thread.sleep(5); |
| 57 | +// } catch (InterruptedException e) { |
| 58 | +// e.printStackTrace(); |
| 59 | +// } |
| 60 | + } |
| 61 | + } |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +### 老年代使用CMS GC |
| 66 | + |
| 67 | +**GC设置方法**:参数中使用-XX:+UseConcMarkSweepGC,说明老年代使用CMS GC,同时年轻代也会触发对ParNew的使用,因此添加该参数之后,新生代使用ParNew GC,而老年代使用CMS GC,整体是并发垃圾收集,主打低延迟 |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +打印出来的GC细节: |
| 72 | + |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | + |
| 77 | +### 新生代使用Serial GC |
| 78 | + |
| 79 | + **GC设置方法**:参数中使用-XX:+UseSerialGC,说明新生代使用Serial GC,同时老年代也会触发对Serial Old GC的使用,因此添加该参数之后,新生代使用Serial GC,而老年代使用Serial Old GC,整体是串行垃圾收集 |
| 80 | + |
| 81 | + |
| 82 | + |
| 83 | + 打印出来的GC细节: |
| 84 | + |
| 85 | + |
| 86 | + |
| 87 | +DefNew代表新生代使用Serial GC,然后Tenured代表老年代使用Serial Old GC |
| 88 | + |
| 89 | +## GC 日志分类 |
| 90 | + |
| 91 | +### MinorGC |
| 92 | + |
| 93 | +MinorGC(或 young GC 或 YGC)日志: |
| 94 | + |
| 95 | +```java |
| 96 | +[GC (Allocation Failure) [PSYoungGen: 31744K->2192K (36864K) ] 31744K->2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs] |
| 97 | +``` |
| 98 | + |
| 99 | + |
| 100 | + |
| 101 | + |
| 102 | + |
| 103 | +### FullGC |
| 104 | + |
| 105 | +```java |
| 106 | +[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K (132096K) ] [Par01dGen: 416K->5453K (50176K) ]5520K->5453K (182272K), [Metaspace: 20637K->20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] |
| 107 | +``` |
| 108 | + |
| 109 | + |
| 110 | + |
| 111 | + |
| 112 | + |
| 113 | +## GC 日志结构剖析 |
| 114 | + |
| 115 | +### 透过日志看垃圾收集器 |
| 116 | + |
| 117 | +- Serial 收集器:新生代显示 "[DefNew",即 Default New Generation |
| 118 | + |
| 119 | +- ParNew 收集器:新生代显示 "[ParNew",即 Parallel New Generation |
| 120 | + |
| 121 | +- Parallel Scavenge 收集器:新生代显示"[PSYoungGen",JDK1.7 使用的即 PSYoungGen |
| 122 | + |
| 123 | +- Parallel Old 收集器:老年代显示"[ParoldGen" |
| 124 | + |
| 125 | +- G1 收集器:显示”garbage-first heap“ |
| 126 | + |
| 127 | +### 透过日志看 GC 原因 |
| 128 | + |
| 129 | +- Allocation Failure:表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据 |
| 130 | +- Metadata GCThreshold:Metaspace 区不够用了 |
| 131 | +- FErgonomics:JVM 自适应调整导致的 GC |
| 132 | +- System:调用了 System.gc()方法 |
| 133 | + |
| 134 | +### 透过日志看 GC 前后情况 |
| 135 | + |
| 136 | +通过图示,我们可以发现 GC 日志格式的规律一般都是:GC 前内存占用-> GC 后内存占用(该区域内存总大小) |
| 137 | + |
| 138 | +```java |
| 139 | +[PSYoungGen: 5986K->696K (8704K) ] 5986K->704K (9216K) |
| 140 | +``` |
| 141 | + |
| 142 | +- 中括号内:GC 回收前年轻代堆大小,回收后大小,(年轻代堆总大小) |
| 143 | + |
| 144 | +- 括号外:GC 回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小) |
| 145 | + |
| 146 | +<mark>注意</mark>:Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代。原因是 Survivor 区只计算 from 部分,而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系,Eden:S0:S1=8:1:1。 |
| 147 | + |
| 148 | +### 透过日志看 GC 时间 |
| 149 | + |
| 150 | +GC 日志中有三个时间:user,sys 和 real |
| 151 | + |
| 152 | +- user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。 |
| 153 | +- sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间 |
| 154 | +- real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。对于并行 gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。 |
| 155 | + |
| 156 | +由于多核的原因,一般的 GC 事件中,real time 是小于 sys time + user time 的,因为一般是多个线程并发的去做 GC,所以 real time 是要小于 sys + user time 的。如果 real > sys + user 的话,则你的应用可能存在下列问题:IO 负载非常重或 CPU 不够用。 |
| 157 | + |
| 158 | +## Minor GC 日志解析 |
| 159 | + |
| 160 | +### 日志格式 |
| 161 | + |
| 162 | +```Java |
| 163 | +2021-09-06T08:44:49.453+0800: 4.396: [GC (Allocation Failure) [PSYoungGen: 76800K->8433K(89600K)] 76800K->8449K(294400K), 0.0060231 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] |
| 164 | +``` |
| 165 | + |
| 166 | +### 日志解析 |
| 167 | + |
| 168 | +#### 2021-09-06T08:44:49.453+0800 |
| 169 | + |
| 170 | +日志打印时间 日期格式 如 2013-05-04T21:53:59.234+0800 |
| 171 | + |
| 172 | +添加-XX:+PrintGCDateStamps参数 |
| 173 | + |
| 174 | +#### 4.396 |
| 175 | + |
| 176 | +gc 发生时,Java 虚拟机启动以来经过的秒数 |
| 177 | + |
| 178 | +添加-XX:+PrintGCTimeStamps该参数 |
| 179 | + |
| 180 | +#### [GC (Allocation Failure) |
| 181 | + |
| 182 | +发生了一次垃圾回收,这是一次 Minor GC。它不区分新生代 GC 还是老年代 GC,括号里的内容是 gc 发生的原因,这里 Allocation Failure 的原因是新生代中没有足够区域能够存放需要分配的数据而失败。 |
| 183 | + |
| 184 | +#### [PSYoungGen: 76800K->8433K(89600K)] |
| 185 | + |
| 186 | +**PSYoungGen**:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的 |
| 187 | + |
| 188 | +- **Serial收集器**:Default New Generation 显示Defnew |
| 189 | +- **ParNew收集器**:ParNew |
| 190 | +- **Parallel Scanvenge收集器**:PSYoung |
| 191 | +- 老年代和新生代同理,也是和收集器名称相关 |
| 192 | + |
| 193 | +**76800K->8433K(89600K)**:GC前该内存区域已使用容量->GC后盖区域容量(该区域总容量) |
| 194 | + |
| 195 | +- 如果是新生代,总容量则会显示整个新生代内存的9/10,即eden+from/to区 |
| 196 | +- 如果是老年代,总容量则是全身内存大小,无变化 |
| 197 | + |
| 198 | +#### 76800K->8449K(294400K) |
| 199 | + |
| 200 | +虽然本次是Minor GC,只会进行新生代的垃圾收集,但是也肯定会打印堆中总容量相关信息 |
| 201 | + |
| 202 | +在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量->GC后堆内存容量(堆内存总容量),并且堆内存总容量 = 9/10 新生代 + 老年代,然后堆内存总容量肯定小于初始化的内存大小 |
| 203 | + |
| 204 | +#### ,0.0088371 |
| 205 | + |
| 206 | +整个GC所花费的时间,单位是秒 |
| 207 | + |
| 208 | +#### [Times:user=0.02 sys=0.01,real=0.01 secs] |
| 209 | + |
| 210 | +- **user**:指CPU工作在用户态所花费的时间 |
| 211 | +- **sys**:指CPU工作在内核态所花费的时间 |
| 212 | +- **real**:指在此次事件中所花费的总时间 |
| 213 | + |
| 214 | +## Full GC 日志解析 |
| 215 | + |
| 216 | +### 日志格式 |
| 217 | + |
| 218 | +```Java |
| 219 | +2021-09-06T08:44:49.453+0800: 4.396: [Full GC (Metadata GC Threshold) [PSYoungGen: 10082K->0K(89600K)] [ParOldGen: 32K->9638K(204800K)] 10114K->9638K(294400K), [Metaspace: 20158K->20156K(1067008K)], 0.0149928 secs] [Times: user=0.06 sys=0.02, real=0.02 secs] |
| 220 | +``` |
| 221 | + |
| 222 | +### 日志解析 |
| 223 | + |
| 224 | +#### 2020-11-20T17:19:43.794-0800 |
| 225 | + |
| 226 | +日志打印时间 日期格式 如 2013-05-04T21:53:59.234+0800 |
| 227 | + |
| 228 | +添加-XX:+PrintGCDateStamps参数 |
| 229 | + |
| 230 | +#### 1.351 |
| 231 | + |
| 232 | +gc 发生时,Java 虚拟机启动以来经过的秒数 |
| 233 | + |
| 234 | +添加-XX:+PrintGCTimeStamps该参数 |
| 235 | + |
| 236 | +#### Full GC(Metadata GCThreshold) |
| 237 | + |
| 238 | +括号中是gc发生的原因,原因:Metaspace区不够用了。 |
| 239 | +除此之外,还有另外两种情况会引起Full GC,如下: |
| 240 | + |
| 241 | +1. Full GC(FErgonomics) |
| 242 | + 原因:JVM自适应调整导致的GC |
| 243 | +2. Full GC(System) |
| 244 | + 原因:调用了System.gc()方法 |
| 245 | + |
| 246 | +#### [PSYoungGen: 100082K->0K(89600K)] |
| 247 | + |
| 248 | +**PSYoungGen**:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的 |
| 249 | + |
| 250 | +- **Serial收集器**:Default New Generation 显示DefNew |
| 251 | +- **ParNew收集器**:ParNew |
| 252 | +- **Parallel Scanvenge收集器**:PSYoungGen |
| 253 | +- 老年代和新生代同理,也是和收集器名称相关 |
| 254 | + |
| 255 | +**10082K->0K(89600K)**:GC前该内存区域已使用容量->GC该区域容量(该区域总容量) |
| 256 | + |
| 257 | +- 如果是新生代,总容量会显示整个新生代内存的9/10,即eden+from/to区 |
| 258 | + |
| 259 | +- 如果是老年代,总容量则是全部内存大小,无变化 |
| 260 | + |
| 261 | +#### ParOldGen:32K->9638K(204800K) |
| 262 | + |
| 263 | +老年代区域没有发生GC,因此本次GC是metaspace引起的 |
| 264 | + |
| 265 | +#### 10114K->9638K(294400K), |
| 266 | + |
| 267 | +在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量->GC后堆内存容量(堆内存总容量),并且堆内存总容量 = 9/10 新生代 + 老年代,然后堆内存总容量肯定小于初始化的内存大小 |
| 268 | + |
| 269 | +#### [Meatspace:20158K->20156K(1067008K)], |
| 270 | + |
| 271 | +metaspace GC 回收2K空间 |
| 272 | + |
| 273 | + |
| 274 | + |
| 275 | +## 论证FullGC是否会回收元空间/永久代垃圾 |
| 276 | + |
| 277 | +```Java |
| 278 | +/** |
| 279 | + * jdk6/7中: |
| 280 | + * -XX:PermSize=10m -XX:MaxPermSize=10m |
| 281 | + * <p> |
| 282 | + * jdk8中: |
| 283 | + * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m |
| 284 | + * |
| 285 | + * @author IceBlue |
| 286 | + * @create 2020 22:24 |
| 287 | + */ |
| 288 | +public class OOMTest extends ClassLoader { |
| 289 | + public static void main(String[] args) { |
| 290 | + int j = 0; |
| 291 | + try { |
| 292 | + for (int i = 0; i < 100000; i++) { |
| 293 | + OOMTest test = new OOMTest(); |
| 294 | + //创建ClassWriter对象,用于生成类的二进制字节码 |
| 295 | + ClassWriter classWriter = new ClassWriter(0); |
| 296 | + //指明版本号,修饰符,类名,包名,父类,接口 |
| 297 | + classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); |
| 298 | + //返回byte[] |
| 299 | + byte[] code = classWriter.toByteArray(); |
| 300 | + //类的加载 |
| 301 | + test.defineClass("Class" + i, code, 0, code.length);//Class对象 |
| 302 | + test = null; |
| 303 | + j++; |
| 304 | + } |
| 305 | + } finally { |
| 306 | + System.out.println(j); |
| 307 | + } |
| 308 | + } |
| 309 | +} |
| 310 | +``` |
| 311 | + |
| 312 | +输出结果: |
| 313 | + |
| 314 | +``` |
| 315 | +[GC (Metadata GC Threshold) [PSYoungGen: 10485K->1544K(152576K)] 10485K->1552K(500736K), 0.0011517 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
| 316 | +[Full GC (Metadata GC Threshold) [PSYoungGen: 1544K->0K(152576K)] [ParOldGen: 8K->658K(236544K)] 1552K->658K(389120K), [Metaspace: 3923K->3320K(1056768K)], 0.0051012 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
| 317 | +[GC (Metadata GC Threshold) [PSYoungGen: 5243K->832K(152576K)] 5902K->1490K(389120K), 0.0009536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
| 318 | +
|
| 319 | +-------省略N行------- |
| 320 | +
|
| 321 | +[Full GC (Last ditch collection) [PSYoungGen: 0K->0K(2427904K)] [ParOldGen: 824K->824K(5568000K)] 824K->824K(7995904K), [Metaspace: 3655K->3655K(1056768K)], 0.0041177 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
| 322 | +Heap |
| 323 | + PSYoungGen total 2427904K, used 0K [0x0000000755f80000, 0x00000007ef080000, 0x00000007ffe00000) |
| 324 | + eden space 2426880K, 0% used [0x0000000755f80000,0x0000000755f80000,0x00000007ea180000) |
| 325 | + from space 1024K, 0% used [0x00000007ea180000,0x00000007ea180000,0x00000007ea280000) |
| 326 | + to space 1536K, 0% used [0x00000007eef00000,0x00000007eef00000,0x00000007ef080000) |
| 327 | + ParOldGen total 5568000K, used 824K [0x0000000602200000, 0x0000000755f80000, 0x0000000755f80000) |
| 328 | + object space 5568000K, 0% used [0x0000000602200000,0x00000006022ce328,0x0000000755f80000) |
| 329 | + Metaspace used 3655K, capacity 4508K, committed 9728K, reserved 1056768K |
| 330 | + class space used 394K, capacity 396K, committed 2048K, reserved 1048576K |
| 331 | +
|
| 332 | +进程已结束,退出代码0 |
| 333 | +
|
| 334 | +``` |
| 335 | + |
| 336 | +通过不断地动态生成类对象,输出GC日志 |
| 337 | + |
| 338 | +根据GC日志我们可以看出当元空间容量耗尽时,会触发FullGC,而每次FullGC之前,至会进行一次MinorGC,而MinorGC只会回收新生代空间; |
| 339 | + |
| 340 | +只有在FullGC时,才会对新生代,老年代,永久代/元空间全部进行垃圾收集 |
0 commit comments