> 技术文档 > jvm冷门知识十讲

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为什么用元空间替换永久代

通用问题的解答步骤:

  1. 原方案的缺点;

  2. 新方案如何解决;

  3. 新方案还有什么缺陷,如何优化(进阶)

永久代的缺点

永久代位于堆内存

它和 Java 堆中的新生代(Young Generation)、老年代(Old Generation)共同构成了 Java 堆的整体内存布局。

类卸载的条件非常苛刻

类的卸载条件:

  1. 该类的所有实例都已经被垃圾回收。

  2. 加载该类的类加载器已经被垃圾回收

  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用

影响堆的垃圾回收性能

永久代的垃圾回收相对复杂,因为它不仅要管理类的元数据,还要处理常量池等信息。而且永久代的垃圾回收与堆的垃圾回收是相互关联的,可能会影响堆的垃圾回收性能。例如,在进行 Full GC 时,需要同时回收永久代和堆中的对象,增加了垃圾回收的停顿时间。

动态字节码技术的发展

动态字节码控制不当容易造成永久代内存溢出

元空间的优点

元空间使用本地内存来存储

理论上空间无限

提高gc效率

元空间 GC 独立触发,减少堆内存 STW 时间

可以参(chao)考(xi)其它jvm的实现

这是我猜的,不一定对😁

元空间的缺点

碎片化

四、什么是碎片化

一句话:切割分配,归还无序

  1. 假设初始内存为10单元,第一次分配1个单元,此时内存被分为0-1、1-10
  2. 第二次分配2个单元,此时内存为0-1、1-3、3-10
  3. 第三次分配3个单元,此时内存为0-1、1-3、3-6、6-10
  4. 后面即使内存使用完毕释放,内存也被切割为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(); }}

运行结果: