一、堆
1.定义
2.堆内存溢出问题
1.演示
-Xmx设置堆大小
3.堆内存的诊断
3.1介绍
1,2都是命令行工具(可直接在ideal运行时,在底下打开终端,输入命令)
1可以拿到Java进程的进程ID,2 jmap只能查询某一个时刻的堆内存
3.2 堆内存jmap演示
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 MbSystem.out.println("2...");Thread.sleep(20000);array = null;System.gc();System.out.println("3...");Thread.sleep(1000000L);}
}
第一个:
第二个(Eden多了10M):
第三个:垃圾回收后
3.3jconsole演示
运行程序 在终端输入jconsole,弹出界面
选择对应的程序,点击不安全连接
3.4.案例二
现象垃圾回收后,内存仍然占用很高
工具:jvisualvm
代码
/*** 演示查看对象个数 堆转储 dump*/
public class Demo1_13 {
public static void main(String[] args) throws InterruptedException {List<Student> students = new ArrayList<>();for (int i = 0; i < 200; i++) {students.add(new Student());
// Student student = new Student();}Thread.sleep(1000000000L);}
}
class Student {private byte[] big = new byte[1024*1024];
}
点击堆Dump,进行查找
二、方法区
1.定义
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!
存的是跟类相关的信息,包括方法,构造器,成员方法等。方法区在虚拟机启动时被创建是在概念上定义的方法区,逻辑上属于堆的组成部分(厂家实现不同)方法区也会导致内存溢出的错误,抛出OutOfMemoryEror
2.组成
以Hotspot 虚拟机为例,jdk1.6 1.7 1.8 内存结构图,1.8使用的是本地内存不在占用堆内存 。
3.方法区内存溢出
-
1.8 之前会导致永久代内存溢出
使用 -XX:MaxPermSize=8m 指定永久代内存大小 -
1.8 之后会导致元空间内存溢出
-
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
-
-
设置元空间大小为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);}} }
4.运行时常量池
4.1.二进制字节码文件的构成
主要分为(类的基本信息,常量池,类方法定义,包含的虚拟机指令)
可将程序运行产生的.class文件通过 javap 命令反编译
源程序
package jvm;
public class HelloWorld {public static void main(String[] args) {System.out.println("hello world");}
}
切换到out输出目录下,使用javap命令
得到反编译后的
Last modified 2024-12-27; size 541 bytesMD5 checksum 1705415cdaac31d861d20edc1e472d95Compiled from "HelloWorld.java"
public class jvm.HelloWorldminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #6.#20 // java/lang/Object."<init>":()V#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;#3 = String #23 // hello world#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class #26 // jvm/HelloWorld#6 = Class #27 // java/lang/Object#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 LocalVariableTable#12 = Utf8 this#13 = Utf8 Ljvm/HelloWorld;#14 = Utf8 main#15 = Utf8 ([Ljava/lang/String;)V#16 = Utf8 args#17 = Utf8 [Ljava/lang/String;#18 = Utf8 SourceFile#19 = Utf8 HelloWorld.java#20 = NameAndType #7:#8 // "<init>":()V#21 = Class #28 // java/lang/System#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;#23 = Utf8 hello world#24 = Class #31 // java/io/PrintStream#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V#26 = Utf8 jvm/HelloWorld#27 = Utf8 java/lang/Object#28 = Utf8 java/lang/System#29 = Utf8 out#30 = Utf8 Ljava/io/PrintStream;#31 = Utf8 java/io/PrintStream#32 = Utf8 println#33 = Utf8 (Ljava/lang/String;)V
{public jvm.HelloWorld();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 4: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Ljvm/HelloWorld;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 6: 0line 7: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
在代码区域,每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。
ldc #3 在常量池中找一个编号为3的符号
4.2定义
常量池: 就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息 运行时常量池: 常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
4.3 StringTable
StringTable底层是一个哈希表
下面这些题你能作对吗:
答案:
public class Demo1 {
public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "a" + "b"; // ab 常量池中String s4 = s1 + s2; // new String("ab") 堆中String s5 = "ab";String s6 = s4.intern();
// 问System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // trueSystem.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd") 堆中x2.intern();String x1 = "cd";//现在是true 上放一行 x1 != x2(false)
// 如果是jdk1.6呢(1.6是将x2拷贝一份入池,1.8是本身入池)System.out.println(x1 == x2);//现在是false 上放一行 x1 != x2(false)}
}
4.3.1StringTable的特性
-
常量池中的字符串仅是符号,只有在被用到时才会转化为对象
-
利用串池的机制,来避免重复创建字符串对象
-
字符串变量拼接的原理是StringBuilder(1.8)
-
字符串常量拼接的原理是编译器优化
-
可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
package jvm; public class Demo { // ["ab", "a", "b"]public static void main(String[] args) { String x = "ab";String s = new String("a") + new String("b"); // 堆 new String("a") new String("b") new String("ab")这个ab是动态拼接的不在串池中,在堆中String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回 System.out.println( s2 == "ab");//trueSystem.out.println( s == "ab");//trueSystem.out.println( s2 == x);//trueSystem.out.println( s == x );//false} }
intern方法 1.8 调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中
-
如果串池中没有该字符串对象,则放入成功
-
如果有该字符串对象,则放入失败 无论放入是否成功,都会返回串池中的字符串对象
-
注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
4.4 StringTable的位置
jdk1.6 StringTable 位置是在永久代中,1.7,1.8 StringTable 位置是在堆中。
4.5 串池 、常量池 、 运行时常量池 的关系:
-
常量池(静态常量池):
-
常量池通常指的是.class文件中的常量池,它包含了类、方法、字段的符号引用,以及字面量等信息。这些信息在类加载到JVM之前就已经确定,并且存储在.class文件中。
-
-
运行时常量池:
-
当类被加载到JVM时,其常量池的信息会被复制到运行时常量池中。运行时常量池是方法区的一部分,它包含了从.class文件中复制来的常量,以及在运行时动态生成的常量。
-
运行时常量池相对于.class文件中的常量池具有动态性,可以在运行时添加新的常量。
-
-
StringTable(串池):
-
StringTable是运行时常量池的一部分,专门用于存储字符串常量。它通过一个哈希表(数组+链表)来实现,确保存储的字符串常量唯一且不重复。
-
在JDK 1.7及之前版本中,StringTable位于方法区(Perm Gen),而在JDK 1.8及之后版本中,StringTable被移到了堆中。
-
StringTable中存储的并不是String对象本身,而是指向堆中String对象的引用。
-
StringTable的创建是懒加载的,即只有当字符串常量第一次被使用时,才会在堆中创建String对象,并将其引用放入StringTable中。
总结来说,StringTable是运行时常量池中专门用于管理字符串常量的部分,它通过优化存储机制来确保相同内容的字符串对象在JVM中只存在一份,从而节省内存。而常量池和运行时常量池则包含了更广泛的信息,包括类、方法、字段的引用和字面量等。StringTable与常量池的关系在于,常量池中的字符串常量在类加载时会被复制到运行时常量池中的StringTable里,而运行时常量池则包含了常量池的所有内容,并支持动态添加新的常
-
4.6 StringTable 垃圾回收
StringTable底层是一个哈希表
先设置虚拟机参数(便于输出观察):
-Xmx10m 指定堆内存大小 -XX:+PrintStringTableStatistics 打印字符串常量池信息 -XX:+PrintGCDetails -verbose:gc 打印 gc 的次数,耗费时间等信息
public class StringTable {public static void main(String[] args) throws InterruptedException {int i = 0;try {for (int j = 0; j < 100000; j++) { // j=100, j=10000String.valueOf(j).intern();i++;}} catch (Throwable e) {e.printStackTrace();} finally {System.out.println(i);} } }
4.7StringTable的性能调优
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
使用时加入配置:-Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009 (字符串很多时考虑)
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
考虑是否需要将字符串对象入池 可以通过 intern 方法减少重复入池
三、直接内存
1.定义
操作系统的内存 Direct Memory
-
常见于 NIO 操作时,用于数据缓冲区
-
分配回收成本较高,但读写性能高
-
不受 JVM 内存回收管理
2.使用直接内存的好处
2.1文件读写流程:
因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。
2.2使用了 DirectBuffer 文件读取流程
直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。
3.直接内存回收原理
public class Code_06_DirectMemoryTest {public static int _1GB = 1024 * 1024 * 1024;public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
// method();method1();}// 演示 直接内存 是被 unsafe 创建与回收private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);Unsafe unsafe = (Unsafe)field.get(Unsafe.class);long base = unsafe.allocateMemory(_1GB);unsafe.setMemory(base,_1GB, (byte)0);System.in.read();unsafe.freeMemory(base);System.in.read();}// 演示 直接内存被 释放private static void method() throws IOException {ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);System.out.println("分配完毕");System.in.read();System.out.println("开始释放");byteBuffer = null;System.gc(); // 手动 gcSystem.in.read();}}
直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。 第一步:allocateDirect 的实现
public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}
底层是创建了一个 DirectByteBuffer 对象。
第二步:DirectByteBuffer 类
DirectByteBuffer(int cap) { // package-privatesuper(-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 boundaryaddress = base + ps - (base & (ps - 1));} else {address = base;}cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。att = null;
}
这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。
public void clean() {if (remove(this)) {try {// 都用函数的 run 方法, 释放内存this.thunk.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;}});}}}
可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法,
public void run() {if (address == 0) {// Paranoiareturn;}// 释放内存unsafe.freeMemory(address);address = 0;Bits.unreserveMemory(size, capacity);}
直接内存的回收机制总结
使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存
注意:
/*** -XX:+DisableExplicitGC 显示的*/private static void method() throws IOException {ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);System.out.println("分配完毕");System.in.read();System.out.println("开始释放");byteBuffer = null;System.gc(); // 手动 gc 失效System.in.read();}
一般用 jvm 调优时,会加上下面的参数:
-XX:+DisableExplicitGC // 静止显示的 GC
意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。