Java——初探JVM

JVM的概念

虚拟机是指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。

JVM是指Java虚拟机。

JVM使用软件模拟Java字节码的指令集。

JVM规范

JVM需要对Java Library提供以下支持:

  1. 反射 java.lang.reflect;
  2. ClassLoader;
  3. 初始化class和interface;
  4. 安全相关java.security;
  5. 多线程;
  6. 弱引用;

JVM的编译:

  1. 源码到JVM指令的对应格式;

  2. Javap;

java代码:

void spin() {
  int i; 
  for (i = 0; i < 100; i++) { ;
     // Loop body is empty
   }	
 }	

反汇编之后(JVM指令代码):

0   iconst_0       // Push int constant 0
1   istore_1       // Store into local variable 1 (i=0)
2   goto 8         // First time through don't increment
5   iinc 1 1       // Increment local variable 1 by 1 (i++)
8   iload_1        // Push local variable 1 (i)
9   bipush 100     // Push int constant 100
11  if_icmplt 5    // Compare and loop if less than (i < 100)
14  return         // Return void when done

JVM启动流程

启动流程如下:

  1. JAVA xxx;
  2. 装载配置,即根据当前路径和系统版本寻找jvm.cfg;
  3. 根据配置寻找JVM.dll,JVM.dll为JVM主要实现;
  4. 初始化JVM获得JNIEnv接口,JNIEnv为JVM接口,finlClass等操作通过它实现;
  5. 找到main方法并运行;

JVM基本结构

JVM基本结构如下:

  1. 类加载器子系统;
  2. 内存空间:
    1. 方法区;
    2. Java堆;
    3. Java栈;
    4. 本地方法栈;
  3. 垃圾收集器;
  4. PC寄存器;
  5. 执行引擎;
  6. 本地方法接口;
  7. 本地方法库;

PC寄存器

pc寄存器概念如下:

  1. 每一个线程拥有一个PC寄存器;
  2. 在线程创建时创建;
  3. 指向下一条指令的地址;
  4. 执行本地方法时,PC的值为undefined;

方法区

方法区概念如下:

  1. 保存装载的类信息:
    1. 类型的常量池;
    2. 字段,方法信息;
    3. 方法字节码;
  2. 通常和永久区(Perm)关联在一起;

Java堆

Java堆的概念如下:

  1. 和程序开发密切相关;
  2. 应用系统对象都保存在Java堆中;
  3. 所有线程共享Java堆;
  4. 对分代GC来说,堆也是分代的;
  5. GC的主要工作区间;

Java栈

Java栈的概念如下:

  1. 线程私有;
  2. 栈由一系列帧组成(因此Java栈也称帧栈);
  3. 帧保存一个方法的局部变量、操作数栈、常量池指针;
  4. 每一次方法调用创建一个帧并压入栈;

局部变量表

局部变量表中包含参数和局部变量。

静态方法与实例方法的第一个槽位相比,实例方法表中的第一个槽位是this,即当前对象的引用。

函数调用组成帧栈

每一次调用产生一帧,完成则弹出栈。

操作数栈

Java没有寄存器,所有参数传递使用操作数栈。

实例:

Java代码:

public static int add(int a,int b){
    int c=0;
    c=a+b;
    return c;
}

反编译代码:

 0:   iconst_0 // 0压栈
 1:   istore_2 // 弹出int,存放于局部变量2
 2:   iload_0  // 把局部变量0压栈
 3:   iload_1 // 局部变量1压栈
 4:   iadd      //弹出2个变量,求和,结果压栈
 5:   istore_2 //弹出结果,放于局部变量2
 6:   iload_2  //局部变量2压栈
 7:   ireturn   //返回

栈上分配

栈上分配的好处:

  1. 小对象(一般几十bytes),在没有逃逸(只在一个线程中使用)的情况下,可以直接分配到栈上;
  2. 直接分配在栈上,可以自动回收,减轻GC压力;
  3. 大对象或者逃逸对象无法栈上分配;

栈、堆和方法区交互

代码示例:

public   class  AppMain     
 //运行时, jvm 把appmain的信息都放入方法区 
{ 
    public   static   void  main(String[] args)  
    //main 方法本身放入方法区。 
    { 
     Sample test1 = new  Sample( " 测试1 " );  
     //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面 
     Sample test2 = new  Sample( " 测试2 " );
     test1.printName(); 
     test2.printName(); 
    } 
}
public   class  Sample       
 //运行时, jvm 把appmain的信息都放入方法区 
{ 
     private  name;     
     //new Sample实例后,name引用放入栈区里,name对象放入堆里
     public  Sample(String name){
         this .name = name;
     }
     //print方法本身放入 方法区里。
     public   void  printName(){
         System.out.println(name); 
     } 
}


运行流程:

Java——初探JVM_第1张图片

内存模型

特点:

  1. 每一个线程有一个工作内存,工作内存和主存独立;
  2. 工作内存存放主存中变量的值的拷贝;

流程图:
Java——初探JVM_第2张图片
流程描述:

  1. 当数据从主内存中复制到工作内存时,必须出现两个动作:
    1. 由主内存执行的读(read)操作;
    2. 由工作内存执行的加载(load)操作;
  2. 当数据从工作内存拷贝到主内存时,也会出现两个操作:
    1. 由工作内存执行的存储(store)操作;
    2. 由主内存执行的相应的写(write)操作;
  3. 其中每一个操作都是原子的,即执行期间不会被中断;
  4. 对于普通变量,一个线程中更新的值,不能马上反应在其它变量中;
  5. 如果需要在其它线程中立即可见,需要使用volatile关键字;

线程、本地内存和主内存的关系:

Java——初探JVM_第3张图片

volatile的作用:

  1. volatile不能代替锁,一般认为volatile比锁性能好(不绝对);
  2. 选择使用volatile的条件是,语义是否满足;
  3. 使变量变为线程间可见,即,将变量设置在主内存中;

实例:

如果将下面代码中的volatile关键字去除,则程序将无法正常结束。

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-04 13:46
 **/
public class One extends Thread{

    private volatile boolean stop = false;
    public void stopMe(){
        stop = true;
    }


    @Override
    public void run() {
        int i = 0;
        while (!stop){
            i++;
        }
        System.out.println(Thread.currentThread().getName() + " 线程 线程运行完毕...");
    }

