JVM知识点笔记

256 阅读4分钟

非常重要,从软件层面屏蔽操作系统差别

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就能防止指令重排。