笔记
-
JVM启动流程
启动过程如下图所示:
注释:
- jvm.cfg的用途:
Controls the JVMs which may be picked with startup flags when invoking java or javac
。
当前系统的默认配置是:
```bash # List of JVMs that can be used as an option to java, javac, etc. # Order is important -- first in this list is the default JVM. # NOTE that this both this file and its format are UNSUPPORTED and # WILL GO AWAY in a future release. # # You may also select a JVM in an arbitrary location with the # "-XXaltjvm=<jvm_dir>" option, but that too is unsupported # and may not be available in a future release. # -server KNOWN -client IGNORE ```
- JVM.dll
在linux上是libjvm.so
,路径:/usr/lib/jvm/java-8-oracle/jre/lib/amd64/server/libjvm.so
。并且没有client目录,表面在linux都是server
模式。
- jvm.cfg的用途:
-
JVM基本结构
总的结构示意图如下所示:
说明:- PC寄存器
每个线程拥有一个,指向下一条指令的地址,执行native方法的时值为undefined
。
这表明线程是执行指令和CPU调度的基本单位。再复杂的系统,也是从main线程扩展起来的。 - 方法区
JDK7放在Perm区,JDK8放在MetaSpace区。
主要用于存放加载的类信息,包括:
1. 类型的常量值(JDK8已经移到Heap上)。
2. 类的属性(字段)和方法信息。
3. 方法字节码。 - Java堆
- 应用新建的对像、数组以及常量等都存放在Java堆上。
- Java堆可以被所有线程访问,是最重要的内存共享区域。
- 在分代GC算法下,Java堆是分代的。典型的分代如下图:
- Java栈
- 线程私有。
- 存放方法调用相关的数据,包括参数、局部变量以及返回值。对象、数组这些分配在堆上,栈里只保留引用。
- 栈上也可以分配对象。
前提条件:对象很小(几十byte);开启逃逸分析(-XX:+DoEscapeAnalysis)
优点:对象在不逃逸的情况下,直接分配在栈上,方法调用结束对象即回收,不增加堆的负担。
缺点:要求对象很小,适用场景有限,用处不大,所以这项技术也没有流行起来。
验证:跑了资料中的代码,在启用逃逸分析后,只发生了很少几次GC。如果禁用逃逸分析,会发生大量的GC。
开启逃逸分析:public class OnStackTest { public static void alloc(int n) { byte[] b = new byte[n]; b[0] = 1; } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 500000000; i++) { alloc(2); } } }
禁用逃逸分析:dalton@fish ~$ java -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC OnStackTest [GC (Allocation Failure) 2048K->376K(9728K), 0.0012055 secs] [GC (Allocation Failure) 2424K->360K(9728K), 0.0018963 secs]
默认会开启逃逸分析的哦dalton@fish ~$ java -Xmx5m -Xms5m -XX:-DoEscapeAnalysis -XX:+PrintGC OnStackTest [GC (Allocation Failure) 1024K->408K(5632K), 0.0091321 secs] [GC (Allocation Failure) 1432K->352K(5632K), 0.0016615 secs] [GC (Allocation Failure) 1376K->352K(5632K), 0.0011853 secs] [GC (Allocation Failure) 1376K->352K(5632K), 0.0017079 secs] [GC (Allocation Failure) 1376K->368K(5632K), 0.0017921 secs] [GC (Allocation Failure) 1392K->352K(5632K), 0.0010113 secs] [GC (Allocation Failure) 1376K->292K(5632K), 0.0010523 secs] [GC (Allocation Failure) 1316K->292K(5632K), 0.0020685 secs] [GC (Allocation Failure) 1316K->292K(5632K), 0.0019285 secs] [GC (Allocation Failure) 1316K->292K(5632K), 0.0020571 secs] [GC (Allocation Failure) 1316K->292K(5632K), 0.0030951 secs] [GC (Allocation Failure) 1316K->292K(5632K), 0.0014476 secs] [GC (Allocation Failure) 1316K->292K(5632K), 0.0007672 secs]
dalton@fish ~/a_dev/d_java/perf $ java -Xmx10m -Xms10m -XX:+PrintGC OnStackTest [GC (Allocation Failure) 2048K->392K(9728K), 0.0012030 secs] [GC (Allocation Failure) 2440K->384K(9728K), 0.0018770 secs]
- PC寄存器
-
内存模型
内存模型,即我们常说的JMM,描述了内存共享变量的读写及可见性。要点:- 每个线程都有自己的工作内存。
- 共享变量存放在主内存中,与各线程的工作内存独立。
-
线程要读取或写入共享变量,各自都要经过另个步骤。
读:read,load。read是从共享内存中读到工作内存;load是从工作内存中加载到变量。
写:store,write。store是从变量保存到工作内存;write是从工作内存写到共享内存。
这个过程的示意图如下:
- 要做到线程间可见(或同步),可以用以下几种方法:
- 用volatile关键字修饰变量。
- 用synchronized关键字修饰方法或代码块。线程在进入synchronized代码块时会从共享内存中读取变量值,离开时会写入变量值。这就保证了执行完synchronized代码块后对共享变量的修改是可见的。
- 使用常量。
-
指令重排
编译器和执行器都会基于一定的优化原则对指令进行重排。其结果是指令的实际执行顺序不一定是我们代码中看到的顺序。重排是一种重要的优化手段,但同时也增加了线程间同步的困难。因重排的条件比较复杂,我们倒是可以记住不发生重排的几种情况:- 写后读
- 读后写
- 写后写
编译器不考虑多线程间的语义。即它不会考虑多线程间的同步。
指令重排的基本原则:
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C 那么A必然先于C
- 线程的start方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()方法
编译与解释运行的概念
解释运行:读一句执行一句字节码。
编译运行:将字节码编译成机器码,然后执行。