JVM源码分析系列

JVM源码分析系列

  1. 不保证顺序的Class.getMethods
    JVM源码分析之不保证顺序的Class.getMethods

  2. Metaspace
    JVM源码分析之Metaspace解密

metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm

  • 为什么会有metaspace
    如果perm设置太小了,系统运行过程中就容易出现内存溢出,设置大了又总感觉浪费,尽管不会实质分配这么大的物理内存。基于这么一个可能的原因,于是metaspace出现了,希望内存的管理不再受到限制,也不要怎么关注元数据这块的OOM问题
  • metaspace的组成
    • Klass Metaspace Klass Metaspace就是用来存klass的,klass是我们熟知的class文件在jvm里的运行时数据结构,不过有点要提的是我们看到的类似A.class其实是存在heap里的,是java.lang.Class的一个对象实例。
    • NoKlass Metaspace NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。

Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以类加载器们要分配内存,但是每个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块。

  1. JVM源码分析之String.intern()导致的YGC不断变长
    JVM源码分析之String.intern()导致的YGC不断变长

在JVM里存在一个叫做StringTable的数据结构,这个数据结构是一个Hashtable,在我们调用String.intern的时候其实就是先去这个StringTable里查找是否存在一个同名的项,如果存在就直接返回对应的对象,否则就往这个table里插入一项,指向这个String对象,那么再下次通过intern再来访问同名的String对象的时候,就会返回上次插入的这一项指向的String对象

YGC的时间长短和扫描StringTable有关,如果StringTable非常庞大,那YGC过程扫描的时间也会变长

YGC过程不会对StringTable做清理,这也就是我们demo里的情况会让Stringtable越来越大,因为到目前为止还只看到YGC过程,但是在Full GC或者CMS GC过程会对StringTable做清理

  1. 如何定位消耗CPU最多的线程

步骤

- 使用top -Hp <pid> 查看进程所有线程的CPU消耗情况

- 使用jstack <pid> 查看各个线程栈

5.JVM源码分析之Object.wait/notify(All)完全解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
new Thread(){
public void run(){
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
public void run(){
synchronized(lock) {
try {
lock.notify();
}
}
}
}.start();
  • 为何要加synchronized锁
    从实现上来说,这个锁至关重要,正因为这把锁,才能让整个wait/notify玩转起来
  • wait方法执行后未退出同步块,其他线程如何进入同步块
    因为在wait处理过程中会临时释放同步锁,不过需要注意的是当某个线程调用notify唤起了这个线程的时候,在wait方法退出之前会重新获取这把锁,只有获取了这把锁才会继续执行
  • 为什么wait方法可能抛出InterruptedException异常
    这个异常大家应该都知道,当我们调用了某个线程的interrupt方法时,对应的线程会抛出这个异常,wait方法也不希望破坏这种规则,因此就算当前线程因为wait一直在阻塞,当某个线程希望它起来继续执行的时候,它还是得从阻塞态恢复过来,因此wait方法被唤醒起来的时候会去检测这个状态,当有线程interrupt了它的时候,它就会抛出这个异常从阻塞状态恢复过来。
  • 被notify(All)的线程有规律吗
    这里要分情况: 如果是通过notify来唤起的线程,那先进入wait的线程会先被唤起来 如果是通过nootifyAll唤起的线程,默认情况是最后进入的会先被唤起来,即LIFO的策略
  • notify执行之后立马唤醒线程吗
    hotspot里真正的实现是退出同步块的时候才会去真正唤醒对应的线程,
  • notifyAll是怎么实现全唤起的
    以notifyAll的实现是调用notify的线程在退出其同步块的时候唤醒起最后一个进入wait状态的线程,然后这个线程退出同步块的时候继续唤醒其倒数第二个进入wait状态的线程,依次类推
  • wait的线程是否会影响load
    当线程进入到wait状态的时候其实是会放弃cpu的,也就是说这类线程是不会占用cpu资源
  1. JVM源码分析之FinalReference完全解读

override finalizer的类对象称为f对象。

Finalizer的客观评价

  • Finalizer其实是实现了析构函数的概念,我们在对象被回收前可以执行一些『收拾性』的逻辑,应该说是一个特殊场景的补充,但是这种概念的实现给我们的f对象生命周期以及gc等带来了一些影响:
  • f对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用了,还是无法立即被回收
    f对象至少经历两次GC才能被回收,因为只有在FinalizerThread执行完了f对象的finalize方法的情况下才有可能被下次gc回收,而有可能期间已经经历过多次gc了,但是一直还没执行f对象的finalize方法
  • cpu资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行f对象的finalize方法
  • 因为f对象的finalize方法迟迟没有执行,有可能会导致大部分f对象进入到old分代,此时容易引发old分代的gc,甚至fullgc,gc暂停时间明显变长
  • f对象的finalize方法被调用了,但是这个对象其实还并没有被回收,虽然可能在不久的将来会被回收
  1. 不可逆的类初始化过程
    定义一个类的时候,可能有静态变量,可能有静态代码块,这些逻辑编译之后会封装到一个叫做clinit的方法里。
    clinit方法在我们第一次主动使用这个类的时候会触发执行,比如我们访问这个类的静态方法或者静态字段就会触发执行clinit,但是这个过程是不可逆的,也就是说当我们执行一遍之后再也不会执行了,如果在执行这个方法过程中出现了异常没有被捕获,那这个类将永远不可用。即使抛出异常,被catch住,也依然会被JVM标记为error标记。
    如果clinit执行失败了,抛了一个未被捕获的异常,那将这个类的状态设置为initialization_error,并且无法再恢复,因为jvm会认为你这次初始化失败了,下次肯定也是失败的,为了防止不断抛这种异常,所以做了一个缓存处理,不是每次都再去执行clinit,因此大家要特别注意,类的初始化过程可千万不能出错,出错就可能只能重启了哦。

8.JVM源码分析之jstat工具原理完全解读

  • jstat如何获取到这些变量的值

变量值显然是从目标进程里获取来的,但是是怎样来的?local socket还是memory share?其实是从一个共享文件里来的,这个文件叫PerfData,主要指的是/tmp/hsperfdata_/这个文件

  • PerfData文件
    • 文件创建
      这个文件是否存在取决于两个参数,一个UsePerfData,另一个是PerfDisableSharedMem,如果设置了-XX:+PerfDisableSharedMem或者-XX:-UsePerfData,那这个文件是不会存在的,默认情况下PerfDisableSharedMem是关闭的,UsePerfData是打开的,所以默认情况下PerfData文件是存在的。
    • 文件删除
      那这个文件什么时候删除?正常情况下当进程退出的时候会自动删除,但是某些极端情况下,比如kill -9,这种信号jvm是不能捕获的,所以导致进程直接退出了,而没有做一些收尾性的工作,这个时候你会发现进程虽然没了,但是这个文件其实还是存在的。在当前用户接下来的任何一个java进程(比如说我们执行jps)起来的时候会去做一个判断,遍历/tmp/hsperfdata_下的进程文件,挨个看进程是不是还存在,如果不存在了就直接删除该文件,判断是否存在的具体操作其实就是发一个kill -0的信号看是否有异常。
    • 文件更新
      由于这个文件是通过mmap的方式映射到了内存里,而jstat是直接通过DirectByteBuffer的方式从PerfData里读取的,所以只要内存里的值变了,那我们从jstat看到的值就会发生变化,内存里的值什么时候变,取决于-XX:PerfDataSamplingInterval这个参数,默认是50ms,也就是说50ms更新一次值,基本上可以认为是实时的了。