Python 官方文档:入门教程 => 点击学习
目录1、堆1.1 定义1.2 堆的作用1.3 特点1.4 堆内存溢出1.5 堆内存诊断2、方法区2.1 结构(1.6 对比 1.8)2.2 内存溢出2.3 常量池2.4 运行时常量池
几乎所有的对象实例都在这里分配内存
】new
关键字创建的对象都会被放在堆内存,JVM 运行时数据区中,占用内存最大的就是堆(Heap)内存!堆是被所有线程
共享的一块内存区域-Xmx -Xms
:JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64 java.lang.OutofMemoryError :java heap space.
堆内存溢出。
内存溢出案例:
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)
用于堆内存诊断的工具:
方法区概述:
java.lang.OutOfMemoryError:PermGen space、java.lang.OutOfMemoryError:Metaspace)。
由上图可以看出,1.6版本方法区是由PermGen永久代实现(使用堆内存的一部分作为方法区),且由JVM 管理,由Class ClassLoader 常量池(包括StringTable) 组成。
1.8 版本后,方法区交给本地内存管理,而脱离了JVM,由元空间实现(元空间不再使用堆的内存,而是使用本地内存,即操作系统的内存),由Class ClassLoader 常量池(StringTable 被移到了Heap 堆中管理) 组成。
方法区的演进:
面试常问
为什么要用元空间取代永久代?
永久代设置空间大小很难确定:(
①. 永久代参数设置过小,在某些场景下,如果动态加载的类过多,容易产生Perm区的OOM,比如某个实际WEB工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误
②. 永久代参数设置过大,导致空间浪费
③. 默认情况下,元空间的大小受本地内存限制)
永久代进行调优很困难:(方法区的垃圾收集主要回收两部分:常量池中废弃的常量和不再使用的类型,而不再使用的类或类的加载器回收比较复杂,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,即没有限制)1.8以前会导致永久代
内存溢出
1.8以后会导致元空间
内存溢出
案例
调整虚拟机参数:-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)
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息
类的二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)。
通过反编译来查看类的信息:
.class
文件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
然后能在控制台看到反编译以后类的信息了
java 8 后,永久代已经被移除,被称为“元数据区”的区域所取代。类的元数据放入native memory, 字符串池和类的静态变量放入java堆中(静态变量之前是放在方法区)。
串池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
“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方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象
如图:
StringTable在内存紧张时,会发生垃圾回收。
(1).有些人认为方法区(如Hotspot,虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 ZGC 收集器就不支持类卸载)
(2). 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 Hotspot 虚拟机对此区域未完全回收而导致内存泄漏。
常量池中废奔的常量和不再使用的类型
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法使用了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);
}
直接内存的回收机制总结
希望大家可以多多关注编程网的其他文章!
--结束END--
本文标题: JVM入门之内存结构(堆、方法区)
本文链接: https://lsjlt.com/news/128220.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-03-01
2024-03-01
2024-03-01
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0