jvm冷门知识十讲
一、类加载时出现死锁,jstack无法检测出来
正常代码出现死锁时,可以jstack指令排查出来
public static void main(String[] args) { new Thread(() -> { synchronized (a) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println(\"t1\"); } } }).start(); synchronized (b) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (a) { System.out.println(\"t2\"); } } }
Found one Java-level deadlock:
=============================
\"Thread-0\":
waiting to lock monitor 0x000001db7c081ca8 (object 0x0000000717189d40, a java.lang.Object),
which is held by \"main\"
\"main\":
waiting to lock monitor 0x000001db7c080808 (object 0x0000000717189d30, a java.lang.Object),
which is held by \"Thread-0\"Java stack information for the threads listed above:
===================================================
\"Thread-0\":
at com.Test1.lambda$main$0(Test1.java:17)
- waiting to lock (a java.lang.Object)
- locked (a java.lang.Object)
at com.Test1$$Lambda$1/1324119927.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)
\"main\":
at com.Test1.main(Test1.java:29)
- waiting to lock (a java.lang.Object)
- locked (a java.lang.Object)Found 1 deadlock.
但如果在类加载时出现的死锁,jstack无法检测
public class A { static { try { Thread.sleep(1000); // 尝试加载 ClassB Class.forName(\"com.B\"); System.out.println(\"ClassA 静态代码块执行\"); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InterruptedException e) { throw new RuntimeException(e); } }}public class B { static { try { Thread.sleep(1000); // 尝试加载 ClassA Class.forName(\"com.A\"); System.out.println(\"ClassB 静态代码块执行\"); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InterruptedException e) { throw new RuntimeException(e); } }}public static void main(String[] args) { new Thread(() -> { try { Class.forName(\"com.A\"); } catch (ClassNotFoundException e) { e.printStackTrace(); } }).start(); try { Class.forName(\"com.B\"); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println(\"main\"); }}
总结:不要在类初始化做有锁操作
二、类的对象保存在堆内还是元空间内
类的基本信息instanceKlass
描述 Java 类在虚拟机内部表现形式的核心类,它承载了类的元数据信息,如类名、继承关系、字段、方法等。在类加载过程中,HotSpot 虚拟机将字节码文件中的类信息解析并存储到
instanceKlass
实例里。在 Java 代码中,Object
类、自定义类等,在虚拟机内部都由instanceKlass
来表示
初始化源码(jdk 1.8,下同):
InstanceKlass* InstanceKlass::allocate_instance_klass( ClassLoaderData* loader_data, int vtable_len, int itable_len, int static_field_size, int nonstatic_oop_map_size, ReferenceType rt, AccessFlags access_flags, Symbol* name, Klass* super_klass, bool is_anonymous, TRAPS) { int size = InstanceKlass::size(vtable_len, itable_len, nonstatic_oop_map_size, access_flags.is_interface(), is_anonymous); // Allocation InstanceKlass* ik; if (rt == REF_NONE) { if (name == vmSymbols::java_lang_Class()) { // 省略其它代码 } else { // normal class ik = new (loader_data, size, THREAD) InstanceKlass( vtable_len, itable_len, static_field_size, nonstatic_oop_map_size, rt, access_flags, is_anonymous); } } else { // 省略其它代码 } // 省略其它代码 // Add all classes to our internal class loader list here, // including classes in the bootstrap (NULL) class loader. loader_data->add_class(ik); Atomic::inc(&_total_instanceKlass_count); return ik;}
类对象instanceMirrorKlass
instanceKlass
的一个特殊子类,专门用于表示 Java 中的java.lang.Class
类的实例。每一个 Java 类在虚拟机中都有一个对应的java.lang.Class
实例,这个实例由instanceMirrorKlass
来描述。instanceMirrorKlass
为 Java 代码提供了反射机制的基础支持,通过它可以获取类的各种信息、调用类的方法等
instanceOop InstanceMirrorKlass::allocate_instance(KlassHandle k, TRAPS) { // Query before forming handle. int size = instance_size(k); KlassHandle h_k(THREAD, this); instanceOop i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL); // Since mirrors can be variable sized because of the static fields, store // the size in the mirror itself. java_lang_Class::set_oop_size(i, size); return i;}
总结:类对象保存在堆内
三、Java8为什么用元空间替换永久代
通用问题的解答步骤:
原方案的缺点;
新方案如何解决;
新方案还有什么缺陷,如何优化(进阶)
永久代的缺点
永久代位于堆内存中
它和 Java 堆中的新生代(Young Generation)、老年代(Old Generation)共同构成了 Java 堆的整体内存布局。
类卸载的条件非常苛刻
类的卸载条件:
-
该类的所有实例都已经被垃圾回收。
-
加载该类的类加载器已经被垃圾回收
-
该类对应的
java.lang.Class
对象没有在任何地方被引用
影响堆的垃圾回收性能
永久代的垃圾回收相对复杂,因为它不仅要管理类的元数据,还要处理常量池等信息。而且永久代的垃圾回收与堆的垃圾回收是相互关联的,可能会影响堆的垃圾回收性能。例如,在进行 Full GC 时,需要同时回收永久代和堆中的对象,增加了垃圾回收的停顿时间。
动态字节码技术的发展
动态字节码控制不当容易造成永久代内存溢出
元空间的优点
元空间使用本地内存来存储
理论上空间无限
提高gc效率
元空间 GC 独立触发,减少堆内存 STW 时间
可以参(chao)考(xi)其它jvm的实现
这是我猜的,不一定对😁
元空间的缺点
碎片化
四、什么是碎片化
一句话:切割分配,归还无序
- 假设初始内存为10单元,第一次分配1个单元,此时内存被分为0-1、1-10
- 第二次分配2个单元,此时内存为0-1、1-3、3-10
- 第三次分配3个单元,此时内存为0-1、1-3、3-6、6-10
- 后面即使内存使用完毕释放,内存也被切割为4份不连续的地址
🤔疑问:如何解决碎片化问题?内存合并算法
五、long、double类型是否有线程不安全
long/double占用的空间:8B
Java 内存模型将 64 位的数据的读和写操作分拆为两个 32 位的操作来进行(为什么?)
总结:有,通过volatile
指定原子操作
六、数组的最大长度是多少
具体位置与指针压缩有关:
理论长度是2的32次方 - 1,但数组长度的类型是int,int 类型是 32 位有符号整数,由于数组长度不能为负数,所以理论上 Java 数组的最大长度是 2147483647(即 Integer.MAX_VALUE)
总结:理论长度为2147483647
七、指针压缩
条件
-
64位系统
-
8字节对齐
-
最大堆内存在32G以下
原理
逻辑地址:
test1:0 - 10000
test2:10000 - 110000
test3:110000 - 1100000
为何是32G以下
压缩后的 32 位指针最多可以表示 2的32次方 个不同的值,乘以 8 字节的对齐单位后,最大可以表示的内存空间为 32GB。
🤔疑问:如何突破32G,zgc是否兼容指针压缩?
八、java9 String底层数组为什么从char类型改为byte,改了会带来什么问题,如何解决
节省空间
char:2B
byte:1B
如何确定字符编码
九、ygc如何判断老年代是否引用了新生代的对象
卡表
十、gc延时计算
[Times: user=0.00 sys=0.00, real=0.00 secs]
user:用户态时间
sys:内核态时间
real:实际花费的时间
real = gc发生的时长(所有阶段加起来) + 所有线程到达安全点的时间
public class TestBlockingThread { static Thread t1 = new Thread(() -> { while (true) { long start = System.currentTimeMillis(); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } long cost = System.currentTimeMillis() - start; //按照正常情况,t1线程,大致上应是每隔1000毫秒左右,会输出一句话 我们使用 cost 来记录实际等待的时间 //如果实际时间cost大于1010毫秒 我们就使用System.err输出,也就是红色字样的输出,否则则是正常输出 (cost > 1010L ? System.err : System.out).printf(\"thread: %s, costs %d ms\\n\", Thread.currentThread().getName(), cost); } }); static Thread t2 = new Thread(() -> { while (true) { //下面是一个counted loop,单次循环末尾不会被加入安全点,整个for循环期执行结束之前,都不会进入安全点 //存在这样一种情况, 如果某次for循环才刚刚开始没多久, 因为内存过多而需要进行垃圾收集 //而我们知道,垃圾收集刚开始的时候需要先获取所有根节点,而根节点的获取依赖所有线程抵达安全点 //线程t1很简单,只需要隔1s就会进入安全点,之后,线程t1需要等到其他线程(t2)也进入到安全点 //而t2此时才刚刚是for循环的刚开始,所以需要消耗大量时间走完剩下的循环次数,这也就是为什么有时候t1实际cost时间多达5s的原因 //也就是gc发生时,要获取所有根节点,而想要获取根节点,就要所有线程抵达安全点,已经抵达的线程(t1)需要等待未抵达的线程(t2)到达安全点 然后才会继续垃圾收集的剩下内容 for (int i = 1; i { while (true) { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } byte[] bytes = new byte[_50KB]; } }); public static void main(String[] args) throws InterruptedException { t1.start(); Thread.sleep(1500L); t2.start(); t3.start(); }}
运行结果: