0%

内存屏障是随着SMP系统的出现而出现的,也就意味着在单核的机器上,不需要任何的内存屏障。

所以要想理解内存屏障的意义,我们需要知道CPU从单核到多核,究竟修改了什么,需要我们引入内存屏障

Read more »

Kafka的是个复杂的系统,除了基本的Producer,Consumer,Broker外,为了实现完备的功能,Kafka中有许多重要的模块,本文梳理一下这些模块的划分,与他们负责的功能。

Read more »

前言

HotSpot的初衷是将运行环境分为Client和Server,并且为他们定制了不同的JIT策略以及不同的JIT编译器(C1和C2)。

设计出ClientMode的年代,个人PC的性能还比较低,无论是CPU资源还是内存资源都比较稀少且价格较高,所以C1节约资源的快速编译是很有必要的。

随着时代的发展,个人计算机的配置在慢慢升级,同时价格也在慢慢降低,在这种环境下,ClientMode并不是那么适用了,所以HotSpot也就慢慢放弃了ClientMode,在个人计算机上默认采用Server模式。

Read more »

前言

通过前面我们知道,对于每个方法,HotSpot都维护两个计数器

  • Invocation Counter:方法被调用次数,每被调用一次都会+1
  • BackEdge Counter:专业的说法就是字节码在执行时的回跳次数。通俗点说就是,在For或者While循环中,每执行一次,都会+1。

并且我们知道对于一个方法,JIT有两种不同的编译方式

  • 完整的原方法编译,就是把原本的方法逻辑进行编译。入参和运行结果和解释运行都是一致的。
  • OSR编译,OSR后的方法入参以及运行流程和原方法有较大差异。

很自然得我们就会想到,其实计数器和编译方式之间是有对应关系的。

  • Invocation Counter -> 完整的原方法编译
  • BackEdge Counter -> OSR编译

当对应的方法计数器达到一定的次数,就会触发响应的编译

Read more »

前言

前面我们讲解了C1和C2的基本知识,但是我们还未触及一个核心的策略,就是什么时候触发即使编译,也就是when的问题。

对于when的问题,相信大家多多少少都大概知道,每个方法都会有一个调用次数的计数器,当这个计数器的次数到达一定的次数时,就会被认为是热点方法,继而触发JIT编译。

但是本文要科普另外一种触发条件,和方法计数器类似。

Read more »

前言

看到标题,你可能会疑问,为什么我会去报考银行,我不是在互联网公司上班吗?难道是想逃离互联网回家养老去了?

Read more »

前言

内联是编程语言编译器中常规的优化操作,几乎所有的语言在编译时或者在执行时都会有内联操作。

内联的本质是把几个方法合并成一个方法

从一方面讲,内联减少了函数调用的栈帧创建和销毁的时间消耗

从另一方面讲,内联为很多其他的优化方法提供了更多的可能,比如逃逸分析,无用代码消除,虚函数优化等,这也是内联被叫做优化之母(The Mother Of All Optimization)的原因。

Read more »

前言

HotSpot是一款Java虚拟机的实现,除了基本的解释功能以外,该虚拟机还拥有将字节码编译成机器码的并执行的能力,我们知道,直接执行机器码肯定比解释更快。

HotSpot最初会通过解释的方式执行程序,当它发现某个方法运行得特别频繁时,就会将这些热点(Hot Spot)代码进行编译,编译成平台相关的机器码。这个过程也叫做JIT(Just In Time),与之相对的是AOT(Ahead Of Time),比较典型的是C和C++语言。

HotSpot进行JIT编译的编译器有两个,分别叫做C1C2,或者也可以叫做Client CompilerServer Compiler。这两种编译器编译策略不同,运用在不同的场景,下面会详细的说明。

Read more »

前言

Cat是美团开源的一套监控系统,功能非常强大
一般对方法进行打点,它会自动生成每个方法的耗时,同时也会记录全链路的每个调用方法的耗时

对于查系统的性能瓶颈和稳定性有非常大的帮助

Read more »

前言

作为写了好几年Java的人,自然想去深入了解一下JVM的构造,它的具体实现。

Clone了代码,运行了起来,看了《HotSpot实战》和《揭秘Java虚拟机-JVM设计原理与实现》,但是能力一般,水平有限,对JVM还是知之甚少,发现继续研究下去将是一种苦修,要做好看很久代码都毫无进展的准备。但是本人志不在此,还是想去研究分布式与数据库,精力有限,只有暂且放弃JVM的深入研究。

如此直接放弃还是有点可惜,虽然目前学到的不成体系,但是还是想写一篇Blog记录一下。

所以此博客是上文提到的两本书的摘要+自己的一些学习理解,内容会很散,同时为了省力,摘要部分不特别标出。

Read more »

起因

开始看Lucene源代码,找了个最简单的FSLockFactory开始看。
然而还是看出了不明白的地方

在NativeFSLockFactory的close方法中有这么一段注释

1
// NOTE: we don't validate, as unlike SimpleFSLockFactory, we can't break others locks

我从网上找到了当初的Bug的讨论帖
https://issues.apache.org/jira/browse/LUCENE-6507

从里面讨论了NativeFSLock了一些关于这个文件锁的实现的问题

Read more »

前言

MVStore是h2数据库的底层的存储文件格式
在1.4版本之前底层的文件存储都是org.h2.store.*包中的
然后1.4之后默认改为了org.h2.mvstore.*包中的

官方的介绍说是根据Log Structed FS设计的,顺序写提高性能

MV的意思是multi-version

同类型的项目有个叫mapDB的项目
https://github.com/jankotek/mapdb

还有一个Apache的项目mavibot
http://directory.apache.org/mavibot/downloads.html

Read more »

前言

看任何JVM的书,oop-klass总是绕不去的坎
我一直想理解这些,但是就是理解不了,但是也说不出哪里不明白。

这篇文章不会对这个模型做系统的阐述,假设读者已经看过oop-klass模型,但是还是有点一知半解的状态。

oop-klass

为什么要这么设计?
其实很多书也提到,既然HotSpot完全基于C++去编写,要实现多态完全可以进行C++层面的转换就行。
但是C++的多态其实每一个对象都维护了一个VTable,就是虚函数表,函数表可以理解为一个函数指针的数组,这个数组在内存上和一个对象是一起的。
但是很多书中提到,为了避免每个对象都有一个VTable,JVM定义了oop-klass模型,其中oop就是保存数据结构的,而klass则担当了一部分VTable的功能,这样的话,VTable就是每个类只存在一个
也就是说,对oop而言,它对应的klass是单例的,而Java层面每New一个对象,都会在JVM生成一个oop。

所以在Java中,类和对象的关系更像是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
Student student1 = new Student("zhang", 19);
Student student2 = new Student("wang", 20);
}
}
class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}
}

动态绑定与VTable

这里再提一个我对动态绑定的理解。
其实动态绑定这个概念,大家都知道大概是个什么意思,但是再详细的说,就理不清很多细节。

现在我们都知道动态绑定是和虚表有关,在Java字节码中,需要动态绑定的指令是invokevirtual
https://cs.au.dk/~mis/dOvs/jvmspec/ref–35.html
这个ref中讲的是,动态绑定需要一个寻找函数的过程

invokevirtual retrieves the Java class for objectref, and searches the list of methods defined by that class and then its superclasses, looking for a method called methodname, whose descriptor is descriptor.

这当然我感觉是一种很扯淡的说法,要是这么来,运行时时间都花在匹配函数上去了。
而寻找函数的过程,我理解完全可以放在编译期去完成
我更认同是这种方式,就是不需要进行运行时的函数匹配,动态绑定的意思是需要在运行时改变代码段的函数指针,类似于下面文章中提到的
https://www.jianshu.com/p/fa50296b301c

编译器内部会发生转换,产生类似下面的代码:
( ( p->vptr )[0] ) (p); //( p->vptr )[0]是函数入口地址

这段代码的生成,对于C++而言是编译时,对于Java则是ClassLoader的时候,并不是运行时。

关于动态绑定,还有一些其他模棱两可的说法
比如这个文章中
https://zhuanlan.zhihu.com/p/24317613

它提到在invokevirtual的调用过程,需要去查父类的方法表

(2) 在Father类型的方法表中查找方法f1,如果找到,则将方法f1在方法表中的索引项11(如上图)记录到AutoCall类的常量池中第15个常量表中(常量池解析 )。这里有一点要注意:如果Father类型方法表中没有方法f1,那么即使Son类型中方法表有,编译的时候也通过不了。因为调用方法f1的类的对象father的声明为Father类型。

