JVM启动流程
Java XXX | 装载配置 | 根据配置寻找JVM.dll | 初始化JVM获得JNIEnv接口 | 找到main方法并运行 |
根据当前路径和系统版本寻找jvm.cfg | JVM.dll为JVM主要实现 | JNIEnv为JVM接口,findClass等操作通过它实现 |
JVM基本结构
PC寄存器:
——每一个线程拥有一个PC寄存器
——在线程创建时创建
——指向下一条指令的地址
——执行本地方法时,值为undefined
方法区:
——保存装载的类信息:类型的常量池、字段,方法信息、方法字节码(JDK6时,String等常量信息置于方法;JDK7时,已经移动到了堆)
——通常和永久区联系一起
Java堆:
——和程序开发密切相关
——应用系统对象都保存在Java堆中
——所有线程共享Java堆
——对分代GC来说,堆也是分代的
——GC的主要工作区间
Java栈:
——线程私有
——栈由一系列帧组成(Java栈也叫帧栈)
——帧保存一个方法的局部变量、操作数栈、常量池指针
——每一次方法调用创建一个帧,并压栈
例:Java栈-局部变量表(包含参数和局部变量)
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;
}
}
0 | int int i |
1 | long long l |
3 | float float f |
4 | reference Object o |
5 | byte byte b |
0 | reference this |
1 | int char c |
2 | int short s |
3 | int boolean b |
public static int runStatic(int i,long l,float f,Object o,byte b){
return runStatic(i, l, f, o, b);
}
0 int int i | 0 int int i | 0 int int i |
1 long long l | 1 long long l | 1 long long l |
3 float float f | 3 float float f | 3 float float f |
4 reference Object o | 4 reference Object o | 4 reference Object o |
5 int byte b | 5 int byte b | 5 int byte b |
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 //返回
![](https://i-blog.csdnimg.cn/blog_migrate/c344b2b94ce48f78b31041fe023ccabb.png)
例:Java栈-栈上分配
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);
}
}
尝试测试一下
栈上分配参数:-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC
堆上分配参数:-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC
-小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
-直接分配在栈上,可以自动回收,减轻GC压力
-大对象或者逃逸对象无法栈上分配
例:栈、堆、方法区交互
public class AppMain {
public static void main(String[] args) {
Sample test1 = new Sample("test1");
Sample test2 = new Sample("test2");
test1.printName();
test2.printName();
}
}
class Sample {
private String name;
public Sample(String name) {
this.name = name;
}
public void printName() {
System.out.println(name);
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/60b99ed6a1669a71fdd0202f70b2bd64.png)
内存模型
——每一个线程有一个工作内存和主存独立
——工作内存存放主存中变量值的拷贝
当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作。
每一个操作都是原子的,即执行期间不会被中断。
对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。如果需要在其他线程中立即可见,需要使用 volatile 关键字。
public class VolatileStopThread extends Thread {
private volatile boolean stop = false;//无volatile则无法停止
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 不能代替锁,一般认为volatile 比锁性能好(不绝对)。选择使用volatile的条件是:语义是否满足应用。
可见性
-一个线程修改了变量,其他线程可以立即知道
保证可见性的方法
-volatile
-synchronized (unlock之前,写变量值回主存)
-final(一旦初始化完成,其他线程就可见)
-有序性
在本线程内,操作都是有序的
在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)
-指令重排
线程内串行语义
写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
以上语句不可重排
编译器不考虑多线程间的语义
可重排: a=1;b=2;
-指令重排的基本原则
程序顺序原则:一个线程内保证语义的串行性
volatile规则:volatile变量的写,先发生于读
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
传递性:A先于B,B先于C 那么A必然先于C
线程的start方法先于它的每一个动作
线程的所有操作先于线程的终结(Thread.join())
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行结束先于finalize()方法
-解释运行
解释执行以解释方式运行字节码
解释执行的意思是:读一句执行一句
-编译运行(JIT)
将字节码编译成机器码
直接执行机器码
运行时编译
编译后性能有数量级的提升
一些问题
1. 写一个程序,让程序在运行之后,最终抛出由于Perm区溢出引起的OOM,给出运行的jdk版本,程序源码,运行参数,以及系统溢出后的截图、程序所依赖的jar包说明,并说明你的基本思路。
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 想要perm抛出Oom,首先要知道oom存放什么数据: 类型的常量池, 字段、方法信息 ,方法字节码
* 由于Java想要动态创建字段、class信息需要引用到第三方Jar包。所以这个地方我利用无限创建常量池来使得抛出perm gen oom jvm
* 运行参数:-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC
*/
public class PermOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
while (true) {
list.add(UUID.randomUUID().toString().intern());
}
}
}
2.你能想到有什么办法,可以让一个程序的函数调用层次变的更深。比如,你在一个递归调用中,发生了stack的溢出,你可以做哪些方面的尝试,使系统尽量不溢出?阐述你的观点和原因。
首先了解到线程在调用每个方法的时候,都会创建相应的栈,在退出方法的时候移出栈桢,并且栈是私用的,也需要占用空间,所以让一个程序的函数调用层次变的更深,减少栈帧的空间很必要。或者增大线程的栈的大小。通过volatile增加调用层次深度。线程会对一个没有volatile的变量进行临时存储,这就导致线程栈占的空间增大,如果对一个变量增加volatile修饰,可以适当增加深度。
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();
}
}