    public static void main(String[] args) throws InterruptedException {
        One t = new One();
        t.start();
        Thread.sleep(1000);
        //此方法调用仅仅是改变了主线程中的stop,此stop在子线程中如果不加volatile关键字则是不可见的
        t.stopMe();
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() +  " 线程 执行完毕...");
    }
}

保证可见性的方法:

  1. volatile关键字;
  2. synchronized关键字(unlock之前,写变量值回主存);
  3. final(一旦初始化完成,其它线程可见);

有序性:

  1. 在本线程中,操作都是有序执行的;
  2. 在线程外观察,操作都是无序的(指令重排或主内存同步延时);

指令重排:

  1. 线程内串行语义:

    1. 写后读
    2. 写后写
    3. 读后写

    以上三种情况不可重排序,重排的原则是:单线程内语义重排前后无变化。不考虑多线程间的语义。

  2. 破坏线程间的有序性;

  3. 保证有序性的方法:

    1. synchronized关键字;
    2. Lock;

指令重排的原则:

  1. 程序顺序原则:一个线程内保证语义的串行性;
  2. volatile规则:volatile变量的写,必发生于读;
  3. 锁规则:解锁必然发生在随后的加锁前;
  4. 传递性:A先于B,B先于C,那么A必然先于C;
  5. 线程的start方法先于它的每一个动作;
  6. 线程的所有操作先于线程的终结(Thread.join());
  7. 线程的中断(interrupt())先于被中断线程的代码;
  8. 对象的构造函数执行结束先于finalize()方法;

关于finalize()方法的补充

finalize()方法在Java9之后的版本便被废弃。

被废弃的一部分原因:

  1. 是否被执行无法确定;
  2. 何时被执行无法确定;
  3. 执行后可能会导致死锁等等问题;

编译和解释运行的概念

解释运行:

  1. 解释执行以解释的方式运行字节码;
  2. 解释执行的意思是:读一句,执行一局;

编译运行(JIT):

  1. 将字节码编译成机器码;
  2. 直接执行机器码;
  3. 运行时编译;
  4. 编译后性能有数量级的提升;

在idea中使用jvm命令

Run->Edit Configurations->VM options

Trace跟踪参数

jvm日志打印开关指令:

  1. -verbose:gc;
  2. -XX:+printGC;(jdk12 报错)

打印GC详细信息:-XX:+PrintGCDetails;

在Jdk12中被替换为:-Xlog:gc*

打印GC发生的时间戳:-XX:+PrintGCTimeStamps;(jdk12 报错)

在使用-XX:+PrintGCDetails后打印的日志中:

–def new generation total 13824K, used 11223K [0x27e80000,
0x28d80000, 0x28d80000]

"[]"中的数字,第一个为低边界即起始位置,第二个为当前边界即当前所申请到位置,第三个为最高边界即可以申请到的最大位置。

打印log文件:-Xloggc:log/gc.log;

即输出到系统当前目录log文件夹中。

在每一次GC后打印堆信息:-XX:+PrintHeapAtGC;(jdk12 报错)

监控类加载:-XX:+TraceClassLoading;

打印类的使用情况:-XX:+PrintClassHistogram;

分别显示序号、实例数量、总大小和类型。

如需打印需要按下ctrl + break。

堆的分配参数

指定最大堆:-Xmx???m;

指定启动时堆大小:-Xms???m;

获取当前系统分配的堆空间:

java代码:

		//系统最大使用空间
        System.out.println("Xmx=" + Runtime.getRuntime().maxMemory()/1024.0/1024 + "M");
        //系统可用空间
        System.out.println("free mem=" + Runtime.getRuntime().freeMemory()/1024.0/1024 + "M");
        //当前系统已经分配空间
        System.out.println("total mem=" + Runtime.getRuntime().totalMemory()/1024.0/1024 + "M");

使用Java代码进行GC操作:

//进行
GCSystem.gc();

使用gc()前后的对比:

Xmx=4066.0M
free mem=253.71802520751953M
total mem=256.0M
Xmx=4066.0M
free mem=8.882171630859375M
total mem=10.0M

执行新生代大小:-Xmn ??? m;

设置绝对值。

设置新生代大小:-XX:NewRatio ???;

设置新生代大小。

新生代(eden + 2*s)和老年代(不包含永久区)的比值。

如果取值为4,则表示新生代:老年代=1:4,即年轻代占堆的1/5。

设置幸存区大小:-XX:SurvivorRatio ???;

设置两个Survivor区和eden的比值。

如果取值为8,则表示Survivor:eden = 2:8,即一个Survivor占年轻代的1/10。

测试代码:

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-05 18:13
 **/
public class ONe {
    public static void main(String[] args) {
        byte[] b = null;
        for (int i=0;i<10;i++){
            b = new byte[1*1024*1024];
        }
    }
}

运行结果:

