Day 8:JVM

目标:

1
2
3
4
5
6
JVM
├── JVM 内存结构(★★★★★)
├── 对象创建过程(★★★★★)
├── 垃圾回收为什么需要 GC Root(★★★★★)
├── 常见垃圾回收器(CMS/G1)
└── 类加载机制(先了解)

GC Root有哪些?

  1. 虚拟机栈中局部变量引用的对象。
  2. 方法区(Metaspace)中静态变量引用的对象。
  3. 方法区中常量引用的对象。
  4. JNI(本地方法)引用的对象。

GC Root 一定在栈上吗?

不一定。
GC Root 的本质是 JVM 认为一定存活、作为可达性分析起点的对象引用。栈中的局部变量只是最常见的一类,静态变量、JNI 引用、活动线程、Class 对象、Monitor 等都可以成为 GC Root。

1. 为什么Java对象放Heap。引用放Stack?

对象放在堆中主要有两个原因。
第一,对象通常比较大,而栈空间比较小,栈主要用于方法调用,如果整个对象都放栈中,每次方法调用和返回都会产生大量数据复制,效率很低。
第二,对象的生命周期通常长于方法,例如对象可以作为返回值返回给调用者,如果放在栈中,方法结束对象就没了,所以对象必须放在堆中,而栈中只保存对象引用。

2. 为什么Java不用引用计数。而采用GC Root?

引用计数最大的缺陷是无法处理循环引用,例如 A 引用 B,B 引用 A,即使整个对象图已经不可达,引用计数仍然大于 0。而 GC Root 可达性分析是从根对象开始遍历,只要从任何 GC Root 都无法到达,就会被回收,因此不会受到循环引用影响。

3. 下面代码方法结束。什么时候User真正释放?立即?还是GC?为什么?

1
2
3
public void test(){
User user = new User();
}

方法结束后,局部变量 user 随栈帧销毁,不再是 GC Root。如果堆中的 User 对象已经不可达,那么它就成为垃圾对象。但垃圾对象不会立即释放,而是在下一次垃圾回收时被 GC 回收,具体时间由 JVM 决定。

4. 下面代码是不是立即回收?为什么?

1
2
User user = new User();
user = null;

user = null 只是断开了一个引用,并不会立即触发 GC。对象只是变成了”可能被回收”。真正什么时候回收,由 JVM 根据垃圾回收策略决定。

5. 你的GIS平台。分析一个3GBShape。分析结束为什么Java不会立刻释放3GB内存?为什么有时候你明明已经task = null;JVM内存还是3GB?

方法结束user引用随着栈帧一起被销毁,但是对象还在heap里,如果它真的不可达的话,会在下一次gc回收。但即使gc回收了,内存也不一定立刻把内存还给操作系统

6. 综合题

下面代码

1
2
3
4
5
6
7
8
public void test() {
User user = new User();
user = null;

System.gc();

System.out.println("结束");
}

请问:

1. System.gc() 会不会一定 GC?

System.gc() 只是向 JVM 发出一次”建议进行垃圾回收”(Suggestion),JVM 可以接受,也可以忽略,它并不能保证一定立即执行 GC。

这里要记住一句经典的话:
System.gc() ≠ 立即 GC。

2. User 对象会不会一定被回收?

如果 User 对象不可达,它就成为了垃圾对象(Eligible for GC),但什么时候真正回收,仍然由 JVM 决定。

3. System.gc() 是不是一定会 Full GC?

System.gc() 只是向 JVM 发出一次”建议进行垃圾回收”(Suggestion),JVM 可以接受,也可以忽略,它并不能保证一定立即执行 GC。

Day9:JVM

目标:

1
2
3
4
1. 对象创建过程
2. Minor GC / Major GC / Full GC
3. G1 垃圾回收器
4. JVM 常见参数

对象创建过程

第一步:检查类是否已经加载
第二步:在 Heap 中申请内存
第三步:对象初始化
第四步:执行构造函数
第五步:栈中保存引用

类加载过程

加载(把.class读进jvm)
链接(准备、准备内存、静态变量赋默认值)
初始化(执行静态代码块,静态变量初始化)

JDK9之后heap改变了

G1 不再固定划分新生代和老年代,而是将堆划分为多个 Region,每次优先回收回收收益最高(Garbage First)的 Region,从而更好地控制停顿时间,因此成为 JDK9 以后的默认垃圾回收器。

GC 算法

  1. 标记清除(Mark-Sweep)
    产生内存碎片
  2. 复制算法(Copying)
    只能针对young区,存活对象少的合适。因为复制成本低。把存活对象存到survive区,eden区直接清空。
  3. 标记整理(Mark-Compact)
    old区使用的,会把存活对象进行整理,减少内存碎片。但是效率低。

1. 为什么对象要先赋默认值,再执行构造函数?

JVM 会先把对象初始化为默认状态,保证所有成员变量都有合法初始值,然后再执行构造函数完成业务初始化。

2. 为什么下面代码不会报错?为什么输出的是0?

1
2
3
4
5
6
7
class User{
int age;
}

User user = new User();

