非常重要,从软件层面屏蔽操作系统差别
jvm内存
- 完整的java虚拟机有三部分。
- 1、类装载系统
- 2、加载到内存(运行时数据区!)
- 3、执行起来
- new 出来的 一般都在堆里。
栈
- 线程栈。java程序运行,一个线程要存局部变量的内存区域。联系数据结构,先进后出的模型。
- 栈帧里主要的4部分,搞清楚这几部分都是干嘛的,反编译java字节码
javap -c
,了解细节。- 局部变量表
istore_1
- 操作数栈例如
iconst_1
。进行操作的临时的数据。也是先进后出! - 动态链接:对象头,对象头中有一部分是类的信息(类型指针)。指向了方法区中的类信息。对象执行那个方法 从这来的。
- 局部变量表
* 方法出口:方法执行完返回上一个方法中该执行的行号。javap -v 能看更详细的的信息。符号引用转为直接引用!方法区和堆互相引用
程序计数器
- 程序要执行的行号。即将执行的行号
方法区
jdk1.8之后改名成元数据区了。
- 存放类的常量和静态变量以及方法。简单理解里面有
HelloWorld.class
本地方法栈
线程的start()
里面调用了一个本地方法private native void start0();
- 早期可能有很多本地方法,比如说九几年公司里很多都是C/C++写的。java去调用需要用本地方法,也是一种通信。现在少了。
- 本地方法执行也是有一个栈,就是本地方法栈了。😊
堆
- 调优主要是调堆内存。
- 默认老年代占用堆的2/3
- 简单来说新new的对象在Eden区。Eden满了触发GC 这个GC是minor gc
- main方法执行完了,main栈帧出栈,进程没结束还有有GC。
什么样的是垃圾?
- 无引用的对象。
- GC ROOT:类加载器,Thread,栈的局部变量表,static成员,常量引用,本地方法栈的局部变量等等。
- 触发一次minor gc,Eden中存活的对象跑到from中。
- from中满了,触发一次minorGC,存活的对象去了to区。这时Eden存活的对象不去from了 去to区。
- to满了 存活的对象 又跑到from区 来回挪动(年龄曾长)。默认15次之后 跑到老年代。对象头中有分代年龄。
jvisualvm
来观察GC,装个插件visualGC
public class TestHeap
{
public static void main(String[] args) {
byte[] big = new byte[1024*1024];
ArrayList<byte[]> bytes = new ArrayList<byte[]>();
while (true){
bytes.add(big);
}
}
}
- old区满了 就OOM。
调优的目的
- 减少full gc的次数和时间。
- 分析gc.log,方法区满了也会full gc。比如大型的web应用 启动的时候会很多类加载。
jmm java内存模型
public class TestThread {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
System.out.println("waiting...init");
while (!flag) {
}
System.out.println("flag:" + flag);
}
}).start();
Thread.sleep(500);
new Thread(new Runnable() {
public void run() {
System.out.println("flag:" + flag);
flag = true;
System.out.println("flag:" + flag);
}
}).start();
}
}
- 总线加锁(效率低,年代久远)
- MESI缓存一致性协议:总线嗅探机制,有人把内存里的共享数据改了之后,触发总线嗅探机制。另一个人会把工作内存中的副本置为失效。然后重新read load
volatile原理
0x00000000037b512e: lock add dword ptr [rsp],0h ;*putstatic flag ; - com.obsidian.jvm.TestThread::testVolatile@1 (line 34)
- 是依靠汇编语言的前缀指令
lock
来实现的。会将修改立刻写回主内存。 - 写回内存经过总线触发总线嗅探机制。其他线程重新读取数据。
- 也加锁了store开始加锁。store的过程就是把修改的值写回主内存,锁粒度小。高性能。
- 如果不加锁,
store
的过程经过总线,触发总线嗅探机制如果还没write
,其他线程拿到的还是没修改的值。
并发编程三大特性
- 可见性
- 原子性
- 有序性
Thread[] ts = new Thread[10];
// 好好了解增强for
for (Thread t : ts) {
t = new Thread(() -> increase());
}
public class TestConcurrent {
public static volatile int num;
public static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[100];
for (int i = 0; i < ts.length; i++) {
ts[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increase();
}
});
}
for (Thread t : ts) {
t.start();
}
for (Thread t : ts) {
t.join();
}
// 输出结果<=100000
System.out.println(num);
}
}
- 原因,一个线程修改完毕后store的时候,触发了总线嗅探机制,其他线程可能已经increase完成了,但是不得不将工作内存中的值置为失效。简单来说,一个线程
store
另一个线程刚assign
完。
volatile指令重排
public class TestConcurrent1 {
private static int x;
private static int y;
public static void main(String[] args) throws InterruptedException {
HashMap<String, Integer> result = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
x = 0;
y = 0;
result.clear();
Thread t1 = new Thread(() -> {
int a = x;
y = 1;
result.put("a", a);
});
Thread t2 = new Thread(() -> {
int b = y;
x = 1;
result.put("b", b);
});
t1.start();
t2.start();
t1.join();
t2.join();
if (result.get("a") == 1 && result.get("b") == 1) {
System.out.println(result);
}
}
}
}
- 打印结果有
a=0,b=1
,a=1,b=0
- 还有a=1,b=1!涉及到指令重排。如果x,y加了volatile就能防止指令重排。