JVM的启动流程
JVM的基本结构
pc寄存器
- PC(Program Couneter)寄存器是每个线程私有的,Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,如果是本地方法,则PC寄存器值为Underfined,寄存器存放如果当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。
方法区
- 保存装载的信息、常量信息、常量池信息、方法信息,通常和永久区(Perm)关联在一起。
Java堆
- Java堆是和Java应用程序关系最密切的内存空间,所有线程共享Java堆,并且Java堆完全是自动化管理,通过垃圾收集机制,垃圾对象会自动清理,不需自己去释放。
- 根据垃圾回收机制的不同,Java堆有可能拥有不同的结构,最为常见的就是将整个Java堆分为新生代和老年代。其中新声带存放新生的对象或者年龄不大的对象,老年代则存放老年对象。绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。
Java栈
- Java栈是由一系列帧组成(因此Java栈也叫做帧栈),每一次方法调用创建一个帧,并压栈。
- Java栈一块线程私有的空间,一个栈,一般由三部分组成:局部变量表、操作数据栈和帧数据区。
- 局部变量表:包含函数的参数及局部变量。局部变量表有很多槽位,每个槽位最多可以容纳32位的数据类型,所以int占用一个槽位,float占用2个槽位,对象是一个引用,占用一个槽位。
实例方法和静态方法有一点不同,局部变量表的第一个槽位占用的是this,代表当前对象的引用。
public class StackDemo {//静态方法
public static int runStatic(int i,long l,float f,Object o ,byte b){
return null;
}
public int runInstance(char c,short s,boolean b){//实例方法
return null;
}
}
- 操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。
下图是一个两数相加的操作数栈的过程:
- 帧数据区:除了局部变量表和操作数据栈以外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着访问常量池的指针,方便计程序访问常量池,另外当函数返回或出现异常时卖虚拟机子必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。
- 直接内存:JavaNio库允许Java程序直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
- 栈、堆、方法区交互
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;
}
public void printName(){//printName方法本身放入方法区里。
System.out.println(name);
}
}
内存模型
- 每个线程有一个工作内存并和主存独立,工作内存中存放主存中变量的值得拷贝
- 当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作。每一个操作都是原子的,即执行期间不会被中断。
- 对于普通变量,一个线程中更新的值,不能马上反应在其他变量中,如果需要在其他线程中立即可见,需要使用volatile关键字,但是volatile不可以代替锁。在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
在JVM被设置成-server时运行下面代码,发现没有volatile时程序不会停止。因为在-server的模式时为了提高线程的运行效率,线程一直在本地的工作内存中取值,当加上volatile关键字时,就不会出现死循环。
public class VolatileThread extends Thread{
private static boolean flag = true;
//private static volatile boolean flag = true;
public static void main(String args[]) throws InterruptedException {
new Thread( new Runnable() {
@Override
public void run() {
while(true){
if (!flag) {
System.out.println(">>>>>>");
System.exit(0);
}
}
}
}
).start();
Thread.sleep(10); //让前面的线程先进行
new Thread( new Runnable() {
@Override
public void run() {
flag = false;
System.out.println(flag);
}
}
).start();
}
}
- 可见性:一个线程修改了变量,其他线程立即知道,保证内存可见行的方法:
①使用volatile定义变量。②使用锁。③使用final定义变量,一旦初始化完成,其他线程就可见。 - 有序性,在此线程内,操作都是有序的,在线程外操作都是无序的(指令重排和主内存同步延时)
- 指令重排:写后读,写后写,读后写等不可重排,可重排: a=1;b=2,编译器不考虑多线程间的语义。
/**
假如有两个线程A和B
线程A首先执行writer()方法
线程B线程接着执行reader()方法
线程B在int i=a+1 是不一定能看到a已经被赋值为1
因为在writer中,两句话顺序可能打乱
*/
class OrderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
int i = a +1;
}
}
}
- 指令重排的基本原则
程序顺序原则:一个线程内保证语义的串行性。
volatile规则:volatile变量的写,先发生于读。
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。
传递性:A先于B,B先于C 那么A必然先于C。
线程的start方法先于它的每一个动作。
线程的所有操作先于线程的终结(Thread.join())。
线程的中断(interrupt())先于被中断线程的代码。
对象的构造函数执行结束先于finalize()方法。
编译和解释运行的概念
解释运行
- 解释执行以解释方式(读一句执行一句)运行字节码。
编译运行(JIT)
- 运行时编译,将字节码编译成机器码,直接执行机器码,编译后性能有数量级的提升。