这我也是觉得是脱裤子放屁的说法,为啥要去查父类的方法表,要知道父类的方法表,是在另外一个Klass对象里,要是继承链比较长,那么需要很多次指针寻址才能找到。

其实这个和另外一个比较经典的动态绑定的解释很像:

如果子类Son中定义了 method() 的方法,则直接调用子类中的相应方法;如果子类Son中没有定义相应的方法,则到其父类中寻找method()方法。

很多人把这个过程理解为动态绑定,这个的问题也是一样的,它的假设是子类的Klass对象中没有父类的方法指针,所以需要去父类的Klass的VTable中去找。

但是通过一些文章我们可以看出,其实子类的VTable完全的Copy了一份父类的VTable。
https://stackoverflow.com/questions/18082651/how-does-dynamic-binding-happens-in-jvm
https://cloud.tencent.com/developer/article/1180981

所以至此,整个动态绑定的过程我们就已经理解,所谓动态绑定,就是比静态绑定多了一个指针寻址,去Klass中找VTable的过程。

1.7到1.8

很多书中都提到,oop-klass模型在1.8中改变较大。
原因是1.8中去掉了永久代(Perm),而改为了元空间(MetaSpace)。

我们先看看1.7中的oop-klass的继承链
https://github.com/openjdk-mirror/jdk7u-hotspot/blob/master/src/share/vm/oops/oopsHierarchy.hpp

oop继承链

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef class oopDesc*                            oop;
typedef class instanceOopDesc* instanceOop;
typedef class methodOopDesc* methodOop;
typedef class constMethodOopDesc* constMethodOop;
typedef class methodDataOopDesc* methodDataOop;
typedef class arrayOopDesc* arrayOop;
typedef class objArrayOopDesc* objArrayOop;
typedef class typeArrayOopDesc* typeArrayOop;
typedef class constantPoolOopDesc* constantPoolOop;
typedef class constantPoolCacheOopDesc* constantPoolCacheOop;
typedef class klassOopDesc* klassOop;
typedef class markOopDesc* markOop;
typedef class compiledICHolderOopDesc* compiledICHolderOop;

klass继承链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Klass;
class instanceKlass;
class instanceMirrorKlass;
class instanceRefKlass;
class methodKlass;
class constMethodKlass;
class methodDataKlass;
class klassKlass;
class instanceKlassKlass;
class arrayKlassKlass;
class objArrayKlassKlass;
class typeArrayKlassKlass;
class arrayKlass;
class objArrayKlass;
class typeArrayKlass;
class constantPoolKlass;
class constantPoolCacheKlass;
class compiledICHolderKlass;

可以说是非常多

但是到了1.8中,就变的很少
oop继承链

1
2
3
4
5
typedef class oopDesc*                            oop;
typedef class instanceOopDesc* instanceOop;
typedef class arrayOopDesc* arrayOop;
typedef class objArrayOopDesc* objArrayOop;
typedef class typeArrayOopDesc* typeArrayOop;

klass继承链

1
2
3
4
5
6
7
8
class Klass;
class InstanceKlass;
class InstanceMirrorKlass;
class InstanceClassLoaderKlass;
class InstanceRefKlass;
class ArrayKlass;
class ObjArrayKlass;
class TypeArrayKlass;

而少掉的那部分,其实只是换了个名字,叫做Metadata

1
2
3
4
5
6
7
8
9
//      class MetaspaceObj
class ConstMethod;
class ConstantPoolCache;
class MethodData;
// class Metadata
class Method;
class ConstantPool;
// class CHeapObj
class CompiledICHolder;

可能你已经猜到了,Metadata的这部分,已经全部转移到了元空间。

以下是 JDK 1.7 中的类在 JDK 1.8 中的存在形式:

  • klassOop -> Klass*
  • klassKlass 不再需要
  • methodOop -> Method*
  • methodDataOop -> MethodData*
  • constMethodOop -> ConstMethod*
  • constantPoolOop -> ConstantPool*
  • constantPoolCacheOop -> ConstantPoolCache*

Klass少掉的部分,还可以理解,但是为啥oop会少了这么多。

这里就牵扯到永久代和元空间的区别了。