[0.003s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
[0.010s][info   ][gc,heap] Heap region size: 1M
[0.011s][info   ][gc     ] Using G1
[0.011s][info   ][gc,heap,coops] Heap address: 0x00000000fec00000, size: 20 MB, Compressed Oops mode: 32-bit
[0.032s][info   ][gc           ] Periodic GC disabled
[0.070s][info   ][gc,start     ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[0.070s][info   ][gc,task      ] GC(0) Using 2 workers of 4 for evacuation
[0.071s][info   ][gc,phases    ] GC(0)   Pre Evacuate Collection Set: 0.0ms
[0.071s][info   ][gc,phases    ] GC(0)   Evacuate Collection Set: 1.0ms
[0.071s][info   ][gc,phases    ] GC(0)   Post Evacuate Collection Set: 0.1ms
[0.071s][info   ][gc,phases    ] GC(0)   Other: 0.1ms
[0.071s][info   ][gc,heap      ] GC(0) Eden regions: 1->0(1)
[0.071s][info   ][gc,heap      ] GC(0) Survivor regions: 0->1(1)
[0.071s][info   ][gc,heap      ] GC(0) Old regions: 0->0
[0.071s][info   ][gc,heap      ] GC(0) Archive regions: 0->0
[0.071s][info   ][gc,heap      ] GC(0) Humongous regions: 0->0
[0.071s][info   ][gc,metaspace ] GC(0) Metaspace: 363K->363K(1056768K)
[0.071s][info   ][gc           ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 1M->0M(20M) 1.274ms
[0.071s][info   ][gc,cpu       ] GC(0) User=0.00s Sys=0.00s Real=0.00s
[0.106s][info   ][gc,start     ] GC(1) Pause Young (Concurrent Start) (G1 Humongous Allocation)
[0.106s][info   ][gc,task      ] GC(1) Using 2 workers of 4 for evacuation
[0.108s][info   ][gc,phases    ] GC(1)   Pre Evacuate Collection Set: 0.0ms
[0.108s][info   ][gc,phases    ] GC(1)   Evacuate Collection Set: 1.4ms
[0.108s][info   ][gc,phases    ] GC(1)   Post Evacuate Collection Set: 0.1ms
[0.108s][info   ][gc,phases    ] GC(1)   Other: 0.1ms
[0.108s][info   ][gc,heap      ] GC(1) Eden regions: 1->0(1)
[0.108s][info   ][gc,heap      ] GC(1) Survivor regions: 1->1(1)
[0.108s][info   ][gc,heap      ] GC(1) Old regions: 0->1
[0.108s][info   ][gc,heap      ] GC(1) Archive regions: 0->0
[0.108s][info   ][gc,heap      ] GC(1) Humongous regions: 8->2
[0.108s][info   ][gc,metaspace ] GC(1) Metaspace: 630K->630K(1056768K)
[0.108s][info   ][gc           ] GC(1) Pause Young (Concurrent Start) (G1 Humongous Allocation) 9M->2M(20M) 1.674ms
[0.108s][info   ][gc,cpu       ] GC(1) User=0.00s Sys=0.00s Real=0.00s
[0.108s][info   ][gc           ] GC(2) Concurrent Cycle
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Clear Claimed Marks
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Clear Claimed Marks 0.007ms
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Scan Root Regions
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Scan Root Regions 0.588ms
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Mark (0.108s)
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Mark From Roots
[0.108s][info   ][gc,task      ] GC(2) Using 1 workers of 1 for marking
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Mark From Roots 0.065ms
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Preclean
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Preclean 0.061ms
[0.108s][info   ][gc,marking   ] GC(2) Concurrent Mark (0.108s, 0.108s) 0.151ms
[0.109s][info   ][gc,start     ] GC(2) Pause Remark
[0.109s][info   ][gc,stringtable] GC(2) Cleaned string table, strings: 2885 processed, 0 removed
[0.109s][info   ][gc            ] GC(2) Pause Remark 10M->10M(20M) 0.230ms
[0.109s][info   ][gc,cpu        ] GC(2) User=0.00s Sys=0.00s Real=0.00s
[0.109s][info   ][gc,marking    ] GC(2) Concurrent Rebuild Remembered Sets
[0.109s][info   ][gc,marking    ] GC(2) Concurrent Rebuild Remembered Sets 0.385ms
[0.109s][info   ][gc,start      ] GC(2) Pause Cleanup
[0.109s][info   ][gc            ] GC(2) Pause Cleanup 10M->10M(20M) 0.025ms
[0.109s][info   ][gc,cpu        ] GC(2) User=0.00s Sys=0.00s Real=0.00s
[0.109s][info   ][gc,marking    ] GC(2) Concurrent Cleanup for Next Mark
[0.110s][info   ][gc,marking    ] GC(2) Concurrent Cleanup for Next Mark 0.179ms
[0.110s][info   ][gc            ] GC(2) Concurrent Cycle 1.837ms
[0.111s][info   ][gc,start      ] GC(3) Pause Young (Concurrent Start) (G1 Humongous Allocation)
[0.111s][info   ][gc,task       ] GC(3) Using 2 workers of 4 for evacuation
[0.111s][info   ][gc,phases     ] GC(3)   Pre Evacuate Collection Set: 0.0ms
[0.111s][info   ][gc,phases     ] GC(3)   Evacuate Collection Set: 0.5ms
[0.111s][info   ][gc,phases     ] GC(3)   Post Evacuate Collection Set: 0.1ms
[0.111s][info   ][gc,phases     ] GC(3)   Other: 0.1ms
[0.111s][info   ][gc,heap       ] GC(3) Eden regions: 1->0(1)
[0.111s][info   ][gc,heap       ] GC(3) Survivor regions: 1->1(1)
[0.111s][info   ][gc,heap       ] GC(3) Old regions: 1->1
[0.111s][info   ][gc,heap       ] GC(3) Archive regions: 0->0
[0.111s][info   ][gc,heap       ] GC(3) Humongous regions: 12->2
[0.111s][info   ][gc,metaspace  ] GC(3) Metaspace: 705K->705K(1056768K)
[0.111s][info   ][gc            ] GC(3) Pause Young (Concurrent Start) (G1 Humongous Allocation) 12M->2M(20M) 0.714ms
[0.111s][info   ][gc,cpu        ] GC(3) User=0.00s Sys=0.00s Real=0.00s
[0.111s][info   ][gc            ] GC(4) Concurrent Cycle
[0.111s][info   ][gc,marking    ] GC(4) Concurrent Clear Claimed Marks
[0.111s][info   ][gc,marking    ] GC(4) Concurrent Clear Claimed Marks 0.009ms
[0.111s][info   ][gc,marking    ] GC(4) Concurrent Scan Root Regions
[0.112s][info   ][gc,marking    ] GC(4) Concurrent Scan Root Regions 0.298ms
[0.112s][info   ][gc,marking    ] GC(4) Concurrent Mark (0.112s)
[0.112s][info   ][gc,marking    ] GC(4) Concurrent Mark From Roots
[0.112s][info   ][gc,task       ] GC(4) Using 1 workers of 1 for marking
[0.113s][info   ][gc,marking    ] GC(4) Concurrent Mark From Roots 0.963ms
[0.113s][info   ][gc,marking    ] GC(4) Concurrent Preclean
[0.113s][info   ][gc,marking    ] GC(4) Concurrent Preclean 0.064ms
[0.113s][info   ][gc,marking    ] GC(4) Concurrent Mark (0.112s, 0.113s) 1.056ms
[0.113s][info   ][gc,start      ] GC(4) Pause Remark
[0.113s][info   ][gc,stringtable] GC(4) Cleaned string table, strings: 2919 processed, 3 removed
[0.113s][info   ][gc            ] GC(4) Pause Remark 4M->4M(20M) 0.215ms
[0.113s][info   ][gc,cpu        ] GC(4) User=0.00s Sys=0.00s Real=0.00s
[0.113s][info   ][gc,marking    ] GC(4) Concurrent Rebuild Remembered Sets
[0.113s][info   ][gc,marking    ] GC(4) Concurrent Rebuild Remembered Sets 0.452ms
[0.113s][info   ][gc,start      ] GC(4) Pause Cleanup
[0.114s][info   ][gc            ] GC(4) Pause Cleanup 4M->4M(20M) 0.030ms
[0.114s][info   ][gc,cpu        ] GC(4) User=0.00s Sys=0.00s Real=0.00s
[0.114s][info   ][gc,marking    ] GC(4) Concurrent Cleanup for Next Mark
[0.114s][info   ][gc,marking    ] GC(4) Concurrent Cleanup for Next Mark 0.159ms
[0.114s][info   ][gc            ] GC(4) Concurrent Cycle 2.484ms
[0.114s][info   ][gc,heap,exit  ] Heap
[0.114s][info   ][gc,heap,exit  ]  garbage-first heap   total 20480K, used 5064K [0x00000000fec00000, 0x0000000100000000)
[0.114s][info   ][gc,heap,exit  ]   region size 1024K, 2 young (2048K), 1 survivors (1024K)
[0.114s][info   ][gc,heap,exit  ]  Metaspace       used 786K, capacity 4531K, committed 4864K, reserved 1056768K
[0.114s][info   ][gc,heap,exit  ]   class space    used 74K, capacity 402K, committed 512K, reserved 1048576K

OOM时导出堆到文件:-XX:+HeapDumpOnOutOfMemoryError;

导出OOM的路径:-XX:+HeapDumpPath=xxxxxx;

导出的.dump文件可以用visualvm打开。

网址:https://visualvm.github.io/

在OOM时,执行一个脚本:-XX:OnOutOfMemoryError=xxxxxx;

因此使得在OOM是,发送邮件,甚至是重启。

所谓脚本可以是任何文件,使用最多的还是脚本。

永久区分配参数

设置永久区初始空间:-XX:PermSize xxx;

设置永久区最大空间:-XX:MaxPermSize xxx;

永久区溢出也可导致OOM。

栈大小的分配

分配栈大小:-Xss;

在jdk12中栈最小180K;

通常只有几百K;

决定了函数调用的深度;

每个线程都有独立的栈空间;

局部变量、参数分配在线程上;

如果想尽可能多的提高方法的调用次数,则设置的栈空间更大;

每一个线程独占的stack内存是一定的,也可修改,总的stack内存是一定的,所以说如果每个线程独占的stack内存越大,JVM创建的线程数上限就越少;

调用次数测试代码:

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-05 19:03
 **/
public class Five {

    private static int count = 0;

    public static void recursion(long a, long b, long c){
        long e=1,f=2,g=3,h=4,i=5,k=6,as=12,sd=122,er=1222,re=10;
        count++;
        recursion(a, b, c);
    }

    public static void main(String[] args) {

        try {
            recursion(0L, 0L, 0L);
        }catch (Throwable e){
            System.out.println("递归次数:" + count);
            e.printStackTrace();
        }

    }

}

当分配180K时:502次;

当分配360K时:2397次;

关于JDK12 JVM规范中关于堆和栈的描述

原文:

2.5  Run-Time Data Areas
The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.
2.5.1 The pc Register
The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6) for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. The Java Virtual Machine's pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.
2.5.2 Java Virtual Machine Stacks
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.
In the First Edition of The Java® Virtual Machine Specification, the Java Virtual Machine stack was known as the Java stack.
This specification permits Java Virtual Machine stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the Java Virtual Machine stacks are of a fixed size, the size of each Java Virtual Machine stack may be chosen independently when that stack is created.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of Java Virtual Machine stacks, as well as, in the case of dynamically expanding or contracting Java Virtual Machine stacks, control over the maximum and minimum sizes.
The following exceptional conditions are associated with Java Virtual Machine stacks:
• If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
• If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.
2.5.3 Heap
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size.
The following exceptional condition is associated with the heap:
• If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an OutOfMemoryError.
2.5.4 Method Area
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods used in class and interface initialization and in instance initialization (§2.9).
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.
The following exceptional condition is associated with the method area:
• If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.
2.5.5 Run-Time Constant Pool
A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file (§4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time. The run-time constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table.
Each run-time constant pool is allocated from the Java Virtual Machine's method area (§2.5.4). The run-time constant pool for a class or interface is constructed when the class or interface is created (§5.3) by the Java Virtual Machine.
The following exceptional condition is associated with the construction of the runtime constant pool for a class or interface:
• When creating a class or interface, if the construction of the run-time constant pool requires more memory than can be made available in the method area of the Java Virtual Machine, the Java Virtual Machine throws an OutOfMemoryError.
See §5 (Loading, Linking, and Initializing) for information about the construction of the run-time constant pool.
2.5.6 Native Method Stacks
An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine's instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created.
This specification permits native method stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the native method stacks are of a fixed size, the size of each native method stack may be chosen independently when that stack is created.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the native method stacks, as well as, in the case of varying-size native method stacks, control over the maximum and minimum method stack sizes.
The following exceptional conditions are associated with native method stacks:
• If the computation in a thread requires a larger native method stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
• If native method stacks can be dynamically expanded and native method stack expansion is attempted but insufficient memory can be made available, or if insufficient memory can be made available to create the initial native method stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

译文:

2.5 运行时区域
JVM定义了多种运行数据区域用来执行一个程序。其中有些区域是JVM启动时创建并在JVM退出时销毁。其它区域则是分配到每个线程的。每个线程的数据区域在线程被创建时创建,在线程被销毁时回收。
2.5.1 程序计数器
JVM支持同时运行多个线程。每一条JVM中的线程都有它自己的程序计数器。在任何时候,每一条JVM中的线程都在执行单一方法的代码,即该线程的现行方法(current method)。如果这个方法不是本地的,那么程序计数器就会比较JVM当前正在执行的机指令的地址。如果该方法当前是线程在本地执行的,那么JVM的程序计数器就是未定义状态。JVM的程序计数器是足够大的去容纳一个返回的地址或者是特定平台上的本机指针(native pointer)。
2.5.1 JVM栈空间
每一个线程都在JVM拥有一个私有的栈空间,在线程被创建时创建。JVM栈空间用来存储帧。JVM的栈空间类似于传统语言C语言的栈空间:栈空间被用来保存局部变量和部分结果,并且在方法的反射和返回中起到一定的作用。因为JVM的栈空间从不被直接操作除了帧的压入和弹出操作,帧也是可以通过堆来分配的。分配给栈空间的内存不需要是连续的。
	在第一版JVM规范中,JVM栈空间就是JAVA的栈空间。
规范允许JVM栈空间既可以是固定大小,也可以是通过计算来动态扩张或者收缩的。如果栈空间是固定大小,那么栈空间的大小在栈创建时被独立的规定。
	JVM的实现提供给程序员或者用户去控制栈空间的初始大小,或者在动态扩张和收缩时的最大值和最小值。
以下的异常与JVM有关:
1.如果在某个线程中的运算所需要的空间大于JVM所提供的,那么JVM就会抛出一个栈溢出错误(StockOverflowError);
2.如果JVM栈空间允许扩展,并且尝试过扩展,但是可用内存空间不足以扩展,或者如果没有可用的内存分配给线程栈空间,那么就会抛出内存溢出错误(OutOfMemoryError);
2.5.3 堆空间
JVM的堆空间是对绝大部分线程共享的。堆空间是运行时数据区域,从中为所有类实例和数组分配内存。
堆空间在JVM启动时创建。回收对象的堆空间是通过自动回收管理系统控制的(比如gc,garbage collector);对象是永远不会被显示释放空间的。JVM不采用特定类型的自动回收管理系统,自动回收管理系统可以根据实现者的系统要求选择。堆空间可是固定大小的也可能是可扩展的,如果堆空间过大而不再必要则会收缩。分配给堆的内存空间不需要是连续的。
	JVM的实现提供给程序员或者用户去控制堆空间的初始大小,或者在动态扩张和收缩时的最大值和最小值。
如果运算所需的堆空间比自动回收管理系统所提供的多,则JVM回抛出内存溢出错误(OutOfMemoryError)。
2.5.4 方法区域
JVM拥有一个对绝大多数线程共享的方法区域。方法区域类似于常规代码编译代码的存储区域,也类似于操作系统进程中的“text”段。它保存了每一个类的结构,就像运行时常量池,成员变量,方法数据,以及方法和构造函数的代码,包括在类和接口初始化以及实例化时使用的特殊方法。
方法区域在JVM启动时创建。虽然方法区域在逻辑上是堆的一部分,但是简单的实现可以选择不被垃圾收集器回收或者压缩。此规范不强制指定用于管理已编译代码的方法区域或策略的位置。方法区域可以是固定大小的,或者根据计算进行扩展,并且如果不再需要更大的空间时,也可以压缩空间。方法区域的内存不需要是连续的。
2.5.5 运行时常量池
运行时常量池是每一个类或者每一个接口在运行时常量池表的代表在一个类文件中。它包含了几种常量,从编译时一致的数字文本到必须在编译时解析的方法和字段引用。运行时常量池提供的功能类似于传统编程语言中的符号表,尽管常量池包含的数据范围要比典型的符号表要更多。
每一个运行时常量池都是从JVM的方法域中分配出来的。每一个类或者接口的常量池都是在这个接口或者这个类被创建时被JVM构建的。
当抛出以下问题时,则是一个类或者接口的常量池在构建时出了问题:
当我们在创建一个类或者接口,如果此时需要的内存空间多于JVM的方法域中可用的空间,则会由JVM抛出内存泄露(OutOfMemoryError)。
2.5.6 本地方法栈
一个JVM的实现,可能使用了传统的栈,习惯称为“C Stack”,去支持本地方法(方法使用非Java语言编写)。本地方法栈也可以被JVM指令集的解释器的实现所使用,如C。不能加载本地方法且本身不依赖于传统堆栈的JVM实现,不需要提供本地方法堆栈。如果提供,则通常在创建每个线程时为每个线程分配本地方法堆栈。
此规范允许本地方法栈有固定的大小,或者根据计算动态扩展或者收缩本地方法栈的大小。如果本地方法栈是固定大小的,在创建该堆栈时可以选择创建的大小。
以下异常条件与本机方法堆栈关联:
1.如果一个在线程中的运算所请求的方法栈大小多于JVM所提供的,则会由JVM抛出栈溢出(StackOverflowError);
2.如果可以动态分配本地方法栈的大小,并且在尝试扩展本地方法栈的大小后,若果可用内存不足,或者如果无法为新线程创建本地方法栈提供足够的内存,则会由JVM抛出内存泄露(OutOfMemoryError);

补充

Native Method :本地方法,简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。

如何捕获UncaughtExceptionHandler?

TestMain.java

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-06 20:37
 **/
public class TestMain {

    public static volatile boolean isStop = false;

    public static void main(String[] args) {

        new Timer("MemoryTimer").schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("剩余内存:" + Runtime.getRuntime().freeMemory()/1024.0/1024 + "M");
            }
        }, 0, 100);

        for (int i=0;i<999999;i++){
            if (!isStop){
                Thread t = new Thread(new MyRunnable());
                t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
                t.start();
            }else {
                System.out.println("出现UncaughtException....");
                break;
            }
        }

        System.out.println("main执行完毕...");

    }

}

