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

目录
  • 1、堆
    • 1.1 定义
    • 1.2 堆的作用
    • 1.3 特点
    • 1.4 堆内存溢出
    • 1.5 堆内存诊断
  • 2、方法区
    • 2.1 结构(1.6 对比 1.8)
    • 2.2 内存溢出
    • 2.3 常量池
    • 2.4 运行时常量池
    • 2.5 常量池与串池的关系
    • 2.6 StringTable的位置
    • 2.7 StringTable 垃圾回收
    • 2.8 方法区的垃圾回收
  • 3、直接内存
    • 释放原理

1、堆

1.1 定义

  • 是Java内存区域中一块用来存放对象实例的区域几乎所有的对象实例都在这里分配内存
  • 通过new关键字创建的对象都会被放在堆内存,jvm 运行时数据区中,占用内存最大的就是堆(Heap)内存!

1.2 堆的作用

  • 此内存区域的唯一目的就是存放对象实例
  • 方法体中的引用变量和基本类型的变量都在栈上,其他都在堆上
  • Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块 Java 堆是被所有线程共享的一块内存区域

1.3 特点

  • 所有线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制,Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage)
  • Java堆可以分成新生代和老年代 新生代可分为To Space、From Space、Eden
  • -Xmx -Xms:JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64

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 堆内存诊断

用于堆内存诊断的工具:

  • jps
  • jmap
  • jconsole
  • jvirsalvm

2、方法区

方法区概述:

  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样都可以是不连续的, 关闭Jvm就会释放这个区域的内存。
  • 方法区逻辑上是堆的一个组成部分,但是在不同版本的虚拟机里实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)
    • (注意:方法区时一种规范,而永久代和元空间是它的一种实现方式)
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:(java.lang.OutOfMemoryError:PermGen space、java.lang.OutOfMemoryError:Metaspace)。
  • 方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

  • 类型信息:( 类class、接口interface、枚举enum、注解annotation)JVM必须在方法区中存储以下类型信息:

    • 这个类型的完整有效名称(全名=包名.类名)
    • 这个类型直接父类的完整有效名(对于interface或是java. lang.Object,都没有父类)
    • 这个类型的修饰符(public, abstract, final的某个子集)
    • 这个类型直接接口的一个有序列表
  • 域信息(成员变量):
    • JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
    • 域的相关信息包括:域名称、 域类型、域修饰符(public, private, protected, static, final, volatile, transient的某个子集)。
  • 方法信息:JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序
    • 方法名称
    • 方法的返回类型(或void)
    • 方法参数的数量和类型(按顺序)
    • 方法的修饰符(public, private, protected, static, final,synchronized, native , abstract的一个子集)
    • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native 方法除外)异常表( abstract和native方法除外)
    • 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

2.1 结构(1.6 对比 1.8)

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

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

  • 方法区是什么

    • 是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(比如class文件)、常量、静态变量、即时编译器编译后的代码等数据。
    • 什么是类信息:类版本号、方法、接口
  • 方法区作用
    • 内存中存放类信息、静态变量、常量等数据,属于线程共享的一块区域。
    • Hotspot使用永久代来实现方法区 JRockit、IBM J9VM Java堆一样管理这部分内存。
  • 方法区特点
    • 并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
    • 方法区也会抛出OutofMemoryError,当它无法满足内存分配需求时 。

方法区的演进:面试常问

  • Jdk 1.6 及之前:有永久代(静态变量存放在永久代上)、字符串常量池(1.6在方法区)
  • Jdk 1.7 :有永久代,但已经逐步 " 去永久代 ",字符串常量池、静态变量移除,保存在堆中
  • dk 1.8 及之后: 无永久代,常量池1.8在元空间。但静态变量、字符串常量池仍在堆中

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

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

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

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

StringTable为什么要调整?

  • jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才能触发。而full gc是老年代的空间不足、永久代不足才会触发。
  • 这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放到堆里,能及时回收内存。

设置方法区大小

jdk7及以前:

  • -XX:PermSize=100m(默认值是20.75M)
  • -XX:MaxPermSize=100m(32位机器默认是64M,64位机器模式是82M)

