JVM内存结构

05月07日 收藏 0 评论 1 java开发

JVM内存结构

转载声明:文章来源https://blog.csdn.net/weixin_47407737/article/details/122814228

1、程序计数器

作用:记住下一条要执行的二进制字节码指令地址(经过解释器翻译成机器码交由CPU运行)

特点:线程私有的;不会存在内存溢出。

2、虚拟机栈

2.1 定义

每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

问题辨析:

1.垃圾回收是否涉及栈内存

不涉及,栈帧在每次方法执行完毕会自动排出栈,即被回收。

2.栈内存分配越大越好吗

可通过-Xss 改变栈内存分配大小;栈内存分配大了更多的是能够执行的方法更多了,那么相对的能够分配给线程的栈数量就减少了。

3.方法内的局部变量是否线程安全

其实就是看变量是共享还是私有的

①如果方法内局部变量没有逃离方法的作用访问,即私有的,则是线程安全;
②如果局部变量引用了对象,并逃离了方法的作用,需要考虑线程安全

2.2 栈内存溢出

栈帧过多导致溢出 --递归调用 StackOverFLOWError

栈帧过大导致溢出 – 数据的循环引用(A类的成员变量有B类,B类的成员变量有A类)

2.3 线程运行诊断

①CPU占用过多:

用top定位哪个进程对cpu的占用过高

ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)

jstack 进程id (可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号)

②程序运行很长时间都没得到结果

很可能是发生了线程死锁 deadlock,如两个线程循环等待对方释放资源

3、本地方法栈

用c/c++语言编写的,java代码通过本地方法native间接的调用实现,如clone()、hashcode(); 查看java源码 用native修饰的;当调用native方法时,所需内存就是在本地方法栈分配。

4、堆

4.1 定义

通过new关键字,创建对象都会使用堆内存;

特点:

①它是线程共享的,堆中对象都需要考虑线程安全问题

②有垃圾回收机制

4.2 堆内存溢出

可通过-Xmx 改变堆内存大小

如果堆中对象没有引用就会被垃圾回收,那么如果一直产生新的对象,并且其他对象也一直被引用着,就可能会发生堆内存溢出。

//内存溢出例子
public static void main(String[] args){
int i = 0;
try{
List<String> list = new ArrayList<>();
String a ="hello";
while(true){
list.add(a);
a = a+a;
i++;
}
}catch(Throwable e){
e.printStackTrace();
System.out.println(i);
}

}

4.3 堆内存诊断

①jps工具

jps 命令

查看当前系统有哪些java进程

②jmap工具

jmap -heap 进程id 命令

查看堆内存占用情况

③jconsole工具

图形界面,多功能的检测工具,可以连续监测

案例:垃圾回收后,内存占用仍然很高

jvisualvm工具 通过堆转储 dump查看哪些对象占用问题

pubilc static void main(String[] args) throws InterruptedException{
List<Student> list = new ArrayList<>();
for(int i = 0; i < 200; i++)
list.add(new Student);

}
class Student{
private byte[] big = new byte[1024*1024];
}

5、方法区(元空间)

5.1 定义

存储每个类的构造信息,譬如运行时的常量池,字段,方法数据以及方法和构造方法的代码,包含一些在类和实例初始化和接口初始化时候使用的特殊方法。

5.2 内存溢出

演示内存溢出 通过-XX:MaxMetspaceSize=8m 改变元空间大小

public class Demo extends ClassLoader{//可以用来加载类的二进制字节码
public static void static (String[] args){
int j = 0;
try{
Demo test = new Demo();
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[] code = cw.toByteArray();
//执行了类的加载
test.defineClass("Class"+i,code,0,code.length);//class对象
}finally{
System.out.println(j);
}

}
}
}

实际场景:产生很多类
spring
mybatis

5.3 运行时常量池

反编译: javap -c *.class

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

运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。


①常量池中的字符串仅是符号,第一次用到时才变为对象;
②利用串池StringTable的机制,来避免重复创建字符串对象
③字符串变量拼接的原理是StringBuilder(1.8)
④字符串常量拼接的原理是编译优化
⑤可以使用intern方法,主动将串池中还没有的字符串对象放入串池(1.8)

StringTable 位置:1.6时在方法区,1.8在堆内存。

调整-XX:StringTable=桶个数

考虑将字符串对象是否入池;如果有大量的字符串操作考虑将其入intern()串池,节约内存的使用

6、直接内存

6.1 定义

Direct Memory

①常见于NIO操作时,用于数据缓冲区
②分配回收成本较高,但读写性能高
③不受JVM内存回收管理

零拷贝机制


内存溢出

//idk6中方法区的实现称为永久代
//idk8 对方法区的实现称为元空间
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);
}
}

释放原理:

实质上使用了unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法;

ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会有ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。

C 1条回复 评论
童立

这家笔试好难

发表于 2022-12-29 21:00:00
0 0