MyUncaughtExceptionHandler.java

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-05 20:29
 **/
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        //此时的打印非常混乱
        //System.out.println(t.getName() + "线程 UncaughtException...");
        //TestMain.isStop = true;
    }
}

MyRunnable.java

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-06 20:32
 **/
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        byte[] bytes = new byte[1024*1024*1];
    }
}

需要重写Thread.UncaughtExceptionHandler接口。
抛出UncaughtException的地方不是启动线程的地方,而是线程本身。

GC的概念

GC(Garbage Collection)垃圾收集;

Java中,GC的对象是堆空间和永久区;

JAVA/.NET中的垃圾回收器。Java是由C++发展来的。它摈弃了C++中一些繁琐容易出错的东西。其中有一条就是这个GC。而C#又借鉴了JAVA;

查看JVM配置:

  1. 在命令行中使用:java -XX:+PrintCommandLineFlags
  2. 在JVM启动时使用:-XX:+PrintCommandLineFlags

JVM参数说明

-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:NewRatio:新生代和老年代的比
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC :新生代使用并行回收收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
-Xms:设置堆的最小空间大小。
-Xmx:设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss:设置每个线程的堆栈大小

G1回收器相关参数说明:
Java——初探JVM_第4张图片

-XX:G1ConcRefinementThreads=n:并发线程最大数目;

