JVM 【运行机制】

2019年3月18日 作者 jacky

JVM的启动流程

JVM基本结构

PC寄存器

  • 每个线程都拥有一个PC寄存器
  • 在线程创建时创建
  • 指向下一个指令的地址
  • 在执行本地方法的时候,PC的值为undefined,也就是未编译的

方法区

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

PS:JDK6时,String等常量信息位于方法,JDK7的时候,已经移移动到了堆,JDK8的时候,

Java堆

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

Java栈

  • 线程私有
  • 栈是由一系列的帧组成的(因此Java栈也叫做帧栈)
  • 栈保存一个方法的局部变量,操作数栈,常量池指针
  • 每一次方法的调用都创建一个帧,并且做压栈

Java栈 – 局部变量表 包含参数和局部变量表

public class StackDemo {
    public static ini runStatic(int i ,long l , float f , Object o , byte b){
        return 0;
    }
    
    public int runInstance(chat c, boolean b ){
        return 0;
    }
}

对于runStatic方法来说,栈中是这样的

而对于`runInstance方法来说,栈中的内容是这样的

Java栈-函数调用组成帧栈

public static int runStatic(int i , long l ,float f ,Object o , byte b{
    return runStatic(i,l,f,o,b);
}

这样递调用的函数,栈中的内容是这样的,也就是说,每次函数的调用都会产生一个帧压栈

Java栈-操作数栈

  • java中没有寄存器,所以所有的参数传递都是用操作数栈
public static int add(int a, int b ){
    int c = 0 ;
    c = a + b;
    return c;
}

具体的步骤:

Java栈-栈上分配

public class OnStackTest{
    public static void alloc(){
        byte[] b = new byte[2];
        b[0] = 1;
    }
    
    public static void main(){
        long b = System.currentTimeMills();
        for(int i = 0 ; i < 1000000000; i++){
            alloc();
        }
        
        long e = System.currentTimeMills();
        System.out.println(e - a);
    }
}
-server -Xmx10m -Xms10m
-XX:+DoEscapeAnalysis -XX:+PrintGC

//输出 。。。

-server -Xmx10m -Xms10m
-XX:-DoEscapeAnalysis -XX:+PrintGC

//输出结果可以看出,大量的GC收集,也就是说,调用玩了,就销毁了

Java栈-栈上分配好处

  • 栈是很小的,小对象(一般是几十bytes),在没有逃逸的情况下,可以直接分配在栈上,那么什么是逃逸呢?通俗来说这个对象除了我这个线程用到了,别的线程也需要用到,那么这个对象就不能再栈上分配了
  • 直接分配在栈上,可以自动回收,减轻GC压力
  • 大对象或者逃逸对象无法再栈上分配

栈,堆,方法区之间交互


内存模型

  • 每一个线程有一个工作内存和主存独立
  • 工作内存存放 主存中变量的值的拷贝

当数据从主内存复制到工作工作内存中,必须出现两个动作:

  • 第一,由主内执行的读(read)操作
  • 第二,由工作内存执行相应的加载(load)操作

当数据从工作内存拷贝到主内存时,也会出现两个操作

  • 第一,由工作内存执行的存储(store)操作
  • 第二,由主内存执行相应的写(write)操作

每一个操作都是原子性的。即执行期间是不会被中断的
对于普通的变量,一个线程中更新的值,不能马上反应在其他的变量中
如果需要在其他线程中立立即可见,需要使用volatile关键字

volatile

public class ValatileStopThread extends Thread {
    private volatile boolean stop = false;
    public void stopMe(){
        stop = true;
    }
    
    public void run(){
        int i = 0 ; 
        while(!stop){
            i++;
        }
        System.out.println("Stop thread");
    }
}

没有volatile, -server运行,是无法停止的
volatile 不能代替锁,一般认为volatile比锁性能好(但是不是绝对的,因为jvm中对锁的优化是很多的),选择使用volatile的条件是语义是否满足使用

volatile的作用

  • 可见性
    • 一个线程修改了变量,其他线程可以立即知道
  • 保证可见性的方法
    • volatile
    • synchronized(unlock之前,写变量值会主内存)
    • final(一旦初始化完成,其他线程就可见)
  • 有序性
    • 在线程中,所有的操作都是有序的
    • 在线程外观察,操作都是无序的(也就是指令重排 或 主内存同步延时)
  • 指令重排
    • 线程内串行语义
    • 写后读 a = 1 ; b = a; 写一个变量之后,在读这个变量
    • 写后写 a = 1,a = 2; 写一个变量之后,再写这个变量
    • 读后写 a = b , b = 1; 读一个变量之后,再写这个变量
    • 以上语句不可重排
    • 编译器不考虑多线程之间的语义
    • 可以重排的: a=1 ; b = 2;

指令重排 – 破坏线程之间的有序性

class OrderExample {
    int a = 0;
    boolean flag = false;
    
    public void writer(){
        a = 1;
        flag = true;
    }
    
    public void reader(){
        if(flag){
            int i = a + 1;
        }
    }
}

线程A首先执行writer()方法
线程B接着执行reader()方法
线程B在int i = a + 1 是不一定能看到a已经被赋值为1
因为在writer中,两句话顺序可能被打乱了

class OrderExample {
    int a = 0;
    boolean flag = false;
    
    public synchronized void writer(){
        a = 1;
        flag = true;
    }
    
    public synchronized void reader(){
        if(flag){
            int i = a + 1;
        }
    }
}

同步之后,即使做了writer重排,因为互斥的缘故,reader线程看writer线程也是顺序执行的

指令重排的基本原则

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

解析运行

  • 解释执行以解释的方式运行字节码
  • 解释执行的意思是: 读一句执行一句

编译运行(JIT)

  • 将字节码编程成机器码
  • 直接执行机器码
  • 运行是编译
  • 编译后性能有数量级的提升,编译比解释运行快得多,因为有编译器的优化

总结

  • JVM启动流程
  • JVM基本结构
  • 内存模型
  • 编译和解释运行的概念

作业

  1. 写一个程序,让程序在运行之后,最终抛出由于Perm区溢出引起的OOM,给出运行的jdk版本,程序源码,运行参数,以及系统溢出后的截图、程序所依赖的jar包说明,并说明你的基本思路

答:
/**
* jdk基于版本6
* 想要perm抛出Oom,首先要知道oom存放什么数据: 类型的常量池, 字段、方法信息 ,方法字节码
* 由于Java想要动态创建字段、class信息需要引用到第三方Jar包。所以这个地方我利用无限创建常量池来使得抛出perm gen oom jvm
* 运行参数:-XX:MaxPerSize=8M 程序只依懒jvm基本的jar包
*
* @author zhanghua
*
*/
public class PermOOM {

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    while (true) {
        list.add(UUID.randomUUID().toString().intern());
    }
}

}
系统溢出后打印的异常栈:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at test.classloader.PermOOM.main(PermOOM.java:20)

基本思路:
首先要知道oom存放什么数据: 类型的常量池, 字段、方法信息 ,方法字节码,
所以这个地方我利用无限创建常量池来使得抛出perm 空间填满,从而抛出perm区的Oom

2.你能想到有什么办法,可以让一个程序的函数调用层次变的更深。比如,你在一个递归调用中,发生了stack的溢出,你可以做哪些方面的尝试,使系统尽量不溢出?阐述你的观点和原因。

答:首先了解到线程在调用每个方法的时候,都会创建相应的栈,在退出方法的时候移出栈桢,并且栈是私用的,也需要占用空间,所以让一个程序的函数调用层次变的更深
减少栈贞的空间很必要。或者增大线程的线的大小。
通过volatile增加调用层次深度。线程会对一个没有volatile的变量进行临时存储,这就导致线程栈的空间增大,如果对一个变量增加volatile修饰,可以适当增加深度,详情看实验:

代码:
/**
  • jdk6
  • 启动参数是默认参数
  • @author zhanghua
    *
    */
    public class OverflowTest {
    private volatile int i=0;
    private volatile int b=0;
    private volatile int c=0;

// private int i=0;
// private int b=0;
// private int c=0;

public static void main(String[] args) {
    OverflowTest o=new OverflowTest();
    try {
        o.deepTest();
    } catch (Throwable e) {
        System.out.println("over flow deep:"+o.i);
        e.printStackTrace();
    }
}
private void deepTest() {
    ++i;
    ++b;
    ++c;
    deepTest();
}

}

在上面代码运行两次:9800(函数调用层次)上下一百范围内浮动,如果将i,b,c用volatile修饰,函数调用层次在11344左右浮动。
所以我想到的方法是:减少方法栈占用空间,或者增加线程栈的空间。