直接内存
- 1. 直接内存
- 1.1 直接内存有哪些
- 1.2 代码案例
- 2. 为什么要使用直接内存
- 3. 直接内存的缺点
- 4. 直接内存案例和场景分析
- 4.1 内存泄漏案例
- 4.2 常规排查方式
- 4.3 使用工具排查
- 4.4 内存泄漏问题解决
- 5. 堆外内存默认多大
- 6. 直接内存总结
1. 直接内存
Java 应用程序通过直接方式从操作系统中申请的内存
1.1 直接内存有哪些
- 使用了 Java 的 Unsafe 类,做了一些本地内存的操作;
- Netty 的直接内存(Direct Memory),底层会调用操作系统的 malloc 函数;
- JNI 或者 JNA 程序,直接操纵了本地内存,比如一些加密库;
JNI 是 Java Native Interface 的缩写,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植。
JNA(Java Native Access )提供一组 Java 工具类用于在运行期间动态访问系统本地库(native library:如 Window 的 dll)而不需要编写任何 Native/JNI 代码。
开发人员只要在一个 java 接口中描述目标 native library 的函数与结构,JNA 将自动实现 Java 接口到 native function 的映射。
JNA 是建立在 JNI 技术基础之上的一个 Java 类库,它使您可以方便地使用 java 直接访问动态链接库中的函数。
原来使用 JNI,你必须手工用 C 写一个动态链接库,在 C 语言中映射 Java 的数据类型。
JNA 中,它提供了一个动态的 C 语言编写的转发器,可以自动实现 Java 和 C 的数据类型映射,你不再需要编写 C 动态链接库。
也许这也意味着,使用 JNA 技术比使用 JNI 技术调用动态链接库会有些微的性能损失。但总体影响不大,因为 JNA 也避免了 JNI 的一些平台配置的开销。
1.2 代码案例
1、 Unsafe 类,-XX:MaxDirectMemorySize 参数的大小限制对这种是无效的
2、ByteBuffer 的这种方式,受到 MaxDirectMemorySize 参数的大小限制
其实底层是
2. 为什么要使用直接内存
直接内存,其实就是不受 JVM 控制的内存。相比于堆内存有几个优势:
1、减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作。
2、加快了复制的速度。因为堆内在 flush 到远程时,会先复制到直接内存(非堆内存),然后再发送,而堆外内存相当于省略掉了这个工作。
3、可以在进程间共享,减少 JVM 间的对象复制,使得 JVM 的分割部署更容易实现。
4、可以扩展至更大的内存空间。比如超过 1TB 甚至比主存还大的空间。
3. 直接内存的缺点
1、 堆外内存难以控制,如果内存泄漏,那么很难排查
2、 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象比较适合。
4. 直接内存案例和场景分析
4.1 内存泄漏案例
- 工作中经常会使用 Java 的 Zip 函数进行压缩和解压,这种操作在一些对传输性能较高的的场景经常会用到。
- 程序将会申请 1kb 的随机字符串,然后不停解压。为了避免让操作系统陷入假死状态,我们每次都会判断操作系统内存使用率,在达到 60% 的时候,我们将挂起程序(不在解压,只不断的让线程休眠)
- 通过访问 8888 端口,将会把内存阈值提高到 85%。
package demo;
import com.sun.management.OperatingSystemMXBean;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpServer;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
*
* -XX:+PrintGC -Xmx1G -Xmn1G
* -XX:+AlwaysPreTouch
* -XX:MaxMetaspaceSize=10M
* -XX:MaxDirectMemorySize=10M
*/
public class LeakProblem {
/**
* 构造随机的字符串
*/
public static String randomString(int strLength) {
Random rnd = ThreadLocalRandom.current();
StringBuilder ret = new StringBuilder();
for (int i = 0; i < strLength; i++) {
boolean isChar = (rnd.nextInt(2) % 2 == 0);
if (isChar) {
int choice = rnd.nextInt(2) % 2 == 0 ? 65 : 97;
ret.append((char) (choice + rnd.nextInt(26)));
} else {
ret.append(rnd.nextInt(10));
}
}
return ret.toString();
}
//复制方法
public static int copy(InputStream input, OutputStream output) throws IOException {
long count = copyLarge(input, output);
return count > 2147483647L ? -1 : (int) count;
}
//复制方法
public static long copyLarge(InputStream input, OutputStream output) throws IOException {
byte[] buffer = new byte[4096];
long count = 0L;
int n;
for (; -1 != (n = input.read(buffer)); count += (long) n) {
output.write(buffer, 0, n);
}
return count;
}
//解压
public static String decompress(byte[] input) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
copy(new GZIPInputStream(new ByteArrayInputStream(input)), out);
return new String(out.toByteArray());
}
//压缩
public static byte[] compress(String str) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(bos);
try {
gzip.write(str.getBytes());
gzip.finish();
byte[] b = bos.toByteArray();
return b;
}finally {
try { gzip.close(); }catch (Exception ex ){}
try { bos.close(); }catch (Exception ex ){}
}
}
private static OperatingSystemMXBean osmxb = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
//通过MXbean来判断获取内存使用率(系统)
public static int memoryLoad() {
double totalvirtualMemory = osmxb.getTotalPhysicalMemorySize();
double freePhysicalMemorySize = osmxb.getFreePhysicalMemorySize();
double value = freePhysicalMemorySize / totalvirtualMemory;
int percentMemoryLoad = (int) ((1 - value) * 100);
return percentMemoryLoad;
}
private static volatile int RADIO = 60;
public static void main(String[] args) throws Exception {
//模拟一个http请求--提高内存阈值
HttpServer server = HttpServer.create(new InetSocketAddress(8888), 0);
HttpContext context = server.createContext("/");
context.setHandler(exchange -> {
try {
RADIO = 85;
String response = "OK!";
exchange.sendResponseHeaders(200, response.getBytes().length);
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
} catch (Exception ex) {
}
});
server.start();
//构造1kb的随机字符串
int BLOCK_SIZE = 1024;
String str = randomString(BLOCK_SIZE / Byte.SIZE);
//字符串进行压缩
byte[] bytes = compress(str);
for (; ; ) {
int percent = memoryLoad();
if (percent > RADIO) {//如果系统内存使用率达到阈值,则等待1s
System.out.println("memory used >"+RADIO+" hold 1s");
Thread.sleep(1000);
} else {
//不断对字符串进行解压
decompress(bytes);
Thread.sleep(1);
}
}
}
}
- 程序打包上传到 CenterOS 的服务器中(服务器内存设置为 4G)
- 使用以下命令把程序跑起来
java -cp JVM.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10M demo.LeakProblem - 分别使用 Xmx、MaxMetaspaceSize、MaxDirectMemorySize 这三个参数限制了堆、元空间、直接内存的大小。
- AlwaysPreTouch 这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了,默认情况下,此选项是禁用的,并且所有页面都在 JVM 堆空间填 充时提交。我们为了减少内存动态分配的影响,把这个值设置为 True。
- 这个程序很快就打印一下显示,这个证明操作系统内存使用率,达到了 60%。
- 通过 top 命令查看,确实有一个进程占用了很高的内存,
- VIRT:virtual memory usage 虚拟内存
1、 进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
2、假如进程申请 100m 的内存,但实际只使用了 10m,那么VIRT = 100m,而不是实际的使用量 - RES:resident memory usage 常驻内存 达到了 1.338G
如果申请 100m 的内存,实际使用 10m,那么RES = 10m,与 VIRT 相反 - 访问服务器的 8888 端口,将内存使用的阈值增加到 85%
curl http://127.0.0.1:8888/
4.2 常规排查方式
- 按照之前的排查方式,如果碰到内存占用过高,我们使用 top 命令来跟踪,然后使用 jmap –heap 来显示
- 这个 2879 的 java 进程,占据的堆空间是1G,合计数小于 top 命令看到的 2.25G
- 使用 jstack 命令来看下此线程的虚拟机栈占用情况
- 发现只有 10 来个线程,这块占用的空间肯定也不多。
- jmap -histo 2879 | head -20 显示占用内存最多的对象
- 全部加起来只有20多M,没有达到2.25G
- jmap -dump:live,format=b,file=heap.bin 2879,把内存 dump 下来,放到 MAT 中进行分析。
- 发现没什么问题,堆空间也好,其他空间也好,这些都没有说的那么大的内存 2.25G 左右。
4.3 使用工具排查
- NativeMemoryTracking,是用来追踪 Native 内存的使用情况。通过在启动参数上加入 -XX:NativeMemoryTracking=detail 就可以启用。使用 jcmd (jdk 自带)命令,就可查看内存分配。
- Native Memory Tracking (NMT) 是 Hotspot VM 用来分析 VM 内部内存使用情况的一个功能。我们可以利用 jcmd(jdk 自带)这个工具来访问 NMT 的数据。
- NMT 必须先通过 VM 启动参数中打开,不过要注意的是,打开 NMT 会带来 5%-10%的性能损耗。
- 在服务器上重新运行程序:
java -cp JVM.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10M -XX:NativeMemoryTracking=detail demo.LeakProblem
jcmd 3344 VM.native_memory summary
- 这个值远没有达到1.338G的内存占用。
4.4 内存泄漏问题解决
- 问题关键点
GZIPInputStream 使用 Inflater 申请堆外内存、我们没有调用 close() 方法来主动释放。如果忘记关闭,Inflater 对象的生命会延续到下一次 GC,有一点类似堆内的弱引用。在此过程中,堆外内存会一直增长。
- 问题修复
调用 close() 方法来主动释放,放置内存泄漏 - 修改之后,重新启动
- 问题得到解决
5. 堆外内存默认多大
没有显示的设置-XX:MaxDirectMemorySize 参数的情况下,通过 ByteBuffer 能够分配的直接内存空间大小就是堆的最大的 可使用 的大小。
堆的最大的可使用的大小= 堆的最大值- 一个 Survivor 的大小(浪费的空间)
- VM 参数配置:-Xmx135m -Xmn100m -XX:SurvivorRatio=8
- 程序报错,堆的最大的可使用的大小=135-10m=125m ,不能分配 128M 的对象
- VM 参数配置:-Xmx138m -Xmn100m -XX:SurvivorRatio=8
- 堆的最大的可使用的大小=138-10m=128m ,刚好可以分配 128M 的对象
6. 直接内存总结
- 直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小
- 其他堆外内存(直接内存),主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。这种情况下没有任何参数能够阻挡它们,要么靠它自己去释 放一些内存,要么等待操作系统对它来处理。所以如果你对操作系统底层以及内存分配使用不熟悉,最好不要使用这块,尤其是 Unsafe 或者其他 JNI 手段直接直接申请的内存。