GC算法

引用计数法

老牌的垃圾回收算法。

通过引用计算来回收垃圾。

引用计数法是指:对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加一,当引用失效时,引用计数器减一。如果对象A的引用计数器的值为0,则对象A就不可能再被使用。

缺点:

  1. 引用和去引用伴随加法和减法,影响性能;
  2. 很难处理循环引用;

可以将一个对象想象为一个链表,链表中有计数器、数据域和指针域,计数器用来记录有多少个其它链表指向了本链表,指针域则用来指向另一个链表。如果两个链表相互引用,则会引起对象的相互引用,导致两个对象永远无法被回收。

标记-清除

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

但是这样也造成了内存碎片化的问题。

标记-压缩

标记-压缩算法适用于存活对象比较多的场合,如老年代。

它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也要首先要从根节点开始,对所有可达对象做一次标记。但之后,它并不是简单的清理未标记的对象,而是将所有存活的对象压缩到内存的一端。这样将非可达对象释放的空间集中到一起,这样也解决了内存碎片化的。

复制算法

与标记-清除算法相比,复制算法是一种相对高效的回收方法。

不适用于存活对象较多的场合。

复制算法将原有的内存空间分为两块,每次只使用其中的一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清理正在使用的内存块中所有的对象,交换两个内存的角色,完成垃圾回收。