jdk1.8及以后:

  • -XX:MetaspaceSize=100m(windows下,默认约等于21M)
  • -XX:MaxMetaspaceSize=100m(默认是-1,即没有限制)

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 常量池

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

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

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

  • 获得对应类的.class文件
  • 在JDK对应的bin目录下运行cmd,也可以在IDEA控制台输入

  • 输入 javac 对应类的绝对路径

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

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

  • 类的基本信息

  • 常量池

  • 虚拟机中执行编译的方法(框内的是真正编译执行的内容,#号的内容需要在常量池中查找)

2.4 运行时常量池

  • 常量池

    • 就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
  • 运行时常量池
    • 常量池是*.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实内存地址
    • 行时常量池( Runtime Constant Pool)是方法区的一部分。
    • 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
    • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
    • **(方法区内常量池之中主要存放的两大类常量:字面量和符号引用。**字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
      • 类和接口的全限定名
      • 字段的名称和描述符
      • 方法的名称和描述符

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

2.5 常量池与串池的关系

串池StringTable

  • 常量池是.class文件,存放堆中数据的引用地址,而不是真实的对象,运行时常量池是jvm运行时将常量池中数据放入池中,此时引用地址真正的指向对象而不是.class文件;Stringtable是哈希表(不能扩容),它也叫做串池,用来存储字符串,这3个不是同一个东西,我们需要进行区分。
  • StringTable中存储的并不是String类型的对象,存储的而是指向String对象的索引,真实对象还是存储在堆中
  • jdk1.6中,StringTable是放在永久代(方法区)中,jvm进行FullGC才会对常量池进行垃圾回收,影响效率,因此在jdk1.8中将StringTable放在堆中,jvm内存紧张时就会对StringTable进行垃圾回收。

特征

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

注意:无论是串池还是堆里面的字符串,都是对象

串池作用:用来放字符串对象且里面的元素不重复

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
  • 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab”一致。
  • 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuffer来创建

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的位置

如图:

  • JDK1.6 时,StringTable是属于常量池的一部分。
  • JDK1.8 以后,StringTable是放在堆中的。

2.7 StringTable 垃圾回收

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

2.8 方法区的垃圾回收

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

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

  • 法区的垃圾收集主要回收两部分内容常量池中废奔的常量和不再使用的类型
  • 先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。 字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
  • HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃常量与回收Java堆中的对象非常类似。
  • 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
    • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
    • Java虛拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了一Xnoclassgc 参数进行控制,还可以使用一verbose:class以及一XX: +TraceClass一Loading、一XX:+TraceClassUnLoading查看类加载和卸载信息。
    • 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及oSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

3、直接内存

  • 属于操作系统,常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理 文件读写流程

使用了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);
}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);
}

直接内存的回收机制总结

  • 使用了Unsafe类来完成直接内存的分配回收,回收需要主动调用freeMemory方法。
  • ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存。ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存。

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

(0)