首先的问题是,为什么撤销永久代而换成元空间
我找到了当初的JEP
http://openjdk.java.net/jeps/122?spm=a2c4e.11153940.blogcont20279.13.13fd33dbw7ltIv

永久代的调优非常难,永久代的大小很难确定,其中涉及到太多因素,如类的总数、常量池大小和方法数量等,而且永久代的数据可能会随着每一次Full GC而发生移动。

这里就要提到元空间的特点

Symbols were moved to the native heap
Interned strings were moved to the Java Heap
Class statics were moved to the Java Heap

  • 永久代属于堆,有大小限制。元空间使用堆外内存,理论上无内存限制
  • JDK7之前的HotSpot,字符串常量池的字符串被存储在永久代中,因此可能导致一系列的性能问题和内存溢出错误。在JDK8中,字符串常量池中只保存字符串的引用。

而如果你去看JDK1.7的oopDesc的定义,你会发现一个奇怪的事

1
2
3
4
5
6
7
8
9
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
}

这里的Klass,为什么是Oop对象?

这里引用R大的解释
https://rednaxelafx.iteye.com/blog/858009

因为HotSpot 1.7之前,包括Class在内的元数据对象都需要被GC管理,因此这四列的对象其实都是oopDesc类型,只不过第一列是描述实例的instanceOopDes, 第二三四列为klassOopDesc;这个klassOopDesc可以看作是klass的一个wrapper,仅仅为了被gc更容易滴管理和表示,它的内部有一个klass成员来表达klass的信息。
所以第二列的 klassOopDesc 内部的klass 乃Integer类的klass,第三列的klass为 klassOopDesc这个对象的klass——instanceKlassKlass,那第三列这个类的klass是什么呢?由于描述instanceKlassKlass,methodKlassKlass,xxxxKlassKlass等一大票KlassKlass需要的元数据实际上是相同的,他们就是第四列的KlassKlass,第四列的KlassKlass的klass可以用它自己来描述,于是就圆满了。

简单说就是为了偷懒,用Oop包裹一层,让GC一同管理了。
到了元方法区,已经不属于堆了,自然不需要这个了,自然可以去掉。

所以到了1.8,就变成了正常的状态

1
2
3
4
5
6
7
class oopDesc {
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
}

MarkOop

还有一些细节,我还是比较困惑的。
比如这个MarkOop的定义

1
2
class markOopDesc: public oopDesc {
}

它是继承于oopDesc的
但是在oopDesc的定义中

1
2
3
4
5
6
7
class oopDesc {
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
}

又用到了markOop

好吧,这个是一件比较奇怪的是。

上面R大提到,继承与oopDesc的都是被GC管理的,
但是这里有个奇怪的点

MarkOop存在于OopDesc中,讲道理应该是个对象才是,但是它的作用却只是作为对象头。
在Java层面没有与之对应的东西。
更没有道理要被GC管理着啊

我翻阅文档的注释
发现了这么一句话

1
2
3
4
//
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//

既然官方解释是历史原因,那就不追究这个问题了。

JIT热点探测

这个算是一个小发现
我们查看oop的体系,发现Method也有对应的oop
在method的oop中,有个变量叫MethodCounters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MethodCounters : public Metadata {
friend class VMStructs;
friend class JVMCIVMStructs;
private:
#if INCLUDE_AOT
Method* _method; // Back link to Method
#endif
#if defined(COMPILER2) || INCLUDE_JVMCI
int _interpreter_invocation_count; // Count of times invoked (reused as prev_event_count in tiered)
u2 _interpreter_throwout_count; // Count of times method was exited via exception while interpreting
#endif
#if INCLUDE_JVMTI
u2 _number_of_breakpoints; // fullspeed debugging support
#endif
InvocationCounter _invocation_counter; // Incremented before each activation of the method - used to trigger frequency-based optimizations
InvocationCounter _backedge_counter; // Incremented before each backedge taken - used to trigger frequencey-based optimizations

这个类维护了几个关于方法调用次数的计数器,和JIT的热点探测有关

具体的细节可以查看
https://www.jianshu.com/p/1ea9b3d1abb9

http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2011-June/005750.html

HSDB

这个工具,可以用来查看运行时的oop和klass数据
简单的使用,网上可以随便百度到,这里介绍下怎么查看虚表
https://cloud.tencent.com/developer/article/1180981

参考文档