但是,在划分内存空间的时候可能会造成空间的浪费。

分代思想

依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。

根据不同代的特点,选取合适的收集算法:

  1. 少量存活对象,适合复制算法;
  2. 大量存活对象,适合标记清理或者标记压缩算法;

可触及性

可触及的:从根节点可以触及到这个对象;

可复活的:一旦所有引用被释放,就是可复活状态;因为在finalize()中可能复活对象;(JDK9以上待考证)

不可触及的:在finalize()之后,可能会进入不可触及状态;不可触及的对象不可复活;可以被GC回收;

JDK9及以前:

One.java

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-07 20:26
 **/
public class One {

    public static One obj;

    @Override
    protected void finalize() throws Throwable {
        //finalize()在引用为null时被执行
        super.finalize();
        System.out.println("One对象执行了finalize()....");
        //如果对象即将被回收时,有了引用此次便不会被回收
        //但是finalize()只能执行一次,下一次引用为null时便被回收
        obj = this;
    }

    @Override
    public String toString() {
        return "这是One obj";
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new One();
        obj = null;
        System.out.println("第一次gc...");
        System.gc();
        Thread.sleep(1000);
        if (obj == null){
            System.out.println("obj 是 null...");
        }else {
            System.out.println("obj 可用...");
        }
        System.out.println("第二次gc...");
        obj = null;
        System.gc();
        Thread.sleep(1000);
        if (obj == null){
            System.out.println("obj 是 null...");
        }else {
            System.out.println("obj 可用...");
        }
    }
}

JDK9及之后:

Two.java

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-07 20:35
 **/
public class Two {

    public static void main(String[] args) {
        //创建字符串对象
        String str = new String("zcd");
        //创建引用队列
        //引用队列存储被gc回收之后对象的引用
        ReferenceQueue rq = new ReferenceQueue();
        //创建虚引用,指向字符串
        PhantomReference pr = new PhantomReference(str, rq);
        System.out.println(rq.poll() == pr);
        //将目标对象置为空
        str = null;
        //执行垃圾回收
        System.gc();
        //强制调用已经失去引用的对象的finalize方法
        System.runFinalization();
        //在被回收的引用中找到了之前的引用
        System.out.println(rq.poll() == pr);
    }

}

两次实验都可以看到,对象在被执行完finalize()之后便被JVM回收。

在JDK9及之前:

  1. 避免使用finalize(),操作不慎可能导致错误;
  2. 优先级较低,不知何时被调用;
  3. 如果想使用finalize()来释放所持有的资源,不如使用try-catch-finally来代替;

根对象:

  1. 栈中引用的对象;
  2. 方法区中静态成员或者常量引用的对象(全局对象);
  3. JNI方法栈中引用对象;

Stop-The-World

Java中一种全局暂停的现象;

全局暂停,指Java代码停止,native代码可以执行,但是不能和JVM交互;

大概率由GC引起:

  1. Dump线程;
  2. 死锁检查;
  3. 对Dump;

GC时为什么会有全局暂停?
当聚会时打扫房间,聚会时本来就乱,又有新的垃圾产生,房间永远打扫不干净,只有让所有人停止活动,才能将房间打扫干净;

危害:

  1. 长时间服务停止,没有响应;
  2. 遇到HA系统,可能引起主备切换,严重危害生产环境;

串行收集器

最古老的,最稳定的;

效率高;

可能会产生较长时间的停顿;

对应的JVM指令:-XX:UseSerialGC;

特点:

  1. 新生代,老年代使用串行回收;
  2. 新生代赋值算法;
  3. 老年代标记-压缩算法;

示意图:
Java——初探JVM_第5张图片

并行收集器

Serial收集器新生代的并行版本;

复制算法;

多线程,需要多核支持;

JVM指令:-XX:+UseParNewGC;

限制线程数量:-XX:ParallelGCThreads=n;

示意图:

Java——初探JVM_第6张图片

多线程但不一定快;

最大停顿时间指令:-XX:MaxGCPauseMills=n;

即GC尽力保证回收时间不超过设定值;

垃圾回收时间占总时间的比指令:-XX:GCTimeRatio=n;

0-100的取值范围,默认99,即最大允许1%的时间做GC;

CMS收集器

Concurrent Mark Sweep并发标记清除;

并发阶段会降低吞吐量;

老年代回收器,新生代使用ParNew;

JVM指令:+UseConcMarkSweepGC;

CMS的标记过程:

  1. 初始标记

    根可以直接关联到的对象;

    速度快;

  2. 并发标记(和用户线程一起)

    主要的标记过程,标记全部对象;

  3. 重新标记

    由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正;

  4. 并发清除(和用户线程一起)

    基于标记结果,直接清理对象;

流程示意图:
Java——初探JVM_第7张图片

