运行时数据区域,还有两组成部分:堆和方法区,和栈、程序计数器不同,它们是线程共享的
一、堆
1、堆heap
- 堆内存是线程共享的
- 创建出来的对象存于堆内存
如图:栈里的局部变量中存了堆上对象的引用:
2、堆溢出
ArrayList<Object> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]);
}
一直new对象往堆里放,最终达到堆内存上限值后堆内存溢出OutOfMemory:Java heap space
3、used、total、max
- used:已使用的堆内存
- total:可用堆内存大小
- max:最大可分配的堆内存
调整下上面的代码:
ArrayList<Object> list = new ArrayList<>();
while (true) {
System.in.read();
System.out.println("往堆中放入一次...");
list.add(new byte[1024 * 1024 * 10]);
}
使用阿尔萨斯工具来看JVM信息:
dashboard -i 刷新频率(毫秒)
或者直接使用memory只查看内存:
memory
随着堆中对象变多,used即将达到total时,total值变大,但最大只能到与max相等:
但并不是当used = max = total的时候,堆内存就溢出了!! 这和垃圾回收有关。
4、设置堆内存大小
添加虚拟机参数:
-Xmx值 -Xms值
//eg:
-Xmx1g -Xms1g
在Dockerfile中可写:
ENV JAVA_OPTS="-Xms512m -Xmx512m "
其中:
- –Xmx是max最大值,-Xms 是初始的total
- 单位默认字节byte且是1024的倍数,可k或者K(KB)、m或者M(MB)、g或者G(GB)
- 限制Xmx必须大于 2 MB,Xms必须大于1MB
开发中,把-Xmx和-Xms设为相同的值,如此,程序启动后的可用内存就是最大内存,无需向JVM频繁申请,以减少申请内存的时间开销。
二、直接内存
直接内存不属于Java运行时的内存区域,用途:
-
Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用
-
普通IO,是文件先读到内存(缓存区),再复制到JVM堆中。引入直接内存则不用复制这一步,直接让堆中存一个引用
-
JDK8及以后,存方法区的数据
使用ByteBuffer创建直接内存上的数据:
public class Demo2 {
public static int size = 1024 * 1024 * 100;
public static List<ByteBuffer> list = new ArrayList<>();
public static int count = 0;
public static void main(String[] args) throws Exception {
System.in.read();
while(true){
ByteBuffer directSpace = ByteBuffer.allocateDirect(size);
list.add(directSpace);
System.out.println(++count);
Thread.sleep(5000);
}
}
}
阿尔萨斯查看:
修改直接内存的上限:
-XX:MaxDirectMemorySize=大小
//eg:-XX:MaxDirectMemorySize=300m
出现直接内存溢出:
1、 NIO与常规IO
分别使用ByteBuffer操作直接内存、使用JVM内存进行文件拷贝,耗时分别为63ms、256ms
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 演示 ByteBuffer 作用
*/
public class DirectMemoryDemo {
static final String FROM = "E:\\bak1\\01-java学习.mp4";
static final String TO = "E:\\bak2\\abc.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); //耗时256.8563
directBuffer();//耗时63.2449
}
//使用直接内存
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
常规IO和NIO的性能差别大,原因如下,
常规IO的数据拷贝:
Java本身不具备磁盘读写的能力,它要调用磁盘读写的话,必须通过操作系统提供的函数,在JDK中的体现就是native本地方法。
因此涉及CPU用户态和内核态的切换,操作系统将文件先读一部分到系统缓存区(如上面设置的1MB),Java在JVM内存的堆中也开辟一块内存,即上面的byte[]
做为缓冲区,通过native方法将文件从系统缓冲区复制到Java的缓冲区(堆的byte[]对象)。
NIO数据拷贝流程:
和上面的常规IO不同,这次开辟出一块直接内存,这块直接内存系统和Java代码都可以直接访问,少了一次缓冲区的复制操作
总结:
直接内存主要通过 java.nio 包下的 ByteBuffer 类来进行分配和使用
ByteBuffer.allocateDirect(int capacity)
直接内存可以减少数据在 Java 堆和操作系统内存之间的拷贝次数,从而提高 I/O 的效率,常用于文件读写,虽然直接内存不在对上,但在 ByteBuffer 对象被回收时,直接内存也会被释放。