1、JVM启动流程
2、JVM基本结构
2.1、PC寄存器
- 每个线程拥有一个PC寄存器
- 在线程创建时 创建
- 指向下一条指令的地址
- 执行本地方法时,PC的值为undefined
2.2、方法区
- 保存装载的类信息
类型的常量池
字段,方法信息
方法字节码
JDK6,String等常量信息置于方法区,但是在JDK7时,已经移到了堆当中。 - 通常和永久区(Perm)关联在一起
内存模型
2.3、Java堆
- 和程序开发密切相关
- 应用系统对象都保存在Java堆中
- 所有线程共享Java堆
- 对分代GC来说,堆也是分代的
- GC的主要工作区间
2.4、Java栈
- 线程私有
- 栈由一系列帧组成(因此Java栈也叫做栈帧)
- 帧保存一个方法的局部变量、操作数栈、常量池指针
- 每一次方法调用创建一个帧,并压栈
2.4.1 局部变量表
包含参数和局部变量
public class StackDemo {
public static int staticMethod(int i,long l,float f,Object o ,byte b){
return 0;
}
public int instanceMethod(char c,short s,boolean b){
return 0;
}
}
对于上面的类,方法调用时栈帧里面的局部变量表如下
序号 | static表类型 | static实际类型 | static变量名 | instance表类型 | instance实际类型 | instance变量名 |
---|---|---|---|---|---|---|
0 | int | int | i | reference | - | this |
1 | long | long | l | int | char | c |
2 | - | long | l | int | short | s |
3 | float | float | f | int | boolean | b |
4 | reference | Object | o | - | - | - |
5 | int | byte | b | - | - | - |
当方法发生调用的时候,方法的栈帧对push到方法栈里面,如下所示
2.4.2 操作数栈
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 //返回
总结:局部变量都是在栈上分配的,这一部分变量永远不会出现内存泄漏(函数调用完,会自动清理)
栈上分配实验:下面一段代码
public class OnStackTest {
public static void alloc(){
byte[] b=new byte[2];
b[0]=1;
}
public static void main(String[] args) {
long b=System.currentTimeMillis();
for(int i=0;i<100000000;i++){
alloc();
}
long e=System.currentTimeMillis();
System.out.println(e-b);
}
}
1、当添加下列运行参数进行运行的时候,会有输出结果:5 (表示花费5ms)
-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC
2、当添加下列运行参数进行运行的时候
-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC
会有下面的结果
……
[GC 3550K->478K(10240K), 0.0000977 secs]
[GC 3550K->478K(10240K), 0.0001361 secs]
[GC 3550K->478K(10240K), 0.0000963 secs]
564
所以看得出来,在堆上面分配对象的时候,会有频繁的GC产生
3、总结
栈上分配一般适用于
- 小对象(一般几十个bytes),在没有逃逸(其他线程也要使用)的情况下,可以直接分配在栈上
- 直接分配在栈上,可以自动回收,减轻GC压力
- 大对象或者逃逸对象无法栈上分配
2.5、栈、堆、方法区交互
public class AppMain{
//运行时, jvm 把appmain的信息都放入方法区
public static void main(String[] args){
//main 方法本身放入方法区。
Sample test1 = new Sample( " 测试1 " );
Sample test2 = new Sample( " 测试2 " );
//test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面
test1.printName();
test2.printName();
}
}
public class Sample{
//运行时, jvm 把appmain的信息都放入方法区
//new Sample实例后, name 引用放入栈区里, name 对象放入堆里
private name;
public Sample(String name){
this.name=name;
}
//print方法本身放入 方法区里。
public void printName(){
System.out.println(name);
}
}
2.6、内存模型
2.6.1 两个概念
- 每一个线程有一个工作内存和主存(主存是所有线程可见的)
- 工作内存存放主存中变量的值的拷贝
线程工作内存与主存之间的RW关系如下所示:
1、当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作
2、每一个操作都是原子的,即执行期间不会被中断
3、对于普通变量,一个线程中更新的值,不能马上反应在其他变量中,如果需要在其他线程中立即可见,需要使用 volatile 关键字
2.6.2 volatile
如果一个线程修改主内存的共享变量的时候,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(); //在主线程当中修改stop变量的值,如果想要在VolatileStopThread线程中立即可见,那么需要添加volatile关键字
Thread.sleep(1000);
}
}
volatile 不能代替锁,一般认为volatile 比锁性能好(不绝对)
选择使用volatile的条件是:
语义是否满足应用
在上例中,如果运行main方法添加 -server参数,那么不加volatile的话,线程永远无法停止,因为-server会让当前线程一直在当前线程工作内存中查找stop字段的值。
2.6.3 可见性
可见性:一个线程修改了变量,其他线程可以立即知道
保证可见性的方法:
- volatile
- synchronized (unlock之前,写变量值回主存)
- final(一旦初始化完成,其他线程就可见)
2.6.4 有序性
在一个线程当中,所有的指令都是顺序执行的!
- 在本线程内,操作都是有序的
- 在线程外观察,操作都是无序的。(两种场景可能看来是无序的: 指令重排 或 主内存同步延时)
2.6.5 指令重排
线程内串行语义
- 写后读 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中,两句话顺序可能打乱。
如果需要规避这样的问题,可以在writer()和reader()方法上面加上关键字synchronized,这样就能保质有序性了。
指令重排的基本原则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C 那么A必然先于C
- 线程的start方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()方法
2.7 解释执行
- 解释执行以解释方式运行字节码
- 解释执行的意思是:读一句执行一句
2.8 编译运行(JIT)
- 将字节码编译成机器码
- 直接执行机器码
- 运行时编译
- 编译后性能有数量级的提升