内存管理篇
对象的创建可以分为五个步骤:检查类加载,分配内存,初始化零值,设置对象头,执行实例构造器
1 HotSpot虚拟机遇到一条new指令,会先检查能否在常量池中定位到这个类的符号引用,检查这个类是否类加载过
1 对象所需的内存在类加载完成后就可以完全确定。
2 分配内存方式:虚拟机在堆上为新对象分配内存,有两种内存分配的方式:指针碰撞,空闲列表。
指针碰撞:
使用场景: 堆内存规整整齐。
过程: 使用过的空间放在一边,空闲的空间放在另一边,中间有一个指针作为分界点指示器,把新生对象放在使用过空间的那一边,中间指针向空闲空间那边挪动一个新生对象的内存大小的距离即可。
特点:简单,高效,因为要堆内存规整整齐,所以垃圾收集器应该要有压缩整理的能力。
空闲列表:
使用场景: 已使用空间和空闲空间交错在一起。
过程: 虚拟机维护一个列表,列表中记录了哪些内存空间可用,分配时找一块足够大的内存空间划分给新生对象,然后更新列表。
特点: 比指针碰撞复杂, 但是对垃圾收集器可以不用压缩整理的能力。
3 分配内存流程
分配内存流程(栈 ==> 老年代 ==> TLAB ==> Eden)
因为在堆上为对象分配内存,内存不足会引起GC,引起GC可能会有STW(Stop The World)影响响应,为了优化减少GC,当对象不会发生逃逸(作用域只在方法中,不会被外界调用)且栈内存足够时,直接在栈上为对象分配内存,当线程结束后,栈空间被回收,(局部变量也被回收)就不用进行垃圾回收了。
开启逃逸分析-XX:+DoEscapeAnalysis
满足条件的对象就在栈上分配内存 (当对象满足不会逃逸条件除了能够优化在栈上分配内存还会带来锁消除,标量替换等优化…)。
TLAB 本地线程分配缓存
原因:堆内存是线程共享的,并发情况下从堆中划分线程内存不安全(正在给A对象分配内存,指针还未修改,使用原来的指针为对象B分配内存),如果直接加锁会影响并发性能。
解决:虚拟机采用TLAB(Thread Local Allocation Buffer本地线程分配缓冲)和CAS+失败重试来保证线程安全。
原理:为每一个线程预先在伊甸园区(Eden)分配一块内存,JVM给线程中的对象分配内存时先在TLAB分配,直到对象大于TLAB中剩余的内存或TLAB内存已用尽时才需要同步锁定(也就是CAS+失败重试)。
CAS+失败重试:采用CAS配上失败重试的方式保证更新操作的原子性。
哪个线程要分配内存就在那个线程的缓冲区上分配,只有缓冲区满了,不够了才使用乐观的同步策略(CAS+失败重试)保证分配内存的原子性。
1 分配内存完成后,虚拟机将分配的内存空间初始化为零值(不包括对象头) (零值: Integer对应0等)
2 保证了对象的成员字段(成员变量)在Java代码中不赋初始值就可以使用
1 把一些信息(这个对象属于哪个类? 对象哈希码,对象GC分代年龄)存放在对象头中 (后面详细说明对象头)
1 init方法 = 实例变量赋值 + 实例代码块 + 实例构造器 (按照我们自己的意愿进行初始化)
1 对象在堆中的内存布局可以分为三个部分:对象头,实例数据,对齐填充
对象头包括两类信息(8Byte + 4Byte):
实例数据是对象真正存储的有效信息:
+XX:CompactFields
)。对齐填充:
Object obj = new Object(); 占多少字节?
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core --> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.12</version> </dependency>
int[] ints = new int[5]; //占多少内存?
mark word:8 byte
类型指针: 4 byte
数组长度: 4 byte
数组内容初始化: 4*5=20byte
对齐填充: 36 -> 40 byte
父类私有字段到底能不能被子类继承?
1 Java程序通过栈上的reference类型数据来操作堆上的对象。
1 对象实例数据: 对象的有效信息字段等(就是上面说的数据,存于堆中)。
2 对象类型数据: 该对象所属类的类信息(存于方法区中)。
句柄访问:
直接指针访问:
1 目的: 为了定义程序中各种共享变量访问规则。
2 Java内存模型规定:
3 注意: 主内存与工作内存 可以类比为 内存与高速缓冲存储器(cache):
从主内存中读取数据到工作内存,线程操作工作内存修改数据,最终从工作内存写到主内存上。
从内存中读取数据到cache,CPU操作cache上的数据,最终从cache再写回到主内存上。
/** * @author Tc.l * @Date 2020/11/3 * @Description: * 线程A读到num为0,让num为0时,线程A就循环 * 然后通过主线程修改num的值 * 但是程序不能停下来 一直处于运行状态(线程A依旧在循环) */ public class JavaMemoryModel { static int num = 0; public static void main(String[] args) { new Thread(()->{ while (num==0){ } },"线程A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } num=1; } }
上面的代码用Java内存模型图可以这样表示:
1 主内存与工作内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
1 JVM对这八种指令的使用,制定了如下规则:
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write。
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存。
不允许一个线程将没有assign的数据从工作内存同步回主内存,即未改变数据,又把数据从工作内存写回主内存。
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load,assign操作。
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁。
如果对一个变量进行lock操作,加锁会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
对一个变量进行unlock操作之前,必须把此变量同步回主内存。
验证可见性
public class JavaMemoryModel { static volatile int num = 0; public static void main(String[] args) { new Thread(()->{ while (num==0){ } },"线程A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } num=1; } }
验证不保证原子性
public class AtomicTest { static int num = 0; public static void add(){ num++; } public static void main(String[] args) { //多线程 执行 num自增 十万次 for (int i = 0; i < 10; i++) { new Thread(()->{ for (int j = 0; j <10000 ; j++) { add(); } }).start(); } //保证线程都执行完 while (Thread.activeCount()>2){ Thread.yield(); } //38806 , 26357 System.out.println(num); } }
理论上num应该为十万,但是每次都少很多。使用javap -c
进行反编译,查看字节码。
实际上num++时需要拿到这个静态变量然后操作,操作完再记录回去,在多线程中可能有的线程已经自加了但是还未记录回去,让别的线程读到错误的数量而导致不安全。
指令重排
Java虚拟机的即时编译器有对指令重排序的优化。
指令重排序: 不影响最终正确结果的情况下,指令执行顺序可能会与程序代码中执行顺序不同。
我们写的程序到机器可以执行的指令,之间这个过程可能会改变指令执行的顺序。
源代码->编译器优化重排->指令并行重排->内存重排->机器执行。
进行指令重排时,会考虑数据间的依赖。
int x = 0;//1 int y = 4;//2 y = x - 1;//3 x = x * x;//4
我们写的顺序是1234,但是执行的时候可能是2134或1423这都是不影响结果的
但是在多线程中(默认一开始b,c,x,y都是0)
线程A | 线程B |
---|---|
x = c | y = b |
b = 1 | c = 2 |
结果: x = 0, y = 0。
重排指令后:
线程A | 线程B |
---|---|
b = 1 | c = 2 |
x = c | y = b |
结果: x = 2 , y = 1。
在多线程中是不安全的(逻辑上存在的)。
使用volatile可以禁止指令重排,以防这种情况发生。
volatile避免指令重排
CPU指令的作用,使用内存屏障指令重排不能把内存屏障后的指令重排到内存屏障前
内存屏障是一个lock前缀的空操作,把前面锁住,前面没执行完就不能执行后面。
lock前缀空操作的作用: 将本处理器的缓存写入内存中,该写入动作也会引起别的处理器或别的内核无效化其缓存,相当于把缓存中的变量store,write写入主内存中,别的处理器发现缓存无效了立马去主内存中读,就实现了可见性通过这个空操作,volatile实现可见性。
lock前缀空操作指令修改同步到内存时,意味着之前操作执行完成,所以指令重排序无法越过内存屏障。
volatile变量与普通变量消耗性能的区别
允许虚拟机自行实现是否保证64位数据类型的load,stroe,read,write四个原子性操作。
主流平台下64位Java虚拟机不会出现非原子性访问行为,而32位存在此风险。
原子性:
monitorenter,monitorexit
来隐式使用lock,unlock(在Java代码中就是synchronized关键字)。可见性:
有序性:
1 时间先后顺序于先行发生原则没有因果关系,衡量并发问题不要受时间顺序干扰,一切必须以先行发生原则为准
内核线程就是直接由操作系统内核支持的线程,该线程由内核来完成线程的切换。
内核通过线程调度器对线程进行调度,并负责将线程任务映射到各个处理器。
一般使用内核线程的高级接口轻量级进程(线程),轻量级进程与内核线程1:1对应。
广义: 只要不是内核线程就是用户线程。
侠义: 完全建立在用户态上的线程(系统内核不知道线程如何实现)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-610f3Yhr-1633441494744)(Java内存模型与线程.assets/image-20201113210735138.png)]
yield()
)。NEW
:尚未启动的线程处于此状态。RUNNABLE
:在Java虚拟机中执行的线程处于此状态(RUNNABLE状态可能处于执行状态也可能处于就绪状态)。BLOCKED
:被阻塞等待监视器锁定的线程处于此状态被阻塞。WAITING
:正在等待另一个线程执行特定动作的线程处于此状态等待。TIMED_WAITING
:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态定时等待。TERMINATED
:已退出的线程处于此状态死亡。Java虚拟机在运行Java程序时,把所管理的内存分为多个区域, 这些区域就是运行时数据区。
运行时数据区可以分为:程序计数器,Java虚拟机栈,本地方法栈,堆和方法区。
什么是程序计数器?
程序计数器的作用是什么?
Java多线程的线程会切换,为了保存线程切换前的正确执行位置,每个线程都应该有程序计数器,因此程序计数器是线程私有的线程执行Java方法时,程序计数器记录的是正在执行的虚拟机字节码指令地址。线程执行本地方法时,程序计数器记录的是空。
执行引擎根据pc寄存器找到对应字节码指令来使用当前线程中的局部变量表(取某个值)或操作数栈(入栈,出栈…)又或是将字节码指令翻译成机器指令,然后由CPU进行运算。
生命周期:
内存溢出异常:
1 Java virtual Mechine Stack:
Java虚拟机栈描述:线程执行Java方法时的内存模型。
Java虚拟机栈的作用:
生命周期:
2 "栈"通常情况指的就是JVM栈,更多情况下**"栈"指的是JVM栈中的局部变量表**:
局部变量表中的存储空间以==局部变量槽(Slot)==来表示, double和long64位的用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 类型是什么?
reference : 直接或间接的查找到对象实例数据(堆中)和对象类型数据(方法区) 也就是通常说的引用。
returnAddress: 曾经用来实现异常处理跳转,现在不用了,使用异常表代替。
Java虚拟机通过定位索引的方式来使用局部变量表。
局部变量表的范围: 0~max_locals-1
。
我们上面代码中add()方法只有一个int参数,也没有局部变量,为什么最大变量槽数量为2呢?
实际上: 默认局部变量槽中索引0的是方法调用者的引用(通过"this"可以访问这个对象)。
其余参数则按照申明顺序在局部变量槽的索引中。
max_stack
操作数栈的最大深度也是编译时就确定下来了的。在方法执行的时候(字节码指令执行),会往操作数栈中写入和提取内容(比如add方法中a=a+2
,a入栈,常数2入栈,执行相加的字节码指令,它们都出栈,然后把和再入栈)。
操作数栈中的数据类型必须与字节码指令匹配(比如 a=a+2
都是Int类型的,字节码指令应该是iadd
操作int类型相加,而不能出现不匹配的情况)这是在类加载时验证阶段的字节码验证过程需要保证的。
静态解析与动态连接
1 内存溢出异常:
2 关于栈的两种异常:
测试StackeOverflowError
-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) */
测试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,创建线程又是必要的,解决办法可以是减小堆内存和减小线程占用栈内存大小。
与JVM栈作用类似。
JVM栈为Java方法服务,本地方法栈为本地方法服务。
内存溢出异常也与JVM栈相同。
hotspot将本地方法栈和Java虚拟机栈合并。
什么是堆?
堆的作用是什么?
生命周期:
堆内存:
年轻代:
老年代。
永久代(JDK8后变为元空间):
常驻内存,用来存放JDK自身携带的Class对象,存储的是Java运行时的一些环境。
JDK 6之前:永久代,静态常量池在方法区。
JDK 7 : 永久代,慢慢退化,去永久代,将静态常量池移到堆中(字符串常量池也是)。
JDK 8后 :无永久代,方法,静态常量池在元空间,元空间仍与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中。
不存在垃圾回收,关闭JVM就会释放这个区域的内存。
什么情况下永久代会崩:
因为这些原因容易OOM所以将永久代换成元空间,使用本地内存。
元空间:逻辑上存在堆,物理上不存在堆(使用本地内存)。
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 */ } }
使用-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
为什么明明设置的是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) */
什么是方法区?
方法区的作用是什么?
生命周期:
历史:
内存溢出异常:
因为方法区的主要责任是用于存放相关类信息,只需要运行时产生大量的类让方法区存放,直到方法区内存不够抛出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)对类增强时都会用到这类字节码技术。
所以增强的类越多,存放在方法区就越容易溢出。
什么是运行时常量池?
运行时常量池的作用是什么?
符号引用:#xx 会指向常量池中的一个直接引用(比如类引用Object):
运行时常量池中,绝大部分是随着JVM运行,从常量池中转化过来的,还有部分可能是通过动态放进来的(String的intern):
1 直接内存不是运行时数据区的一部分,因为这部分内存被频繁使用,有可能导致抛出OOM。
2 Java1.4加入了NIO(NEW Input/Output)类,引入了以通道传输,缓冲区存储的IO方式。
3 它可以让本地方法库直接分配物理内存,通过一个在Java堆中DirectByteBuffer
的对象作为这块物理内存的引用进行IO操作 避免在Java堆中和本地物理内存 堆中来回copy数据。
4 直接内存分配不受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
分配直接内存也会抛出内存溢出异常,但是它抛出异常并没有真正向操作系统申请空间,只是通过计算内存不足,自己手动抛出的异常。
/* 测试直接内存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()
)。1 本地方法通过本地方法接口来访问虚拟机中的运行时数据区。
2 某线程调用本地方法时,它就不受虚拟机的限制,在OS眼里它和JVM有同样权限。
3 可以直接使用本地处理器中的寄存器,直接从本地内存分配任意内存。
1 本地方法栈中登记native
修饰的方法,由执行引擎来加载本地方法库。
异常。
[外链图片转存中…(img-JP8vvSW9-1633441494755)]
/* 测试直接内存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()
)。1 本地方法通过本地方法接口来访问虚拟机中的运行时数据区。
2 某线程调用本地方法时,它就不受虚拟机的限制,在OS眼里它和JVM有同样权限。
3 可以直接使用本地处理器中的寄存器,直接从本地内存分配任意内存。
1 本地方法栈中登记native
修饰的方法,由执行引擎来加载本地方法库。
[外链图片转存中…(img-8nqU4sir-1633441494756)]