特点:

  1. 尽可能降低停顿;

  2. 会影响系统整体吞吐量和性能;

    比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度下降一半;

  3. 清理不彻底;

    因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理;

  4. 因为和用户线程一起运行,所以不能在空间快满时再清理;

    触发GC阈值的JVM指令:-XX:CMSInitiatingOccupancyFraction=n;

    如果不幸预留的空间不够,就会引起concurrent mode failure;

其他指令:

  1. -XX:+UseCMSCompactAtFullCollection ,Full GC后,进行一次整理;

    整理过程是独占的,会引起停顿时间变长;

  2. -XX:+CMSFullGCsBeforeCompaction ,设置进行几次Full GC后,进行一次碎片整理;

  3. -XX:ParallelCMSThreads=n,设置CMS的线程数量;

class装载验证流程

JVM打印类加载的指令:

JDK12:-Xlog:class+load=info;

JDK8:-XX:+TraceClassLoading;

具体流程如下:

  1. 加载

    1. 加载是装载类的第一阶段;
    2. 取得类的二进制流;
    3. 转为方法区数据结构;
    4. 在Java堆中生成对应的java.lang.Class对象;
  2. 链接

    1. 验证

      1. 目的:保证Class流的格式是正确的;
      2. 文件格式的验证:是否是以0xCAFEABEB开头;版本号是否合理;
      3. 元数据验证:是否有父类;是否继承了final类;非抽象类实现了所有的抽象方法;
      4. 字节码验证:运行检查;栈数据类型和操作码数据参数吻合;跳转指令指定到合理的位置;
      5. 符号引用验证:常量池中描述是否存在;访问的方法或字段是否存在且有足够的权限;
    2. 准备

      1. 分配内存,并为类设置初始值(在方法区中);

        例如,public static int v =1;

        在准备阶段中,v会被设置为0,在初始化的中才会被置为1;

        又例如,public static final int v = 1;

        对于static final类型,在准备阶段就会被赋上目标值;

    3. 解析

      符号引用替换为直接引用;

      字符串引用对象不一定被加载;

      指针或者地址偏移量引用对象一定在内存;

  3. 初始化

    1. 执行类构造器

      此阶时执行static变量的赋值语句;

      执行static{}中的语句;

    2. 子类的调用前保证父类的被调用;

    3. 是线程安全的;

关于的补充

方法是由编译器自动收集的,包括所有类变量(非静态final)赋值和静态语句块(static{})。

执行时机

类加载的初始化阶段运行。且在执行子类前,会先执行父类的方法。也就是说JVM中执行的第一个方法来自于Object类。

测试代码:

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 13:35
 **/
public class One {

    public static void main(String[] args) {
        System.out.println("hello world!");
    }

}

测试结果:

E:\jdk12\bin\java.exe -Xlog:class+load=info "-javaagent:D:\idea\IntelliJ IDEA 2019.2\lib\idea_rt.jar=57078:D:\idea\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath E:\宝库\本地demo仓库\JVMDemo\out\production\JVM edu.zcd.SixthChapter.One
[0.008s][info][class,load] opened: E:\jdk12\lib\modules
[0.016s][info][class,load] java.lang.Object source: shared objects file
[0.016s][info][class,load] java.io.Serializable source: shared objects file

输出的部分代码,如上所示,可以到加载的第一个类确实是来自Object类的。

注意,接口中也有方法,但实现类初始化或者子类接口执行方法时,不会执行父接口的。而是要等到父接口定义变量使用时才会调用。

以下情况发生时,如果没有发生过初始化,则会先触发其初始化(JDK12):

  1. 对一个对象使用new操作;
  2. XXX.class.newInstance();(此方法在JDK9之后被弃用)
  3. 使用java.lang.reflect包的方法对类进行反射调用时;
  4. 当初始化一个类的时候,如果父类还没有初始化过,则需要先触发父类初始化(接口除外);
  5. 使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个句柄所在类还未初始化过。(未验证)

以下情况不会触发初始化:

  1. System.out.println(a.staticFinal);
  2. Class a = loader.loadClass(“xx.a”);
  3. Class.forName(“xxx”);
  4. 读取或者写入一个类的静态字段(除了final static或者已在编译器把变量放入常量池的静态字段);
  5. 调用一个类的静态方法;
  6. 执行main方法所在的类;

在以下测试中,只有两种情况不会调用:

  1. System.out.println(a.staticFinal);
  2. Class a = loader.loadClass(“xx.a”);

测试代码:

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 16:57
 **/
public class SM {

    static {
        System.out.println("静态代码块,调用静态方法...");
    }

    public static void show(){
        System.out.println("调用静态方法...");
    }

}
/**
 * @program: JVMDemo
 * @description: Class.forName()
 * @author: 郑畅道
 * @create: 2019-11-09 16:54
 **/
public class CFNTest {

    static {
        System.out.println("静态代码块,使用Class.forName()...");
    }

    public void show(){
        System.out.println("使用Class.forName()....");
    }

}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 16:56
 **/
public class CNI {

    static {
        System.out.println("静态代码块,使用class.newInstance()....");
    }

    public void show(){
        System.out.println("使用CNI.class.newInstance()....");
    }

}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 16:57
 **/
public class SField {

    static {
        System.out.println("静态代码块,读取静态字段...");
    }

    public static String show = "静态字段读取...";

    public static String input = "";

}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 17:02
 **/
public class ReflectTest {

    static {
        System.out.println("静态代码块,使用java.lang.reflect包的方法对类进行反射调用...");
    }

    public void show(){
        System.out.println("使用java.lang.reflect包的方法对类进行反射调用...");
    }

}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 17:03
 **/
public class SonTest extends FatherTest {

    static {
        System.out.println("静态代码块,子类...");
    }
    public void showSon(){
        System.out.println("这是子类...");
    }

}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 17:04
 **/
public class SFTest {

    public static final String show = "Static Final字段调用...";

    public SFTest() {
        System.out.println("初始化,Static Final字段调用...");
    }
}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 17:03
 **/
public class FatherTest {

    static {
        System.out.println("静态代码块,父类...");
    }

    public void showFather(){
        System.out.println("这是父类...");
    }

}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 17:06
 **/
public class LoadClassTest {
    static {
        System.out.println("静态代码块,使用loadClass()调用...");
    }

    public void show(){
        System.out.println("这是loadClass()调用...");
    }

}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 17:40
 **/
