JVM
Day 8:JVM
目标:
1 | JVM |
GC Root有哪些?
- 虚拟机栈中局部变量引用的对象。
- 方法区(Metaspace)中静态变量引用的对象。
- 方法区中常量引用的对象。
- 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 | public void test(){ |
方法结束后,局部变量 user 随栈帧销毁,不再是 GC Root。如果堆中的 User 对象已经不可达,那么它就成为垃圾对象。但垃圾对象不会立即释放,而是在下一次垃圾回收时被 GC 回收,具体时间由 JVM 决定。
4. 下面代码是不是立即回收?为什么?
1 | User user = new User(); |
user = null 只是断开了一个引用,并不会立即触发 GC。对象只是变成了”可能被回收”。真正什么时候回收,由 JVM 根据垃圾回收策略决定。
5. 你的GIS平台。分析一个3GBShape。分析结束为什么Java不会立刻释放3GB内存?为什么有时候你明明已经task = null;JVM内存还是3GB?
方法结束user引用随着栈帧一起被销毁,但是对象还在heap里,如果它真的不可达的话,会在下一次gc回收。但即使gc回收了,内存也不一定立刻把内存还给操作系统
6. 综合题
下面代码
1 | public void test() { |
请问:
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 | 1. 对象创建过程 |
对象创建过程
第一步:检查类是否已经加载
第二步:在 Heap 中申请内存
第三步:对象初始化
第四步:执行构造函数
第五步:栈中保存引用
类加载过程
加载(把.class读进jvm)
链接(准备、准备内存、静态变量赋默认值)
初始化(执行静态代码块,静态变量初始化)
JDK9之后heap改变了
G1 不再固定划分新生代和老年代,而是将堆划分为多个 Region,每次优先回收回收收益最高(Garbage First)的 Region,从而更好地控制停顿时间,因此成为 JDK9 以后的默认垃圾回收器。
GC 算法
- 标记清除(Mark-Sweep)
产生内存碎片- 复制算法(Copying)
只能针对young区,存活对象少的合适。因为复制成本低。把存活对象存到survive区,eden区直接清空。- 标记整理(Mark-Compact)
old区使用的,会把存活对象进行整理,减少内存碎片。但是效率低。
1. 为什么对象要先赋默认值,再执行构造函数?
JVM 会先把对象初始化为默认状态,保证所有成员变量都有合法初始值,然后再执行构造函数完成业务初始化。
2. 为什么下面代码不会报错?为什么输出的是0?
1 | class User{ |
不会报错,因为创建对象的时候会先对成员变量进行赋默认值再调用构造器。
局部变量不会赋默认值。
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 | 1. GC 算法(★★★★★) |
双亲委派
保证了 Java 核心类不会被用户自定义类替换,保证了类加载的安全性和唯一性。
JVM 参数
1 | -Xms |
为什么标记清除容易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 | new String("abc") |
8. String s = new String(“abc”);创建了几个对象?
情况一:字符串常量池已经有 “abc”,就只在heap创建 String 对象。一共创建了一个对象。
情况而:字符串常量池没有 “abc”,先在字符串常量池创建“abc”,然后在heap里创建 String 对象。一共创建了两个对象。










