二、JVM运行机制

1 JVM启动流程在这里插入图片描述
2 JVM组成
JVM组成:
类加载器子系统(class loader),
执行引擎子系统
运行时数据区(Runtime Data Area)
Native接口

在这里插入图片描述

在这里插入图片描述

3 类加载器(classloader)
负责加载字节码到jvm中,根据类中定义的内容分配到不同的运行时数据区

分类:
1)Bootstrap ClassLoader:classLoader的根,启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。
2)Extension ClassLoader 扩展类加载器:这个类加载器由sun.misc.LauncherKaTeX parse error: Expected 'EOF', got '\lib' at position 32: …。它负责<JAVA_HOME>\̲l̲i̲b̲\ext目录中的,或者被jav…AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。
4)user ClassLoader 用户自定义类加载器

在这里插入图片描述

4 类加载器的双亲委派模型
请注意双亲委派模式中的父子关系并非通常所说的类继承关系。
  其工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。
  通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改
  
5 类加载器加载过程
1)加载
负责找到class文件,并加载到jvm中,根据全类名加载
2).连接
校验:确保类的格式正确,校验class是否规范
准备:为类中的变量分配内存空间,并初始化默认值
解析:符号引用转换为直接引用,new

3).初始化: 执行静态代码块,构造器,静态属性
在调用new(), 反射调用类中的方法,子类调用初始化函数

6 执行引擎
执行class指令,将class指令转为操作系统能够识别的指令

7 本地接口
JAVA API

8 运行时数据区

1)堆(线程共有)
和程序开发密切相关
应用系统对象都保存在Java堆中
所有线程共享Java堆
对分代GC来说,堆也是分代的
GC的主要工作区间
在这里插入图片描述
虚拟机中用于存放对象与数组实例的地方,垃圾回收的主要区域就是这里(还可能有方法区)。
如果垃圾收集算法采用按代收集(目前大都是这样),这部分还可以细分为新生代和老年代。
新生代又可能分为Eden区(伊甸区),From Survivor区(幸存0区)和To Survivor区(幸存1区),主要是为了垃圾回收。所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
伊甸区:创建对象时分配空间
发生GC后,伊甸区存活对象放到幸存区,如果对象比较大会直接放到老年代
老年代:生命周期比较长的对象

Java堆只要求逻辑上是连续的,在物理空间上可以不连续。

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

3)虚拟机栈/java栈(线程私有)
线程私有
栈由一系列帧组成(因此Java栈也叫做帧栈)
帧保存一个方法的局部变量、操作数栈、常量池指针,动态链接、方法出口等
每一次方法调用创建一个帧,并压栈

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

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

在这里插入图片描述

b) Java栈 – 函数调用组成帧栈
public static int runStatic(int i,long l,float f,Object o ,byte b){
return runStatic(i,l,f,o,b);
}
在这里插入图片描述
c) Java栈 – 操作数栈
Java没有寄存器,所有参数传递使用操作数栈
在这里插入图片描述

Java栈 – 栈上分配
小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
直接分配在栈上,可以自动回收,减轻GC压力
大对象或者逃逸对象无法栈上分配

逃逸分析:对象被外部引用,不能被GC回收

4)本地方法栈(线程私有)
与虚拟机栈类似,只是是执行本地方法时使用的。
本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法
后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法(操作系统的方法,c或c++);

5)程序计数器(线程私有)
每个线程拥有一个PC寄存器,较小的内存空间,一般不出现OOM,当前线程执行的字节码的行号指示器(多线程切换执行对应行号的指令)
在线程创建时 创建
指向下一条指令的地址
执行本地方法时,PC的值为undefined
在这里插入图片描述
在这里插入图片描述

直接内存(堆外内存)
不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;这块内存不受java堆大小限制,但受本机总内存的限制,可以通过设置MaxDirectMemorySize(默认与堆内存最大值一样),所以也会出现OOM异常

对象访问

当新建一个对象时,会在堆中为这个对象分配内存,并在栈中有一个对这个对象引用,除此之外,在Java堆中还要能通过这个对象找到它的类型信息(对象类型,父类,实现的接口,包含的字段与方法等)。

在这里插入图片描述
9 JVM 内存模型(jmm)

在这里插入图片描述

当数据从主内存复制到工作内存时,必须出现两个动作:
第一,由主内存执行的读(read)操作;
第二,由工作内存执行的相应的load操作;
当数据从工作内存拷贝到主内存时,也出现两个操作:
第一个,由工作内存执行的存储(store)操作;
第二,由主内存执行的相应的写(write)操作

每一个操作都是原子的,即执行期间不会被中断

对于普通变量,一个线程中更新的值,不能马上反应在其他变量中
如果需要在其他线程中立即可见,需要使用 volatile 关键字

一个线程再修改普通变量的时候,其他线程不能看到修改的值

3种可以看到的方式
a)volatile
b) final 一旦初始化完成,其他线程就可见
c) Synchronized:(必须是同一把锁)
1).线程加锁的时候,先清空工作内存中的变量,因此使用变量时,
需要从主内存中重新读取最新的值
2).线程解锁时,把线程独有的工作内存中的变量刷新到主内存中

