Java 虚拟机栈:Java Virtual Machine Stacks,每个线程运行时所需要的内存
每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(一个方法一个栈帧)
Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的
虚拟机栈是每个线程私有的,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着:
局部变量表:存储方法里的 Java 基本数据类型以及对象的引用
动态链接:也叫指向运行时常量池的方法引用
方法返回地址:方法正常退出或者异常退出的定义
操作数栈或表达式栈和其他一些附加信息
设置栈内存大小:-Xss size``-Xss 1024k
虚拟机栈特点:
栈内存不需要进行GC,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据
栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大)
方法内的局部变量是否线程安全:
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析)
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
异常:
局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
局部变量表最基本的存储单元是 slot(变量槽):
栈:可以使用数组或者链表来实现
操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)
栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率
基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术
动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定
[外链图片转存中…(img-40Zfh1tb-1658977927894)]
常量池的作用:提供一些符号和常量,便于指令的识别
[外链图片转存中…(img-yDv5SyPY-1658977927895)]
Return Address:存放调用该方法的 PC 寄存器的值
方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置
正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者
异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出
两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值
栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息
本地方法栈是为虚拟机执行本地方法时提供服务的
JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植
不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常
虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一
本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序
当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
直接从本地内存的堆中分配任意数量的内存
可以直接使用本地处理器中的寄存器
图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md
Program Counter Register 程序计数器(寄存器)
作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空)
原理:
特点:
Java 反编译指令:
javap -v Test.class
#20:代表去 Constant pool 查看该地址的指令
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题
存放哪些资源:
对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存
字符串常量池:
字符串常量池原本存放于方法区,JDK7 开始放置于堆中
字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table
静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率
设置堆内存指令:-Xmx Size
内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常
堆内存诊断工具:(控制台命令)
jhsdb jmap --heap --pid 进程id
在 Java7 中堆内会存在年轻代、老年代和方法区(永久代):
分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能
public static void main(String[] args) {
// 返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回Java虚拟机使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M
System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M
}
方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式
方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)
方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现
为了避免方法区出现 OOM,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中
类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表
常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的字面量、符号引用,JVM 为每个已加载的类维护一个常量池
运行时常量池是方法区的一部分
虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM
本地内存:又叫做堆外内存,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 Java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM
本地内存概述图:
PermGen 被元空间代替,永久代的类信息、方法、常量池等都移动到元空间区
元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制
方法区内存溢出:
-XX:MaxPermSize=8m #参数设置
-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);
}
}
}
直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域
直接内存详解参考:NET → NIO → 直接内存
变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置
静态内部类和其他内部类:
类变量:
实例变量:
局部变量:
类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?
什么是字面量?什么是符号引用?
int a = 1; //这个1便是字面量
String b = "iloveu"; //iloveu便是字面量
system.in.read()
cpu 占用过多
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
- 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是 StringBuilder (1.8)
字符串常量拼接的原理是编译期优化
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
查看GC机制:jmap -dump:format=b,live,file=文件名.bin 进程ID
被引用计数+1 如果某个变量不再引用它就-1 变为0则可以被回收**缺点:**循环引用
JVM中的垃圾回收期是采用可达性分析算法来探索所有存活的对象:扫描堆中的对象看是否能够沿着GC Root对象作为起点为引用链找到该对象,若找不到则可以回收(本ROOT对象直接或间接引用的对象不可以被回收)
局部变量引用的对象可以作为根对象
方法参数可以作为跟对象
ROOT对象的分类:系统类,本地方法(操作系统的方法),运行的线程,以及锁
强引用,软引用,弱引用,虚引用,终结器引用
当被软引用和弱引用创建的时候分配了引用队列,那么当软弱引用引用的对象被回收时,软弱引用将会进入到该引用队列中,(优点:软弱引用也需要占用一定的内存,如果需要操作软弱引用占的内存,那就需要用引用队列来找到这两个引用)
虚引用与终结器引用创建时必须要关联引用队列来使用
引用队列
//引用队列
RederenceQueue<byte[]> queue = new RederenceQueue<byte[]>;
//关联了引用队列,当软引用所关联的byte[]被回收时,软引用会自己加入到queue中
SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[1024*4*1024],queue);
Reference<? extends byte[]> poll = queue.poll();
while(poll != null){
list.remove(poll);
poll = queue.poll();
}
优点:垃圾回收的速度快 缺点:空间不连续,会产生内存碎片
三种垃圾回收算法,视情况而定---------------------------------->分代回收
新生代 ---- > 垃圾回收执行的比较频繁 Minor GC
伊甸园
幸存区From
幸存区 To
老年代 — >垃圾回收执行的频率比较低 Full GC
对象首先分配在伊甸园区域
新生代空间不足时,触发minor gc,伊甸园和from存活的对象copy到to中,存活的对象年龄也要加1,并且交换from to
minor gc 会引发 stop the world(STW暂停其他用户线程),等垃圾回收结束,用户线程才恢复运行
当对象寿命超过阈值时,会晋升值老年代,最大寿命是15(4bit)
当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc(STW的时间更长)如果full gc执行后空间还是不足的话就会报:内存溢出 java.lang.OutOfMemoryError: PermGen space
----------------------->分代回收
新生代 ---- > 垃圾回收执行的比较频繁 Minor GC
伊甸园
幸存区From
幸存区 To
老年代 — >垃圾回收执行的频率比较低 Full GC
对象首先分配在伊甸园区域
新生代空间不足时,触发minor gc,伊甸园和from存活的对象copy到to中,存活的对象年龄也要加1,并且交换from to
minor gc 会引发 stop the world(STW暂停其他用户线程),等垃圾回收结束,用户线程才恢复运行
当对象寿命超过阈值时,会晋升值老年代,最大寿命是15(4bit)
当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc(STW的时间更长)如果full gc执行后空间还是不足的话就会报:内存溢出 java.lang.OutOfMemoryError: PermGen space
对于OopMap与安全点和安全区域与RememberedSet 理解请看我的这篇Blog:
OopMap与安全点和安全区域与RememberedSet 理解
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/qq_47109099/article/details/126030779
内容来源于网络,如有侵权,请联系作者删除!