JVM_01 内存结构

205 阅读5分钟

JVM_01 内存结构

什么是JVM

定义:

Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

好处:

  • 一次编写,到处运行

  • 自动内存管理,垃圾回收功能

  • 数组下标越界检查

  • 多态

比较:

jvm jre jdk

内存结构

1、程序计数器

jvm内存结构.png

1.1、定义

Program Counter Register 程序计数器(寄存器)

作用:是记住下一条jvm指令的执行地址

特点

  • 是线程私有的

  • 不会存在内存溢出

1.2 、作用

二进制 (解释器) JVM指令 JAVA源代码

0: getstatic    #20 	// PrintStream out = System.out; 
3: astore_1 		// -- 
4: aload_1 			// out.println(1); 
5: iconst_1 		// -- 
6: invokevirtual #26 // -- 
9: aload_1 				// out.println(2); 
10: iconst_2 			// -- 
11: invokevirtual #26	 // -- 
14: aload_1 			// out.println(3); 
15: iconst_3 			// -- 
16: invokevirtual #26 		// -- 
19: aload_1 			// out.println(4); 
20: iconst_4 			// -- 
21: invokevirtual #26 		// -- 
24: aload_1 				// out.println(5); 
25: iconst_5 			// -- 
26: invokevirtual #26 		// -- 
29: return

2.虚拟机栈

2.1、定义

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析

  • 垃圾回收是否涉及栈内存?

  • 栈内存分配越大越好吗?

  • 方法内的局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用访问,它是线程安全的

如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

2.2、栈内存溢出

  • 栈帧过多导致栈内存溢出

  • 栈帧过大导致栈内存溢出

演示栈内存溢出

/**
 * 演示栈内存溢出 java.lang.StackOverflowError
 * -Xss256k
 */
public class Demo1_2 {
    private static int count;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

    private static void method1() {
        count++;
        method1();
    }
}

2.3、线程运行诊断

案例1: cpu 占用过多

定位

  • 用top定位哪个进程对cpu的占用过高

  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)

  • jstack 进程id

可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

/**
 * 演示线程死锁
 */
class A{};
class B{};
public class Demo1_3 {
    static A a = new A();
    static B b = new B();


    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (b) {
                synchronized (a) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
    }

}

案例2:程序运行很长时间没有结果

3、本地方法栈

4、堆

4.1、定义

Heap 堆

通过 new 关键字,创建对象都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题

  • 有垃圾回收机制

4.2、堆内存溢出

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

4.3、堆内存诊断

案例

垃圾回收后,内存占用仍然很高

/**
 * 演示堆内存
 */
public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

5、方法区

5.1、定义

5.2、组成

方法区.png

/**
 * 演示 StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下设置 -XX:MaxPermSize=10m
 */
字符串延迟加载
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

方法区溢出

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

运行时常量池:

  Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。

  类加载后,常量池中的数据会在运行时常量池中存放!

  这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)

字符串常量池:

  HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容

jdk 1.7后,移除了方法区间,运行时常量池和字符串常量池都在堆中。

5.3、方法区内存溢出

  • 1.8 以前会导致永久代内存溢出
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space 
* -XX:MaxPermSize=8m
  • 1.8 之后会导致元空间内存溢出
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace 
* -XX:MaxMetaspaceSize=8m

场景

Springmybatis

5.4、运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量

等信息

  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量

池,并把里面的符号地址变为真实地址

5.5、StringTable

先看几道面试题:

String s1 = "a"; 
String s2 = "b"; 
String s3 = "a" + "b"; 
String s4 = s1 + s2; 
String s5 = "ab"; 
String s6 = s4.intern(); 
// 问 
System.out.println(s3 == s4); 
System.out.println(s3 == s5); 
System.out.println(s3 == s6); 
String x2 = new String("c") + new String("d"); 
String x1 = "cd"; 
x2.intern(); 

// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢 
System.out.println(x1 == x2);
5.5.1
  • 常量池中的字符串仅是符号,第一次用到时才变为对象

  • 利用串池的机制,来避免重复创建字符串对象 (底层map)

  • 字符串变量拼接的原理是 StringBuilder (1.8)

  • 字符串常量拼接的原理是编译期优化

  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池

    1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串

池中的对象返回

1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,

放入串池, 会把串池中的对象返回

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

5.6、StringTable 垃圾回收

5.7、StringTable 性能调优

  • 调整 -XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池

6、直接内存

6.1、定义

Direct Memory

  • 常见于 NIO 操作时,用于数据缓冲区

  • 分配回收成本较高,但读写性能高

  • 不受 JVM 内存回收管理

6.2、分配和回收原理

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法

  • ByteBuffffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffffer 对象,一旦

ByteBuffffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调

用 freeMemory 来释放直接内存

/**
 * 演示直接内存溢出
 */
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}