JVM--内存结构

2021-09-03  本文已影响0人  zhemehao819

一、什么是JVM

定义

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

好处

比较

JVM JRE JDK的区别

截屏2021-09-18 20.57.24.png

二、内存结构

整体架构

截屏2021-09-18 21.00.52.png

分成3大块:类加载器、JVM内存结构、执行引擎。

1、程序计数器

作用

用于保存JVM中下一条所要执行的指令的地址

特点

2、虚拟机栈

定义

演示

public class Main {
    public static void main(String[] args) {
        method1();
    }
    private static void method1() {
        method2(1, 2);
    }
    private static int method2(int a, int b) {
        int c = a + b;
        return c;
    }
}
截屏2021-09-18 21.16.57.png

在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点

问题辨析

内存溢出

Java.lang.stackOverflowError 栈内存溢出
使用 -Xss256k 指定栈内存大小

发生原因

线程运行诊断

案例一:cpu 占用过多

3、本地方法栈

一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法

4、堆

定义

通过new关键字创建的对象都会被放在堆内存

特点

堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出
可以使用 -Xmx8m 来指定堆内存大小。
调试程序时怀疑是堆内存溢出,可以尝试把堆内存调小一点试试。

堆内存诊断

        System.out.println("1");
        Thread.sleep(30000);

        System.out.println("2");
        byte[] bytes = new byte[1024 * 1024 *10];
        Thread.sleep(10000);

        System.out.println(3);
        bytes = null;
        System.gc();
        Thread.sleep(10000);

-- java11环境:jhsdb jmap --heap --pid 922

5、方法区

定义

JVM线程之间共享的方法区域。它存储每个类的结构数据,例如运行时常量池字段方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。

永久代(1.8之前):存储包括类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。

元空间(1.8以后):使用的是物理内存,元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息常量池方法数据方法代码符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。

方法区内存溢出

常量池

二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

使用 javap -v Hello.class 命令反编译查看结果:

截屏2021-09-20 11.05.02.png

运行时常量池

串池StringTable

public class StringTableStudy {
    public static void main(String[] args) {
        String a = "a"; 
        String b = "b";
        String ab = "ab";
    }
}

编译后的二进制中,常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串

0: ldc           #2                  // String a
2: astore_1
3: ldc           #3                  // String b
5: astore_2
6: ldc           #4                  // String ab
8: astore_3
9: return

当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)

当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中

当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中

最终StringTable [“a”, “b”, “ab”]

注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

public class StringTableStudy {
    public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String ab = "ab";
        //拼接字符串对象来创建新的字符串
        String ab2 = a+b; 
    }
}

反编译后的结果:

     Code:
      stack=2, locals=5, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
        27: astore        4
        29: return

通过变量拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()

最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中

String ab = "ab";
String ab2 = a+b;
//结果为false,因为ab是存在于串池之中,ab2是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
System.out.println(ab == ab2);
public class StringTableStudy {
    public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String ab = "ab";
        String ab2 = a+b;
        //使用拼接字符串的方法创建字符串
        String ab3 = "a" + "b";
    }
}

反编译后的结果:

      Code:
      stack=2, locals=6, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
        27: astore        4
        //ab3初始化时直接从串池中获取字符串
        29: ldc           #4                  // String ab
        31: astore        5
        33: return

intern方法 (jkd1.8)

一般,只有字面量才能放入串池中。

但,调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

注意:此时如果调用intern方法成功(放入串池成功),堆内存与串池中的字符串对象是同一个对象;如果失败(放入串池失败,也就是串池中已经存在),则不是同一个对象
例1

public class Main {
    public static void main(String[] args) {
        //"a" "b" 被放入串池中,str则存在于堆内存之中
        String str = new String("a") + new String("b");
        //调用str的intern方法,这时串池中没有"ab",则会将该字符串对象放入到串池中,此时堆内存与串池中的"ab"是同一个对象
        String st2 = str.intern();
        //给str3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回
        String str3 = "ab";
        //因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为true
        System.out.println(str == st2);
        System.out.println(str == str3);
    }
}

例2

public class Main {
    public static void main(String[] args) {
        //此处创建字符串对象"ab",因为串池中还没有"ab",所以将其放入串池中
        String str3 = "ab";
        //"a" "b" 被放入串池中,str则存在于堆内存之中
        String str = new String("a") + new String("b");
        //此时因为在创建str3时,"ab"已存在与串池中,所以放入失败,但是会返回串池中的"ab"
        String str2 = str.intern();
        //false
        System.out.println(str == str2);
        //false
        System.out.println(str == str3);
        //true
        System.out.println(str2 == str3);
    }
}

intern方法 (jkd1.6)

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

StringTable 的位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

永久代中gc是full gc,堆内存中gc是minorr gc。

/**
 * 演示 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);
        }
    }
}

通过不同版本jdk的StringTable内存不足时报错信息:

jdk1.6:java.lang.OutOfMemoryError:PermGen space
jdk1.8:java.lang.OutOfMemoryError:GC overhead limit exceeded
或者:java.lang.OutOfMemoryError:Java heap space

StringTable 垃圾回收

StringTable在内存紧张时,会发生垃圾回收

-Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息

【代码演示】

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                I++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

分别看往StringTable里添加100和10000个字符串常量看串池常量数增加和gc现象。

StringTable调优

-XX:StringTableSize=桶个数(最少设置为 1009 以上)
/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {
    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }
    }
}
/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {
    public static void main(String[] args) throws IOException {
        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

6、直接内存

文件读写流程:

使用了DirectBuffer:

直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率

释放原理

直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放

通过
//通过ByteBuffer申请1M的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);

申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?

allocateDirect的实现

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

DirectByteBuffer类

DirectByteBuffer(int cap) {   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size); //申请内存
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象
    att = null;
}

这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存

public void clean() {
       if (remove(this)) {
           try {
               this.thunk.run(); //调用run方法
           } catch (final Throwable var2) {
               AccessController.doPrivileged(new PrivilegedAction<Void>() {
                   public Void run() {
                       if (System.err != null) {
                           (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                       }

                       System.exit(1);
                       return null;
                   }
               });
           }

对应对象的run方法

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address); //释放直接内存中占用的内存
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

直接内存的回收机制总结

上一篇下一篇

猜你喜欢

热点阅读