System.out.println(user.age);

不会报错,因为创建对象的时候会先对成员变量进行赋默认值再调用构造器。
局部变量不会赋默认值。

3. 为什么 JVM 要把 Heap 分成 Young 和 Old?

JVM 发现绝大多数对象生命周期都很短,因此采用分代思想,让不同生命周期的对象采用不同的回收策略。

4. 为什么 Minor GC 比 Full GC 快很多?

Young 区的大部分对象都是垃圾对象,存活率低,所以复制成本很低。
新生代(Young)最经典的是复制算法(Copying)。不是把垃圾对象删除,而是把活对象复制到另一块 Survivor 区。然后清空整个 Eden。

Full GC使用的标记整理(Mark-Compact)法。

5. 为什么 G1 比 CMS 更适合大内存服务器?

因为G1把heap分成了很多个区域,每次发生gc的时候会挑选垃圾对象较多的区域进行回收。可以更好的控制停顿时间。如果每次gc的时候都清理一整个大heap,停顿时间会很长。

6. 为什么很多线上服务建议 “-Xms = -Xmx”

因为jvm申请内存需要stop the world。

7. 为什么 Java 局部变量没有默认值,而成员变量有?

java创建对象的时候会先赋默认值,再调用构造器。
局部变量需要手动赋默认值,否则容易掩盖开发bug。

8. 为什么 Java 要把heap分代,而不是整个 Heap 全部统一GC?

为了更好的控制gc停顿。jvm发现大部分对象都是存活时间很短的对象,所以对不同生命周期的对象采用不同的回收策略。Minor GC复制成本低,所以快。

JVM 发现大部分对象都是朝生夕死的,因此采用分代思想。新生代对象存活率低,采用复制算法,只复制少量存活对象,回收效率很高;老年代对象存活率高,采用标记整理等算法,避免大量对象复制。因此分代不仅减少了 GC 停顿时间,也让不同生命周期的对象使用最适合的垃圾回收算法。

Day10:JVM

目标:

1
2
3
4
1. GC 算法(★★★★★)
2. G1 回收器(★★★★★)
3. 类加载机制(★★★★★)
4. JVM 参数(★★★★★)

双亲委派

保证了 Java 核心类不会被用户自定义类替换,保证了类加载的安全性和唯一性。

JVM 参数

1
2
3
4
5
6
7
8
9
10
11
-Xms
初始Heap
---------
-Xmx
最大Heap
---------
-Xss
每个线程栈大小
---------
-XX:+UseG1GC
使用G1

为什么标记清除容易OOM?

不是Heap不够。而是连续空间不足。

1.为什么 “static int a = 100;” 不是直接100?为什么先0再100?

因为jvm加载class的时候会先对static变量进行赋默认值,然后再执行静态代码块和初始化静态变量。

2. 为什么标记清除会产生内存碎片?

标记然后删除会产生空洞,这些空洞就是内存碎片。他们不是连续的内存空间,容易发生oom。
容易因为无法找到足够大的连续空间而导致内存分配失败。

3. 为什么Young用复制算法Old用标记整理?

因为young区的对存活率低,复制的对象少,复制成本低,所以使用复制算法。
old区的对象存活率高,使用复制的话每次都要复制很大的数据,效率低。而标记清除法容易产生内存碎片。标记整理也是删除,但是会移动整理,减少内存碎片。
JVM 根据对象不同的存活率,选择不同的垃圾回收算法。

4. 为什么G1比CMS停顿更容易控制?

因为G1把heap分成了很多个区,每次GC的时候会挑选垃圾比例高(Garbage First)的 Region进行回收。停顿时间更可控。

5. 为什么Java需要双亲委派?

防止开发者自己写了类似java.lang.String的类,导致整个Java错乱。
保证一个类在 JVM 中只有一个 Class 对象。

6. 为什么双亲委派可以防止重复加载?

因为ClassLoader加载类的时候会先询问自己上级有没有这个类,并且逐级向上,如果都没有才加载自己的。
已经加载过的类,子加载器不会重复加载,而是复用父加载器已经加载的 Class 对象。

7. String s = “abc”;和 String s = new String(“abc”);有什么区别?

String s = “abc” 会先检查字符串常量池,如果 “abc” 已经存在,则直接让引用指向常量池中的对象,不会重复创建。

new String(“abc”) 会先保证字符串常量池中存在 “abc”,然后再在堆中创建一个新的 String 对象,因此至少会创建一个堆对象,而字符串常量池中的 “abc” 可能是新建的,也可能是复用已有的。

创建流程:

1
2
3
4
5
6
7
8
9
new String("abc")

字符串常量池有没有 "abc"

没有就创建

Heap 创建一个新的 String 对象

Heap 中的 String 对象内部引用 Pool 中的 "abc"

8. String s = new String(“abc”);创建了几个对象?

情况一:字符串常量池已经有 “abc”,就只在heap创建 String 对象。一共创建了一个对象。
情况而:字符串常量池没有 “abc”,先在字符串常量池创建“abc”,然后在heap里创建 String 对象。一共创建了两个对象。