亲宝软件园·资讯

展开

JVM入门 JVM入门之内存结构(堆、方法区)

兴趣使然的草帽路飞 人气:0
想了解JVM入门之内存结构(堆、方法区)的相关内容吗,兴趣使然的草帽路飞在本文为您仔细讲解JVM入门的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:JVM,内存结构,JVM方法区,JVM,,堆,下面大家一起来学习吧。

1、堆

在这里插入图片描述

1.1 定义

1.2 堆的作用

1.3 特点

1.4 堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出。

内存溢出案例:

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m 最大堆空间的jvm虚拟机参数,默认是4g
 */
public class Demo05 {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();// new 一个list 存入堆中-------- list的有效范围  ---------
            String a = "hello";
            while (true) {// 不断地向list 中添加 a
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }//------------------------------------------------------------------ list的有效范围  ---------
        } catch (Throwable e) {// list 使用结束,被jc 垃圾回收
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

异常输出结果:

// 给list分配堆内存后,while(true)不断向其中添加a 最终堆内存溢出 
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at com.haust.jvm_study.demo.Demo05.main(Demo05.java:19)

1.5 堆内存诊断

用于堆内存诊断的工具:

在这里插入图片描述

2、方法区

在这里插入图片描述

方法区概述:

2.1 结构(1.6 对比 1.8)

由上图可以看出,1.6版本方法区是由PermGen永久代实现(使用堆内存的一部分作为方法区),且由JVM 管理,由Class ClassLoader 常量池(包括StringTable) 组成。

1.8 版本后,方法区交给本地内存管理,而脱离了JVM,由元空间实现(元空间不再使用堆的内存,而是使用本地内存,即操作系统的内存),由Class ClassLoader 常量池(StringTable 被移到了Heap 堆中管理) 组成。

方法区的演进:面试常问

在这里插入图片描述

为什么要用元空间取代永久代?

永久代设置空间大小很难确定:(

①. 永久代参数设置过小,在某些场景下,如果动态加载的类过多,容易产生Perm区的OOM,比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误
②. 永久代参数设置过大,导致空间浪费
③. 默认情况下,元空间的大小受本地内存限制)

永久代进行调优很困难:(方法区的垃圾收集主要回收两部分:常量池中废弃的常量和不再使用的类型,而不再使用的类或类的加载器回收比较复杂,full gc 的时间长)

StringTable为什么要调整?

设置方法区大小

jdk7及以前:

jdk1.8及以后:

2.2 内存溢出

1.8以前会导致永久代内存溢出

1.8以后会导致元空间内存溢出

案例

调整虚拟机参数:-XX:MaxMetaspaceSize=8m

在这里插入图片描述

/**
 * 演示元空间内存溢出 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);
        }
    }
}
3331
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space // 元空间内存溢出
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at com.haust.jvm_study.metaspace.Demo1_8.main(Demo1_8.java:23)

2.3 常量池

常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息

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

通过反编译来查看类的信息

img

F:\JAVA\JDK8.0\bin>javac F:\Thread_study\src\com\nyima\JVM\day01\Main.javaCopy

输入完成后,对应的目录下就会出现类的.class文件

在控制台输入javap -v 类的绝对路径

javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy

然后能在控制台看到反编译以后类的信息了

img

img

img

img

2.4 运行时常量池

java 8 后,永久代已经被移除,被称为“元数据区”的区域所取代。类的元数据放入native memory, 字符串池和类的静态变量放入java堆中(静态变量之前是放在方法区)。

2.5 常量池与串池的关系

串池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: returnCopy

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

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

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

最终串池中存放:StringTable [“a”, “b”, “ab”

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


案例1:使用拼接字符串变量对象创建字符串的过程:

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

反编译后的结果:

	 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);

案例2:使用拼接字符串常量对象的方法创建字符串

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a";
		String b = "b";
		String ab = "ab";
        // 拼接字符串对象来创建新的字符串
		String ab2 = a+b;// StringBuilder().append(“a”).append(“b”).toString()
		// 使用拼接字符串的方法创建字符串
		String ab3 = "a" + "b";// String ab (javac 在编译期进行了优化)
	}
}

反编译后的结果:

 	  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

JDK1.8 中的intern方法

调用字符串对象的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);// true
		System.out.println(str == str3);// true
	}
}

例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();
		System.out.println(str == str2);// false
		System.out.println(str == str3);// false
		System.out.println(str2 == str3);// true
	}
}

JDK1.6 中的intern方法

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

如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

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

2.6 StringTable的位置

在这里插入图片描述

如图:

2.7 StringTable 垃圾回收

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

2.8 方法区的垃圾回收

(1).有些人认为方法区(如Hotspot,虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 ZGC 收集器就不支持类卸载)

(2). 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 Hotspot 虚拟机对此区域未完全回收而导致内存泄漏。

3、直接内存

img

使用了DirectBuffer

img

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

释放原理

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

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

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

allocateDirect的实现:

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

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);
}

直接内存的回收机制总结

希望大家可以多多关注的其他文章!

加载全部内容

相关教程
猜你喜欢
用户评论