相关推荐

  • JVM入门之类加载与字节码技术(类加载与类的加载器)

    1. 类加载阶段 1.1 加载阶段 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有: _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用 _super 即父类 _fields 即成员变量 _methods 即方法 _constants 即常量池 _class_loader 即类加载器 _vtable 虚方法表 _ita

  • JVM上高性能数据格式库包Apache Arrow入门和架构详解(Gkatziouras)

    Apache Arrow是是各种大数据工具(包括BigQuery)使用的一种流行格式,它是平面和分层数据的存储格式.它是一种加快应用程序内存密集型. 数据处理和数据科学领域中的常用库: Apache Arrow.诸如Apache Parquet,Apache Spark,pandas之类的开放源代码项目以及许多商业或封闭源代码服务都使用Arrow.它提供以下功能: 内存计算 标准化的柱状存储格式 一个IPC和RPC框架,分别用于进程和节点之间的数据交换 让我们看一看在Arrow出现之前事物是如何

  • 新手入门Jvm--jvm概览

    目录 1. 什么是Jvm 2. Jvm组成 2.1 Jdk体系结构 2.2 Jvm体系结构 3. 类加载机制 3.1 流程 3.2 类加载器和双亲委派机制 3.3 类加载器初始化过程 3.4 双亲委派机制 3.5 为什么要设计双亲委派机制? 3.6 全盘负责委托机制 4. Jvm各区域串联 5. 总结 1. 什么是Jvm JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能

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

    目录 1.堆 1.1 定义 1.2 堆的作用 1.3 特点 1.4 堆内存溢出 1.5 堆内存诊断 2.方法区 2.1 结构(1.6 对比 1.8) 2.2 内存溢出 2.3 常量池 2.4 运行时常量池 2.5 常量池与串池的关系 2.6 StringTable的位置 2.7 StringTable 垃圾回收 2.8 方法区的垃圾回收 3.直接内存 释放原理 1.堆 1.1 定义 是Java内存区域中一块用来存放对象实例的区域[几乎所有的对象实例都在这里分配内存] 通过new关键字创建的对象都

  • 深入理解Java虚拟机 JVM 内存结构

    目录 前言 JVM是什么 JVM内存结构概览 运行时数据区 程序计数器 Java虚拟机栈 本地方法栈 方法区 运行时常量池 Java堆 直接内存 前言 JVM是Java中比较难理解和掌握的一部分,也是面试中被问的比较多的,掌握好JVM底层原理有助于我们在开发中写出效率更高的代码,可以让我们面对OutOfMemoryError时不再一脸懵逼,可以用掌握的JVM知识去查找分析问题.去进行JVM的调优.去让我们的应用程序可以支持更高的并发量等......总之一句话,学好JVM很重要! JVM是什么 J

  • java虚拟机jvm方法区实例讲解

    和java堆一样,方法区是一块所有线程共享的内存区域,用于保存系统的类信息,类的信息有哪些呢.字段.方法.常量池.方法区也有一块内存区域所以方法区的内存大小,决定了系统可以包含多少个类,如果系统类太多,方法区内存不够肯定会导致方法区溢出,虚拟机同样会抛出内存溢出信息.(内存溢出后面相关文章给大家总结) jdk6和jdk7中,方法区可以理解为永久区(Perm).永久区可以使用参数-XX:PermSize和-XX:MaxPermSize制定.默认情况下-XX:MaxPermSize为64MB.如果你

  • 关于java方法区详解

    目录 方法区 图例(方法区中都保存什么) 类型信息 类型的常量池 (即运行时常量池) 字段信息 方法信息 类变量(即static变量) 对类加载器的引用 对Class类的引用 方法表 JVM如何使用方法区里面的数据 方法区 保存在着被加载过的每一个类的信息:这些信息由类加载器在加载类的时候,从类的源文件中抽取出来:static变量信息也保存在方法区中: 可以看做是将类(Class)的元数据,保存在方法区里: 方法区是线程共享的:当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线

  • JVM入门之JVM内存结构内容详解

    一.java代码编译执行过程 源码编译:通过Java源码编译器将Java代码编译成JVM字节码(.class文件) 类加载:通过ClassLoader及其子类来完成JVM的类加载 类执行:字节码被装入内存,进入JVM虚拟机,被解释器解释执行   注:Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,   用Java语言编写并编译的程序可以运行在这个平台上 二.JVM简介 1.java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然

  • JVM内存结构:程序计数器、虚拟机栈、本地方法栈

    目录 一.JVM 入门介绍 JVM 定义 JVM 优势 JVM JRE JDK的比较 学习步骤 二.内存结构 整体架构 1.程序计数器(寄存器) 1.1 作用 1.2 特点 2.虚拟机栈 2.1 定义 2.2 演示 2.3 面试问题辨析 2.4 内存溢出 2.5 线程运行诊断 3.本地方法栈 4.总结 一.JVM 入门介绍 JVM 定义 Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境) JVM 优势 一次编写,到处运行 自动内存管理,垃圾回收机制

  • JVM内存结构相关知识解析

    最近在看< JAVA并发编程实践 >这本书,里面涉及到了 Java 内存模型,通过 Java 内存模型顺理成章的来到的 JVM 内存结构,关于 JVM 内存结构的认知还停留在上大学那会的课堂上,一直没有系统的学习这一块的知识,所以这一次我把< 深入理解Java虚拟机JVM高级特性与最佳实践 >.< Java虚拟机规范 Java SE 8版 >这两本书中关于 JVM 内存结构的部分都看了一遍,算是对 JVM 内存结构有了新的认识.JVM 内存结构是指:Java 虚拟机定义

  • JVM内存结构划分实例解析

    这篇文章主要介绍了JVM内存结构划分实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 数据区域划分 运行时内存区域划分:程序计数器.虚拟机栈.本地方法栈.堆.方法区 程序计数器 线程私有 通过寄存器实现 不会存在运行溢出 当前线程所执行的行号指示器,记住下一条JVM指令的执行地址 虚拟机栈 垃圾回收不涉及栈内存 栈内存是线程私有的,可以理解为线程运行需要的内存空间 栈由栈帧组成,每个栈帧代表一个方法执行时需要的内存(参数,局部变量,返回地

  • 深入解析JVM之内存结构及字符串常量池(推荐)

    前言 Java作为一种平台无关性的语言,其主要依靠于Java虚拟机--JVM,我们写好的代码会被编译成class文件,再由JVM进行加载.解析.执行,而JVM有统一的规范,所以我们不需要像C++那样需要程序员自己关注平台,大大方便了我们的开发.另外,能够运行在JVM上的并只有Java,只要能够编译生成合乎规范的class文件的语言都是可以跑在JVM上的.而作为一名Java开发,JVM是我们必须要学习了解的基础,也是通向高级及更高层次的必修课:但JVM的体系非常庞大,且术语非常多,所以初学者对此非

  • 分别在Linux和Windows下设置JVM内存的简单方法

    Linux服务器: 在/usr/local/apache-tomcat-5.5.23/bin 目录下的catalina.sh 添加:JAVA_OPTS='-Xms512m -Xmx1024m' 或者 JAVA_OPTS="-server -Xms800m -Xmx800m -XX:MaxNewSize=256m" 或者 CATALINA_OPTS="-server -Xms256m -Xmx300m" Windows服务器: 在/apache-tomcat-5.5.

随机推荐