Java虚拟机在运行Java程序时,把所管理的内存分为多个区域, 这些区域就是运行时数据区
运行时数据区可以分为:程序计数器,Java虚拟机栈,本地方法栈,堆和方法区
Program Counter Register 程序记数寄存器
pc寄存器保存下一条要执行的字节码指令地址
执行引擎根据pc寄存器找到对应字节码指令来使用当前线程中的局部变量表(取某个值)或操作数栈(入栈,出栈..)又或是将字节码指令翻译成机器指令,然后由CPU进行运算
Java Virtual Mechine Stack
因为是线程私有的,所以随着线程的创建而创建,随着线程的消亡而消亡
"栈"通常情况指的就是JVM栈,更多情况下 "栈"指的是JVM栈中的局部变量表
局部变量表中的存储空间以 局部变量槽(Slot) 来表示, double和long 64位的用2个槽来表示,其他数据类型都是1个
内存空间是在编译期间就已经确定的,运行时不能更改
这里的局部变量槽真正的大小由JVM来决定
结构图
栈帧是Java虚拟机栈中的数据结构
Java虚拟机栈又是属于线程私有的
调用方法和方法结束 可以看作是 栈帧入栈,出栈操作
Java虚拟机以方法作为最基本的执行单位
每个栈帧中包括: 局部变量表,操作数栈,栈帧信息(返回地址,动态连接,附加信息)
从Java程序来看:在调用堆栈的所有方法都同时处于执行状态(比如:main方法中调用其他方法)
从执行引擎来看:当前线程只有处于栈顶的栈帧才是当前栈帧,此栈帧对应的方法为当前方法,执行引擎所运行的字节码指令只针对当前栈帧*也就是执行引擎执行的字节码指令只针对栈顶栈帧(方法)*
public void add(int a){
a=a+2;
}
public static void main(String[] args) {
new Test().add(10);
}
局部变量表用于存放方法中的实际参数
和方法内部定义的变量
(存储)
以局部变量槽为单位(编译期间就确定了)
每个局部变量槽都可以存放byte,short,int,float,boolean,reference,returnAddress
byte,short,char,boolean在存储前转为int (boolean:0为false 非0为true)
而double,long
由 两个局部变量槽存放
每个局部变量槽的真正大小应该是由JVM来决定的
reference 和 returnAddress 类型是什么
Java虚拟机通过定位索引
的方式来使用局部变量表
局部变量表的范围: 0~max_locals-1
比如: 我们上面代码中add()方法只有一个int参数,也没有局部变量,为什么最大变量槽数量为2呢?
实际上: 默认局部变量槽中索引0的是方法调用者的引用(通过"this"可以访问这个对象)
其余参数则按照申明顺序在局部变量槽的索引中
槽的复用:如果PC指令申明局部变量(j)已经超过了某个局部变量(a)的作用域,那么j就会复用a的slot
max_stack
操作数栈的最大深度也是编译时就确定下来了的
在方法执行的时候(字节码指令执行),会往操作数栈中写入和提取内容(比如add方法中a=a+2
,a入栈,常数2入栈,执行相加的字节码指令,它们都出栈,然后把和再入栈)
操作数栈中的数据类型必须与字节码指令匹配(比如 a=a+2
都是Int类型的,字节码指令应该是iadd
操作int类型相加,而不能出现不匹配的情况)
这是在类加载时验证阶段的字节码验证过程需要保证的
动态连接:栈帧中指向运行时常量池所属方法的引用
静态解析与动态连接
符号引用转换为直接引用有两种方式
执行方法后,有两种方式可以退出
正常调用完成与异常调用完成
增加一些《Java虚拟机规范》中没有描述的信息在栈帧中(取决于具体虚拟机实现)
关于栈的两种异常
测试StackeOverflowError
另外在hotSpot虚拟机中不区分虚拟机栈和本地方法栈,所以-Xoss
无效,只有-Xss
设置单个线程栈的大小
/**
* @author Tc.l
* @Date 2020/10/27
* @Description: 测试栈溢出StackOverflowError
* -Xss:128k 设置每个线程的栈内存为128k
*/
public class StackSOF {
private int depth=1;
public void recursion(){
depth++;
recursion();
}
public static void main(String[] args) throws Throwable {
StackSOF sof = new StackSOF();
try {
sof.recursion();
} catch (Throwable e) {
System.out.println("depth:"+sof.depth);
throw e;
}
}
}
/*
depth:1001
Exception in thread "main" java.lang.StackOverflowError
at 第2章Java内存区域与内存溢出.StackSOF.recursion(StackSOF.java:12)
at 第2章Java内存区域与内存溢出.StackSOF.recursion(StackSOF.java:13)
...
at 第2章Java内存区域与内存溢出.StackSOF.recursion(StackSOF.java:13)
at 第2章Java内存区域与内存溢出.StackSOF.main(StackSOF.java:19)
*/
减小了栈内存的空间,又递归调用频繁的创建栈帧,很快就会超过栈内存,从而导致StackOverflowError
测试OOM
在我们经常使用的hotSpot虚拟机中是不支持栈扩展的
所以线程运行时不会因为扩展栈而导致OOM,只有可能是创建线程无法申请到足够内存而导致OOM
/**
* @author Tc.l
* @Date 2020/10/27
* @Description: 测试栈内存溢出OOM
* -Xss2m 设置每个线程的栈内存为2m
*/
public class StackOOM {
public void testStackOOM(){
//无限创建线程
while (true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//让线程活着
while (true) {
}
}
});
thread.start();
}
}
public static void main(String[] args) {
StackOOM stackOOM = new StackOOM();
stackOOM.testStackOOM();
}
}
/*
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at 第2章Java内存区域与内存溢出.StackOOM.testStackOOM(StackOOM.java:19)
at 第2章Java内存区域与内存溢出.StackOOM.main(StackOOM.java:25)
*/
操作系统为(JVM)进程分配的内存大小是有效的,这个内存再减去堆内存,方法区内存,程序计数器内存,直接内存,虚拟机消耗内存等,剩下的就是虚拟机栈内存和本地方法栈内存
此时增加了线程分配到的栈内存大小,又在无限建立线程,就很容易把剩下的内存耗尽,最终抛出OOM
如果是因为这个原因出现的OOM,创建线程又是必要的,解决办法可以是减小堆内存和减小线程占用栈内存大小
Native Method Stacks
与JVM栈作用类似
JVM栈为Java方法服务
本地方法栈为本地方法服务
内存溢出异常也与JVM栈相同
hotspot将本地方法栈和Java虚拟机栈合并
去永久代
,将静态常量池移到堆中(字符串常量池也是)元空间:逻辑上存在堆,物理上不存在堆(使用本地内存)
GC垃圾回收主要在伊甸园区,老年区
指令 | 作用 |
---|---|
-Xms | 设置初始化内存大小 默认1/64 |
-Xmx | 设置最大分配内存 默认1/4 |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
-XX:NewRatio = 2 | 设置老年代占堆内存比例 默认新生代:老年代=1:2(新生代永远为1,设置的值是多少老年代就占多少) |
-XX:SurvivorRatio = 8 | 设置eden与survivor内存比例 文档上默认8:1:1实际上6:1:1(设置的值是多少eden区就占多少) |
-Xmn | 设置新生代内存大小 |
-XX:MaxTenuringThreshold | 设置新生代去老年代的阈值 |
-XX:+PrintFlagsInitial | 查看所有参数默认值 |
-XX:+PrintFlagsFinal | 查看所有参数最终值 |
public class HeapTotal {
public static void main(String[] args) {
//JVM试图使用最大内存
long maxMemory = Runtime.getRuntime().maxMemory();
//JVM初始化总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("JVM试图使用最大内存-->"+maxMemory+"KB 或"+(maxMemory/1024/1024)+"MB");
System.out.println("JVM初始化总内存-->"+totalMemory+"KB 或"+(totalMemory/1024/1024)+"MB");
/*
JVM试图使用最大内存-->2820669440KB 或2690MB
JVM初始化总内存-->191365120KB 或182MB
*/
}
}
默认情况下 JVM试图使用最大内存是电脑内存的1/4 JVM初始化总内存是电脑内存的1/64 (电脑内存:12 G)
使用-Xms1024m -Xmx1024m -XX:+PrintGCDetails 执行HeapTotal
JVM试图使用最大内存-->1029177344B 或981MB
JVM初始化总内存-->1029177344B 或981MB
Heap
PSYoungGen total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 6% used [0x00000000eab00000,0x00000000eba5c420,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3180K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
最好-Xms初始化分配内存与-Xmx最大分配内存一致,因为扩容需要开销
为什么明明设置的是1024m 它显示使用的是981m?
因为幸存from,to区采用复制算法,总有一个幸存区的内存会被浪费
年轻代内存大小 = eden + 1个幸存区 (305664 = 262144 + 43520)
堆内存大小 = 年轻代内存大小 + 老年代内存大小 (305664 + 699392 = 1005056KB/1024 = 981MB)
所以说: 元空间逻辑上存在堆内存,但是物理上不存在堆内存
因为堆是存放对象实例的地方,所以只需要不断的创建对象
并且让GC Roots
到各个对象间有可达路径来避免清除这些对象(因为用可达性分析算法来确定垃圾)
最终就可以导致堆内存没有内存再为新创建的对象分配内存,从而导致OOM
/**
* @author Tc.l
* @Date 2020/10/27
* @Description: 测试堆内存溢出
*/
public class HeapOOM {
/**
* -Xms20m 初始化堆内存
* -Xmx20m 最大堆内存
* -XX:+HeapDumpOnOutOfMemoryError Dump出OOM的内存快照
*/
public static void main(String[] args) {
ArrayList<HeapOOM> list = new ArrayList<>();
while (true){
list.add(new HeapOOM());
}
}
}
/*
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid17060.hprof ...
Heap dump file created [28270137 bytes in 0.121 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at 第2章Java内存区域与内存溢出.HeapOOM.main(HeapOOM.java:20)
*/
解决这个内存区域的异常的常用思路:
Method Area
也和堆一样可以固定内存也可以扩展
因为方法区的主要责任是用于存放相关类信息,只需要运行时产生大量的类让方法区存放,直到方法区内存不够抛出OOM
使用CGlib操作字节码运行时生成大量动态类
导入CGlib依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.3.0</version>
</dependency>
/*
* -XX:MaxMetaspaceSize=20m 设置元空间最大内存20m
* -XX:MetaspaceSize=20m 设置元空间初始内存20m
*/
public class JavaMethodOOM {
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(JavaMethodOOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
}
/*
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
... 11 more
*/
很多主流框架(Spring)对类增强时都会用到这类字节码技术
所以增强的类越多,存放在方法区就越容易溢出
Runtime Constant Pool
符号引用:#xx 会指向常量池中的一个直接引用(比如类引用Object)
并且会把符号引用翻译成直接引用保存在运行时常量池中
运行时也可以将常量放在运行时常量池(String的intern方法)
运行时常量池中,绝大部分是随着JVM运行,从常量池中转化过来的,还有部分可能是通过动态放进来的(String的intern)
Direct Memory
直接内存不是运行时数据区的一部分,因为这部分内存被频繁使用,有可能导致抛出OOM
Java1.4加入了NIO类,引入了以通道传输,缓冲区存储的IO方式
它可以让本地方法库直接分配物理内存,通过一个在Java堆中DirectByteBuffer
的对象作为这块物理内存的引用进行IO操作 避免在Java堆中和本地物理内存堆中来回copy数据
直接内存分配不受Java堆大小的影响,如果忽略掉直接内存,使得各个内存区域大小总和大于物理内存限制,扩展时就会抛出OOM
public class LocalMemoryTest {
private static final int BUFFER = 1024 * 1024 * 1024 ;//1GB
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("申请了1GB内存");
System.out.println("输入任意字符释放内存");
Scanner scanner = new Scanner(System.in);
scanner.next();
System.out.println("释放内存成功");
buffer=null;
System.gc();
while (!scanner.next().equalsIgnoreCase("exit")){
}
System.out.println("退出程序");
}
}
默认直接内存与最大堆内存一致
-XX:MaxDirectMemorySize
可以修改直接内存
使用NIO中的DirectByteBuffer
分配直接内存也会抛出内存溢出异常,但是它抛出异常并没有真正向操作系统申请空间,只是通过计算内存不足,自己手动抛出的异常
真正申请分配直接内存的方法是Unsafe::allocateMemory()
/* 测试直接内存OOM
* -XX:MaxDirectMemorySize=10m
* -Xmx20m
*/
public class DirectMemoryOOM {
static final int _1MB = 1024*1024;
public static void main(String[] args) throws IllegalAccessException {
Field declaredField = Unsafe.class.getDeclaredFields()[0];
declaredField.setAccessible(true);
Unsafe unsafe =(Unsafe) declaredField.get(null);
while (true){
unsafe.allocateMemory(_1MB);
}
}
}
由直接内存出现的OOM的明显特征就是:Dump 堆快照中,没有什么明显的异常
如果是这种情况,且使用了NIO的直接内存可以考虑这方面的原因
本地方法: 关键字native
修饰的方法,Java调用非Java代码的接口
注意: native
不能和abstract
一起修饰方法
为什么需要本地方法
本地方法很少了,部分都是与硬件有关(比如启动线程start0()
)
只是部分虚拟机支持本地方法
本地方法接口
本地方法通过本地方法接口来访问虚拟机中的运行时数据区
某线程调用本地方法时,它就不受虚拟机的限制,在OS眼里它和JVM有同样权限
可以直接使用本地处理器中的寄存器,直接从本地内存分配任意内存
本地方法库
本地方法栈中登记native
修饰的方法,由执行引擎来加载本地方法库
本片文章详细说明jvm运行时内存区域以及可能发生的内存溢出异常
线程私有的程序计数器保存要执行的字节码指令,程序计数器不会发生内存溢出异常
线程私有的栈服务于方法,每个方法代表一个栈帧,方法的调用与调用结束标志着栈帧的入栈与出栈,栈帧中的局部变量表、操作数栈、方法返回地址、动态连接(运行时常量池引用)、附加信息是为了帮助更好的服务方法,栈;hospot虚拟机中栈可能存在栈溢出异常(递归调用无终止条件),也可能存在内存溢出异常(当创建线程无法分配内存时)
线程私有的本地栈与栈的区别就是,本地栈用于服务本地方法
线程公有的堆服务于存储对象,大部分对象都存储在堆中,堆内存划分为新生代(伊甸园区、幸存0区、1区)、老年代、元空间(直接内存),对象生命周期长短不一,新生代与老年代GC时使用的算法也不同;当堆内存不足,无法给新对象分配内存时,发生内存溢出异常(堆内存OOM需要排查的是到底是内存不足还是发生了内存泄漏)
线程公有的方法区存储类相关信息和运行时常量池,运行时常量池存放常量和引用,符号引用和动态连接会指向运行时常量池的直接引用;如果大量加载类信息,方法区也会发生内存溢出异常
直接内存也有可能发生内存溢出异常,当发生内存溢出异常时,堆内存没有异常可能是直接内存的原因
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。