JVM--内存结构
JVM内存结构
一、JVM介绍
1.1JDK、JRE、JVM
Java development kit
Java runtime environment
Java virtual machine 跨平台
1.2JVM:跨语言的平台
-
Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
-
JVM平台的各种语言可以共享Java虛拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。
-
Java技术的核心就是Java虚拟机(JVM, Java Virtual Machine),因为所有的Java程序都运行在Java虛拟机内部。
-
作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
-
特点
- 一次编译,到处运行
-
自动内存管理
- 自动垃圾回收功能
-
数组下标越界检查
jvm是运行在操作系统之上的,它与硬件没有直接的交互。
1.3JVM的整体结构
二、内存结构
2.1程序计数器
定义
Program Counter Register 程序计数器(寄存器)
- 作用,是记住下一条jvm指令的执行地址
- 特点
- 是线程私有的
- 不会存在内存溢出
作用
2.2虚拟机栈
定义
Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
1.垃圾回收是否涉及栈内存?
答:不会。栈内存在方法调用结束后自动清除。
2.栈内存分配越大越好吗?
答:不是。因为栈内存变大会影响线程数量,栈越大线程越少。
3.方法内的局部变量是否线程安全?
-
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
-
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出
-
栈帧过多导致栈内存溢出
-
栈帧过大导致栈内存溢出
通过设置-Xss来配置栈内存大小。
栈溢出案例
public class stack {
public static void main(String[] args) throws JsonProcessingException {
Dept dept = new Dept("开发部");
Emp emp1 = new Emp("张三", dept);
Emp emp2 = new Emp("李四", dept);
dept.addEmp(emp1);
dept.addEmp(emp2);
dept.setEmps(Arrays.asList(emp1, emp2));
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(dept));
}
}
class Emp {
private String name;
private Dept dept;
public Emp(String name, Dept dept) {
this.name = name;
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;
public Dept(String name) {
this.name = name;
this.emps = new ArrayList<>();
}
public void addEmp(Emp emp) {
emps.add(emp);
}
public void set(String name) {
this.name = name;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
上述代码会出现栈溢出错误:
原因是:
dept.setEmps(Arrays.asList(emp1, emp2));
emp中含有dept会出现:一直无限往里套娃!
{name:'研发部',emps;[{name:'张三',dept:{name:'',emps:[{}]}}]}
为了避免出现这样的问题,需要在emp的dept字段加上@JsonIgnore
线程运行诊断
案例1:cpu占用过多(Linux环境下)
定位:
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id
- 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
2.3本地方法栈
为本地方法提供内存空间。
2.4堆
定义
Heap 堆
- 通过 new 关键字,创建对象和数组都会使用堆内存
- 特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆的组成
- 新生代(Young Generation):新生代分为Eden Space和Survivor Space。在Eden Space中, 大多数新创建的对象首先存放在这里。Eden区相对较小,当Eden区满时,会触发一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分为两个相等大小的区域,称为S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下来的对象会被移动到其中一个Survivor空间,以继续它们的生命周期。这两个区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。
- 老年代(Old Generation/Tenured Generation):存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此Major GC(也称为Full GC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor GC长。老年代的空间通常比新生代大,以存储更多的长期存活对象。
- 元空间(Metaspace):从Java 8开始,永久代(Permanent Generation)被元空间取代,用于存储类的元数据信息,如类的结构信息(如字段、方法信息等)。元空间并不在Java堆中,而是使用本地内存,这解决了永久代容易出现的内存溢出问题。
- 大对象区(Large Object Space / Humongous Objects):在某些JVM实现中(如G1垃圾收集器),为大对象分配了专门的区域,称为大对象区或Humongous Objects区域。大对象是指需要大量连续内存空间的对象,如大数组。这类对象直接分配在老年代,以避免因频繁的年轻代晋升而导致的内存碎片化问题。
堆内存溢出
使用-Xmx8m进行堆空间大小修改
堆内存诊断
- 案例
垃圾回收后,内存占用仍然很高,使用jvisualvm进行可视化的查看。
2.5方法区
方法区中方法执行过程
当程序中通过对象或类直接调用某个方法时,主要包括以下几个步骤:
- 解析方法调用:JVM会根据方法的符号引用找到实际的方法地址(如果之前没有解析过的话)。
- 栈帧创建:在调用一个方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 执行方法:执行方法内的字节码指令,涉及的操作可能包括局部变量的读写、操作数栈的操作、跳转控制、对象创建、方法调用等。
- 返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境。
组成
《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
- 类信息:包括类的结构信息、类的访问修饰符、父类与接口等信息。
- 常量池:存储类和接口中的常量,包括字面值常量、符号引用,以及运行时常量池。
- 静态变量:存储类的静态变量,这些变量在类初始化的时候被赋值。
- 方法字节码:存储类的方法字节码,即编译后的代码。
- 符号引用:存储类和方法的符号引用,是一种直接引用不同于直接引用的引用类型。
- 运行时常量池:存储着在类文件中的常量池数据,在类加载后在方法区生成该运行时常量池。
- 常量池缓存:用于提升类加载的效率,将常用的常量缓存起来方便使用。
方法区内存溢出
jdk 1.8前导致永久代内存溢出:这里不在演示
jdk 1.8之后导致原空间内存溢出
场景:
- spring
- mybatis
运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
StringTable(字符串常量池)
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; // 一开始存在常量池,用到才会创建对象,懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
//s3 != s4
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab 上边s4是两个变量拼接,可以变,所以不确定
System.out.println(s3 == s5);
}
分析:使用 javap -v 类名.class查看常量池文件
StringBuilder中的toString的方法:相当于调用了new String()方法。
StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern() 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
// ["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")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == x); //true 串池中的"ab"
System.out.println( s == x ); //false 堆中的对象 跟 串池中的并不相等
}
//StringTable面试题
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab StringTable{"a","b","ab"};
String s4 = s1 + s2; // new String("ab") 放在了堆中
String s5 = "ab"; // 串中有,不会新建了
String s6 = s4.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,
// 会把串池中的对象返回
// 问
System.out.println(s3 == s4); // false 因为堆中的对象跟串中的对象不相等
System.out.println(s3 == s5); // true 都是串池中的对象
System.out.println(s3 == s6); // true 都是串池中的对象
String x2 = new String("c") + new String("d"); // new String("cd") 放到堆中
x2.intern(); // 尝试放入串池,如果串池中有,则不会放入,如果没有,则放入串池,返回串池中的对象
String x1 = "cd"; // 串中有,不会新建了
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2); // true
}
StringTable位置
在jdk1.6时候,位于常量池中,常量池位于永久代中,内存空间不足导致永久代空间不足。
在jdk1.8以后,位于堆中。
/**
* 演示 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());//放到StringTable中
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
堆空间不足。
StringTable垃圾回收机制
/**
* 演示 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性能调优
StringTable的底层相当于HashTable。可以调整桶的大小来改变StringTable的存储速度。
/**
* 演示串池大小对性能的影响
* -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()
/**
* 演示 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();
}
}
2.6直接内存
定义
Direct Memory(直接内存)
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
直接内存溢出
/**
* 演示直接内存溢出
*/
public class Demo1_10 {
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
// jdk8 对方法区的实现称为元空间
}
}
直接内存释放原理
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
/**
* 直接内存分配的底层原理:Unsafe
*/
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
ByteBuffer底层源码:
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;
}
//调用了 setMemory方法存储
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 = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
//Deallocator中的run方法
public void run() {
if (address == 0) {
// Paranoia
return;
}
//回收数据
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
/*
关闭显示的垃圾回收
* -XX:+DisableExplicitGC 显式的
*/
原文地址:https://blog.csdn.net/m0_51275144/article/details/144437751
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!