美文网首页
第二周 JVM运行机制

第二周 JVM运行机制

作者: 空谷幽心 | 来源:发表于2018-07-15 22:52 被阅读37次

    笔记

    1. JVM启动流程
      启动过程如下图所示:


      bootup.png

      注释:

      • 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模式。
    2. JVM基本结构
      总的结构示意图如下所示:

      structure.png
      说明:
      • PC寄存器
        每个线程拥有一个,指向下一条指令的地址,执行native方法的时值为undefined
        这表明线程是执行指令和CPU调度的基本单位。再复杂的系统,也是从main线程扩展起来的。
      • 方法区
        JDK7放在Perm区,JDK8放在MetaSpace区。
        主要用于存放加载的类信息,包括:
        1. 类型的常量值(JDK8已经移到Heap上)。
        2. 类的属性(字段)和方法信息。
        3. 方法字节码。
      • Java堆
        1. 应用新建的对像、数组以及常量等都存放在Java堆上。
        2. Java堆可以被所有线程访问,是最重要的内存共享区域。
        3. 在分代GC算法下,Java堆是分代的。典型的分代如下图:
          heap_gen.png
      • Java栈
        1. 线程私有。
        2. 存放方法调用相关的数据,包括参数、局部变量以及返回值。对象、数组这些分配在堆上,栈里只保留引用。
        3. 栈上也可以分配对象。
          前提条件:对象很小(几十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]
        
    3. 内存模型
      内存模型,即我们常说的JMM,描述了内存共享变量的读写及可见性。要点:

      • 每个线程都有自己的工作内存。
      • 共享变量存放在主内存中,与各线程的工作内存独立。
      • 线程要读取或写入共享变量,各自都要经过另个步骤。
        读:read,load。read是从共享内存中读到工作内存;load是从工作内存中加载到变量。
        写:store,write。store是从变量保存到工作内存;write是从工作内存写到共享内存。
        这个过程的示意图如下:


        jmm.png
      • 要做到线程间可见(或同步),可以用以下几种方法:
        1. 用volatile关键字修饰变量。
        2. 用synchronized关键字修饰方法或代码块。线程在进入synchronized代码块时会从共享内存中读取变量值,离开时会写入变量值。这就保证了执行完synchronized代码块后对共享变量的修改是可见的。
        3. 使用常量。
    4. 指令重排
      编译器和执行器都会基于一定的优化原则对指令进行重排。其结果是指令的实际执行顺序不一定是我们代码中看到的顺序。重排是一种重要的优化手段,但同时也增加了线程间同步的困难。因重排的条件比较复杂,我们倒是可以记住不发生重排的几种情况:

      • 写后读
      • 读后写
      • 写后写

      编译器不考虑多线程间的语义。即它不会考虑多线程间的同步。

      指令重排的基本原则:

      • 程序顺序原则:一个线程内保证语义的串行性
      • volatile规则:volatile变量的写,先发生于读
      • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
      • 传递性:A先于B,B先于C 那么A必然先于C
      • 线程的start方法先于它的每一个动作
      • 线程的所有操作先于线程的终结(Thread.join())
      • 线程的中断(interrupt())先于被中断线程的代码
      • 对象的构造函数执行结束先于finalize()方法
    5. 编译与解释运行的概念
      解释运行:读一句执行一句字节码。
      编译运行:将字节码编译成机器码,然后执行。

    理解与思考

    相关文章

      网友评论

          本文标题:第二周 JVM运行机制

          本文链接:https://www.haomeiwen.com/subject/hjjmpftx.html