虚拟机整体内存结构如下:
1. 内存区域
内存区域核心包含以下几部分:程序计数器、JAVA虚拟机栈、本地方法栈、方法区和堆。有的是线程级别的【一个线程会单独启动一个内存区域】,比如:程序计数器、JAVA虚拟机栈、本地方法栈。有的是虚拟机启动就存在的,不同线程共享使用。比如:堆和方法区。所有线程加载的类都在方法区中。所有的对象创建都在堆中。
(1) 程序计数器
程序计数器,可以看做当前线程锁执行的字节码的行号的指示器。指示当前线程下一条字节码指令的位置。程序计数器是线程私有的。
- 问题1:程序计数器存储线程下一条指定行号的作用是什么?
多线程执行,同一个CPU一段时间内会在不同线程之间切换执行。所以,同一个线程被CPU执行一段时间后会停止,下次再被执行的时候需要知道上一次字节码命令执行的位置,需要接着上次继续向后执行。所以,需要记录当前线程上次执行的位置。即接着执行的话,需要从那条指令接着执行。
- 问题:如果正在执行本地方法Native方法?
如果执行的是native方法,程序计数器为空。
- 问题2:为什么程序计数器是线程私有?
因为CPU在不同线程之间切换,每个线程都需要独立的记录自己执行的位置。所以需要线程私有。
(2) JAVA虚拟机栈
一个线程,会启动一个虚拟机栈。虚拟机栈随着线程的生灭而生灭。保存的是线程中所调用的方法。一个栈帧代表一个方法。方法的调用和完成调用对应栈帧的入栈和出栈。
① 栈定义
栈也叫JAVA虚拟栈。和程序计数器一样,栈也是线程私有。他和线程的生命周期一样。栈描述的是Java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧。每个方法调用到执行完成的过程,对应着一个栈帧的入栈到出栈的过程。
一个线程调用多个方法,则有多个栈帧的入栈和出栈。
② 栈帧 - Stack Frame
一个方法对应一个栈帧,调用时候到完成。对应在栈中入栈和出栈。栈帧包含以下几个部分。
栈可能出现的异常有两种:
- 线程请求的栈深度大于虚拟机允许的栈深度会抛出StackOverflowError,(虚拟机栈空间不能动态扩展的情况下)。
- 如果虚拟机栈空间可以动态扩展(目前多数的虚拟机都可以),当动态扩展无法申请到足够的空间时会抛出OutOfMemory异常。
参考:https://www.pianshen.com/article/7222697061/
1) 局部变量表-local veritable table
存储的是方法的参数和方法中定义的局部变量。在编译期间就为局部变量表分配好了内存空间。局部变量表存储3种类型的数据。用于存储方法的参数和方法过程中定义的局部变量。数据保存在局部变量表中(在方法执行的过程中,从局部变量表中复制取出,然后在操作数栈中入栈。接着使用的时候从操作数栈中弹出。进行计算,之后将结果压栈)。
局部变量表的容量以变量槽为最小单位的【Slot】。
赋值:
类变量赋值的过程有两次。在类加载的 猪呢比阶段,赋予系统初始值。另外一次是类加载的初始化阶段,赋予程序员定义的初始值。【因此,类变量初始化阶段没有未类变量赋值也没关系,类变量仍然具有一个确定的初始值】
局部变量:如果被定义,但是没有初始化,是不能执行的。编译阶段会提示。
优化点
方法栈帧长时间不结束,可是方法前部分的对象执行已经完成。因为方法栈帧未释放,造成对已经不用的对象仍然引用。是的垃圾回收器不能释放。因此,在方法中,如果前面的对象已经不被后续代码需要了,则手动设置为null。把变量对应的slot中的引用去掉。垃圾回收器会发现,当前对象已经不被引用。从而把当前对象内存进行回收。
- 基本数据类型。
- 引用类型:指向一个对象在内存中的地址。
- Return Address类型:指向指令的地址。
2) 操作数栈 - operator stack
当虚拟机执行一些指令的时候,会对操作数栈进行入栈和出栈的操作【比如iadd指令会将两个数值相加,会操作数栈将两个数据弹出-出栈,累加之后再压入栈中】--能否理解为存放中间结果数据的。
方法计算的过程中,会从局部变量表中获取数据、多个需要计算的数据入栈。然后从操作数栈弹出需要计算的数据,运算结束后,将结果压入栈中。所以,操作数栈的功能是,将计算的数据入栈,计算之前出栈,计算结构保存在栈中。
保存参数和中间结果。
3) 动态链接 -dynamic linking
理解需要加强
一个栈帧代表一个方法。栈帧中有一个指向运行时常量池中当前栈帧所属方法的一个引用。这个引用目标是支持调用过程中使用动态链接。
class文件的常量池中有大量的“符号引用”,字节码中的方法调用指令 以“符号引用”为参数。
静态链接:这些引用一部分会在类加载阶段或者第一次使用的时候转化成直接引用。这种转换称之为“静态引用”。
动态链接:在每次运行期间,转成直接引用。这部分称之为“动态链接”。
4) 方法返回地址-return value
方法执行完成,可能正常退出【返回的字节码指令】,也可能异常退出。
无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时需要在栈帧中保存一些数值。供上层方法使用。
(3) 本地方法栈
本地方法栈和虚拟机栈功能类似。区别是虚拟机栈是为虚拟机执行java方法服务。本地方法栈为了虚拟机执行本地方法【Native】服务。而Hotspot虚拟机将java虚拟栈和本地方法栈合二为一。
(4) 堆
Java堆是所有线程共享的一块内存区域。虚拟机启动的时候就创建。目的是存放实例。
分类:分为新生代【Eden区,From survivor 和 To Survivor】和老年代。
(5) 方法区
方法区是所有线程共享的区域。它用来存储被虚拟机加载的类信息、常量、静态方法、即时编译器编译后的代码等数据。在Hotspot虚拟机中,也称之为“永久代”。方法区包含“运行时常量池”。
(6) 运行时常量池
运行时常量池是方法区的一部分。用于存放编译生成的各种字面量和符号引用。这部分内容将在 类加载后 存入方法区的“运行时常量池”中。运行期间,也可能将新的常量放入常量池中。
(7) 直接内存
直接内存不是java虚拟机的一部分。但是,也在java运行期间被使用。也可能导致OutOfMemory。
2. 内存对象的操作
(1) 对象创建
核心为两步:其一:确保类加载到方法区,如果没有加载,先类加载。其二:在heap堆的新生代中的Eden区中分配区域,创建类,之后初始化。
(2) 对象内存布局
① 对象头:两部分:运行时数据【Hash码、GC分代年龄】、类型指针【内存中对象是哪个类的实例】。
② 实例数据:类真实数据。
③ 填充对齐:如果实例数据不是8byte的整数倍,需要使用填充对其补齐。
(3) 对象的访问
① 直接访问:JAVA栈中局部变量变中reference类型保存的是内存对象在heap中的地址。【Hotspot虚拟机使用的是直接引用】优点【访问的时候,少一次移动】
② 句柄访问:JAVA栈中局部变量变中reference类型保存的是对象的句柄。【在heap中有句柄池和对象池,对象在对象池中创建。句柄池中的句柄指向具体的对象,在java栈中保存的是对象的句柄】优点【垃圾回收的时候,使用中的对象被频繁移动。使用句柄的方式,java栈中的引用类型不需要修改】
3. 常见的内存溢出
(1) Java堆溢出
内存溢出会提示Java heap space 。核心是需要区分内存泄露还是内存溢出。
分析手段:对转储快照进行分析。重点确认是内存泄露还是内存溢出。如果不需要的对象大量的存在而没有被回收器回收则是【代码问题对象未被及时回收】内存泄露。如果内存中的对象都是需要的,则为【内存太小而不能满足代码运行】内存溢出。
处理思路
内存泄露:根据GCroot引用链分析,在哪里需要及时释放对象引用,从而让垃圾回收器及时回收对象,释放内存空间。
内存溢出:(1).调整java虚拟机内存 -xmx -xms 。(2).分析对象是否存在生命周期过长的情况,尝试减少运行期的内存消耗。
(2) 虚拟机栈和本地方法栈方法溢出
StackOverFlow :两种情况实际上都会报这个错误。1.栈的深度大于虚拟机所允许的最大深度。2.JAVA虚拟机栈扩展时无法申请到足够的空间。
一般情况,在多线程场景下容易出现,每个线程都会申请java虚拟机栈,建立过多的线程会导致内存溢出。不能满足建立指定多的线程。
处理思路:
Java内存核心分为3部分。堆、方法区和栈。如果栈内存不够,也可以通过设置减小堆和减小栈容量来换取更多的线程。
(3) 方法区和运行时常量池溢出
运行时常量池溢出,会提示“PermGen space”
动态代理等字节码增强技术会在运行时动态生成类,方法区需要将类加载到方法区。如果生成的类比较多,会造成方法区和运行时常量池溢出。
(4) 本地直接内存溢出
通常在NIO操作时候,可能会造成直接内存的溢出。因而,如果直接内存溢出可以多关注NIO的操作部分。