public class MyClassLoader extends ClassLoader {
    public Class<?> MyClassLoader(String name)throws Exception{
        FileInputStream fin = new FileInputStream(name);
        byte buf[] = new byte[512];
        int len = 0;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        while ((len = fin.read(buf)) != -1){
            bos.write(buf, 0, len);
        }
        bos.close();
        byte[] b = bos.toByteArray();
        Class<?> c = this.defineClass(null, b, 0, b.length);
        return c;
    }
}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 16:54
 **/
public class MainTest {

    public MainTest() {
        System.out.println("初始化,Main方法...");
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
            InstantiationException, NoSuchMethodException, InvocationTargetException {

        NewTest newTest = new NewTest();
        newTest.show();

        Class.forName("edu.Ctest.CFNTest");

        CNI.class.newInstance();

        System.out.println(SField.show);
        SField.input = "类静态字段写入...";

        SM.show();

        Class c = Class.forName("edu.Ctest.ReflectTest");
        Object obj = c.newInstance();
        Method method = c.getMethod("show");
        method.invoke(obj);

        SonTest sonTest = new SonTest();
        sonTest.showSon();

        System.out.println(SFTest.show);

        MyClassLoader loader = new MyClassLoader();
        Class cl = loader.loadClass("edu.Ctest.LoaderTest");

    }

}

输出:

静态代码块,使用new操作符...
使用new操作符...
静态代码块,使用Class.forName()...
静态代码块,使用class.newInstance()....
静态代码块,读取静态字段...
静态字段读取...
静态代码块,调用静态方法...
调用静态方法...
静态代码块,使用java.lang.reflect包的方法对类进行反射调用...
使用java.lang.reflect包的方法对类进行反射调用...
静态代码块,父类...
静态代码块,子类...
这是子类...
Static Final字段调用...

上述测试类中,有部分被类加载器加载,但是加载后不一定会初始化。

多线程与

在多线程场景下,由JVM保证只有一个线程能够执行该类的方法。在执行过程中,其它线程全部阻塞等待。等唯一的一次方法完成后,所有线程开始执行自己的代码。

测试代码:

/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 18:17
 **/
public class MyRunnable implements Runnable{

    public MyRunnable() {
        System.out.println(Thread.currentThread().getName() + " 线程 调用初始化方法...");
    }
    static {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " ... ");
        clinitMethod();
    }

    public static void clinitMethod(){
        System.out.println(Thread.currentThread().getName() + " 线程 静态方法...");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/**
 * @program: JVMDemo
 * @description:
 * @author: 郑畅道
 * @create: 2019-11-09 18:18
 **/
public class MainTest {

    public static void main(String[] args) {
        for (int i=0;i<10;i++){
            Thread t = new Thread(new MyRunnable());
            t.start();
        }
    }

}

输出:

main 线程 调用静态代码块...
main 线程 调用初始化方法...
main 线程 调用初始化方法...
main 线程 调用初始化方法...
main 线程 调用初始化方法...
main 线程 调用初始化方法...
main 线程 调用初始化方法...
main 线程 调用初始化方法...
Thread-0 ... 
main 线程 调用初始化方法...
Thread-1 ... 
main 线程 调用初始化方法...
Thread-0 线程 静态方法...
main 线程 调用初始化方法...
Thread-2 ... 
Thread-3 ... 
Thread-3 线程 静态方法...
Thread-2 线程 静态方法...
Thread-4 ... 
Thread-4 线程 静态方法...
Thread-5 ... 
Thread-5 线程 静态方法...
Thread-6 ... 
Thread-6 线程 静态方法...
Thread-7 ... 
Thread-7 线程 静态方法...
Thread-1 线程 静态方法...
Thread-8 ... 
Thread-9 ... 
Thread-8 线程 静态方法...
Thread-9 线程 静态方法...

可以看到确实如此。

什么是类加载器ClassLoader

ClassLoader是一个抽象类;

ClassLoader的实例将读入Java字节码并将类装载到JVM中;

ClassLoader可以定制,满足不同的字节码流获取方式;

ClassLoader负责类装载过程中的加载阶段;

ClassLoader中的重要方法:

  1. public Class loadClass(String name) throws ClassNotFoundException{…}

    载入并返回一个Class;

  2. protected final Class defineClass(byte[] b, int off, int len){…}

    定义一个类,不公开调用;

  3. protected Class findClass(String name) throws ClassNotFoundException{…}

    loadClass回调该方法,自定义ClassLoader的推荐做法;

  4. protected final Class findLoadedClass(String name){…}

    寻找已经加载的类;

关于自带性能监控工具

jps

在jdk->bin->使用命令行运行jps。

列出Java的进程,类似于Liunx中的ps命令。

相关命令:

  1. 参数-q可以指定jps只输出进程ID ,不输出类的短名称;
  2. 参数-m可以用于输出传递给Java进程(主函数)的参数;
  3. 参数-l可以用于输出主函数的完整路径;
  4. 参数-v可以显示传递给JVM的参数;

jinfo

可以用来查看正在运行的Java应用的扩展参数,支持在运行时修改部分参数。

相关命令:

  1. -flag :打印指定JVM的参数值;
  2. -flag [+|-]:设置指定JVM参数的布尔值;
  3. -flag [+|-]:设置指定JVM参数的布尔值;

jmap

生成Java应用程序的堆快照和对象的统计信息。

例如,–jmap -histo 2972 >c:\s.txt,即输出2972线程的堆快照到c盘的s.txt。

jstack

用于打印线程dump。

相关参数:

  1. -l 打印锁信息;
  2. -m打印java和native的帧信息;

JConsole

图形化监控工具。

查看Java运行程序的运行概况。

Visual VM

故障诊断和性能监控工具。

内存溢出(OOM)的原因

Java栈溢出

这里的栈溢出指,在创建线程的时候,需要为线程分配栈空间,这个栈空间是向操作系统请求的,如果操作系统无法给出足够空间,就会抛出OOM。

示意图:
Java——初探JVM_第8张图片

直接内存溢出

ByteBuffer.allocateDirect()无法从操作系统获取足够的空间。

示意图:
Java——初探JVM_第9张图片

使用Visual VM分析堆

jdk9之后此组件不再集成在JDK中。

部分资料来源于网络,侵删

你可能感兴趣的:(读书笔记)