线程执行互斥的过程(Synchronized)
1).获取互斥锁
2).清空工作内存
3).从主内从同步变量的值到工作内存

4).执行代码(工作内存)
5).把工作内存中的变量刷新到主内容
6).释放锁

public class Ticket implements Runnable {
private int count = 100;// 100张票 成员变量(属性)存在堆里面(线程共享),线程不安全

	@Override
	public void run() {
		
		while (this.count > 0) {
			if (this.count > 0) {
				
				System.out.println(Thread.currentThread() + "售出第"
						+ (count--) + "张票");
				try {
					Thread.sleep(200);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		
		int i = 0;//局部变量,存入栈中(线程私有),线程安全
		while(i<100){
			i++;
			try {
				Thread.sleep(100);
			} catch (Exception e) {
				e.printStackTrace();
			}
			
		}
		System.out.println("线程内部变量值为:"+i);
	}
	
	
    public static void main(String[] args) {  
        Ticket tr=new Ticket();  
        //四个线程对应四个窗口  
        Thread t1=new Thread(tr, "窗口A");  
        Thread t2=new Thread(tr, "窗口B");  
        Thread t3=new Thread(tr, "窗口C");  
        Thread t4=new Thread(tr, "窗口D");  
          
        t1.start();  
        t2.start();  
        t3.start();  
        t4.start();  
    }  

}
出现了0票
在这里插入图片描述

加 volatile 关键字

public class Ticket implements Runnable {
private volatile int count = 100;// 100张票

	@Override
	public void run() {
		
		while (this.count > 0) {
			if (this.count > 0) {
				
				System.out.println(Thread.currentThread() + "售出第"
						+ (count--) + "张票");
				try {
					Thread.sleep(200);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		
		int i = 0;
		while(i<100){
			i++;
			try {
				Thread.sleep(100);
			} catch (Exception e) {
				e.printStackTrace();
			}
			
		}
		System.out.println("线程内部变量值为:"+i);
	}
	
	
    public static void main(String[] args) {  
        Ticket tr=new Ticket();  
        //四个线程对应四个窗口  
        Thread t1=new Thread(tr, "窗口A");  
        Thread t2=new Thread(tr, "窗口B");  
        Thread t3=new Thread(tr, "窗口C");  
        Thread t4=new Thread(tr, "窗口D");  
          
        t1.start();  
        t2.start();  
        t3.start();  
        t4.start();  
    }  

}
出现了0票, volatile 关键字保证了可见性,但无法保证原子性,不能保证线程的一系列有序操作不被干扰,volatile 关键字相当于让所有线程直接操作主存,不做工作内存与主存的同步操作
在这里插入图片描述

public class Ticket implements Runnable {
private int count = 100;// 100张票

	@Override
	public void run() {
		
		while (this.count > 0) {
			if (this.count > 0) {
				synchronized (this) {
					System.out.println(Thread.currentThread() + "售出第"
							+ (count--) + "张票");
				}
				
				try {
					Thread.sleep(200);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		
		int i = 0;
		while(i<100){
			i++;
			try {
				Thread.sleep(100);
			} catch (Exception e) {
				e.printStackTrace();
			}
			
		}
		System.out.println("线程内部变量值为:"+i);
	}
	
	
    public static void main(String[] args) {  
        Ticket tr=new Ticket();  
        //四个线程对应四个窗口  
        Thread t1=new Thread(tr, "窗口A");  
        Thread t2=new Thread(tr, "窗口B");  
        Thread t3=new Thread(tr, "窗口C");  
        Thread t4=new Thread(tr, "窗口D");  
          
        t1.start();  
        t2.start();  
        t3.start();  
        t4.start();  
    }  

}
加锁之后正常
在这里插入图片描述

10 指令重排
有序性 :在本线程内,操作都是有序的;在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)。
指令重排:不按代码顺序执行
什么情况下不会产生指令重排,也就是串行执行:
1. a=1;b=a; 写后读, 先写一个变量,然后再读取这个变量
2. a=1;a=11; 写后写,两次写同一个变量
3.a=b;b=1;读后写, 先读取变量值,然后在写变量值

会产生指令重排:
4 a=1;b=11; 写后写,两次写不同变量

指令重排 – 破坏线程间的有序性
实例
在这里插入图片描述
指令重排 – 保证有序性的方法,加Synchronized关键字
在这里插入图片描述

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

12 volatile可见性
可见性:一个线程修改了变量,其他线程可以立即知道
public class VolatileStopThread 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”);
}

public static void main(String args[]) throws InterruptedException{
VolatileStopThread t=new VolatileStopThread();
t.start();
Thread.sleep(1000);
t.stopMe();
Thread.sleep(1000);
}
}
没有volatile -server 运行 无法停止

volatile 不能代替锁
一般认为volatile 比锁性能好(不绝对)
选择使用volatile的条件是:语义是否满足应用

13 解释运行与编译运行(JIT)
1)解释运行
解释执行以解释方式运行字节码
解释执行的意思是:读一句执行一句
2)编译运行(JIT)
将字节码编译成机器码
直接执行机器码
运行时编译
编译后性能有数量级的提升

参考:https://blog.csdn.net/qq_33583322/article/details/81239508


版权声明:本文为m0_38143867原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>