直接内存(堆外内存)指的是 Java 应用程序通过直接方式从操作系统中申请的内存。这个差别与之前的堆、栈、方法区,那些内存都是经过了虚拟化。所以严格来说,这里是指直接内存。
使用了 Java 的 Unsafe 类,做了一些本地内存的操作;
Netty 的直接内存(Direct Memory),底层会调用操作系统的 malloc 函数。
JNI 或者 JNA 程序,直接操纵了本地内存,比如一些加密库;
JNI 是 Java Native Interface 的缩写,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植。
JNA(Java Native Access )提供一组 Java 工具类用于在运行期间动态访问系统本地库(native library:如 Window 的 dll)而不需要编写任何 Native/JNI 代码。
下面通过代码来具体分析
/**
* @author
* 参数无效:-XX:MaxDirectMemorySize=10m
*/
public class UnsafeDemo {
public static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
long addr = unsafe.allocateMemory(100*_1MB);
}
}
2.
import java.nio.ByteBuffer;
/**
* @author
* VM Args:-XX:MaxDirectMemorySize=100m
* 限制最大直接内存大小100m
* -XX:MaxDirectMemorySize=128m
* -Xmx128m
* -Xmx135m -Xmn100m -XX:SurvivorRatio=8
* -Xmx138m -Xmn100m -XX:SurvivorRatio=8
*/
public class ByteBufferDemo {
static ByteBuffer bb;
public static void main(String[] args) throws Exception {
//直接分配128M的直接内存
bb = ByteBuffer.allocateDirect(128*1024*1024);
}
}
3.
如果这样配置参数,发现不会发生OOM。这里思考一下为什么138m的时候就不会发生oom了呢?
直接内存,其实就是不受 JVM 控制的内存。相比于堆内存有几个优势:
1、减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作。
2、加快了复制的速度。因为堆内在 flush 到远程时,会先复制到直接内存(非堆内存),然后再发送,而堆外内存相当于省略掉了这个工作。
3、可以在进程间共享,减少 JVM 间的对象复制,使得 JVM 的分割部署更容易实现。
4、可以扩展至更大的内存空间。比如超过 1TB 甚至比主存还大的空间。
直接内存有很多好处,我们还是应该要了解它的缺点:
1、 堆外内存难以控制,如果内存泄漏,那么很难排查
2、 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象比较适合。
内存泄漏案例
工作中经常会使用 Java 的 Zip 函数进行压缩和解压,这种操作在一些对传输性能较高的的场景经常会用到。
程序将会申请 1kb 的随机字符串,然后不停解压。为了避免让操作系统陷入假死状态,我们每次都会判断操作系统内存使用率,在达到 60% 的时候,
我们将挂起程序(不在解压,只不断的让线程休眠)
通过访问 8888 端口,将会把内存阈值提高到 85%。
package ex15;
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;
/**
* @author
*
* -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);
}
}
}
}
把上面的代码打成jar包部署到linux环境进行测试。我的虚拟机2核,4g内存
使用以下命令把程序跑起来:
java -cp ref-jvm3.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M
-XX:MaxDirectMemorySize=10M ex15.LeakProblem
参数解释:
分别使用 Xmx、MaxMetaspaceSize、MaxDirectMemorySize 这三个参数限制了堆、元空间、直接内存的大小。
AlwaysPreTouch 这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了,默认情况下,此选项是禁用的,并且所有页面都在 JVM 堆空间填充时提交。我们为了减少内存动态分配的影响,把这个值设置为 True。
这个程序很快就打印一下显示,这个证明操作系统内存使用率,达到了 60%。
通过 top 命令查看,确实有一个进程占用了很高的内存,
VIRT :virtual memory usage 虚拟内存
1、 进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
2、假如进程申请 100m 的内存,但实际只使用了 10m,那么它会增长 100m,而不是实际的使用量
RES :resident memory usage 常驻内存 达到了 1.4G
如果申请 100m 的内存,实际使用 10m,它只增长 10m,与 VIRT 相反
按照之前的排查方式,如果碰到内存占用过高,我们使用 top 命令来跟踪,然后使用 jmap –heap 来显示
我们发现这个 2741的 java 进程,占据的堆空间是比较小的,合计数远远小于 top 命令看到的 1.4G
我们怀疑是不是虚拟机栈占用过高。于是使用 jstack 命令来看下线程
发现也就那么 10 来个左右的线程,这块占用的空间肯定也不多。
jmap -histo 2741| head -20 显示占用内存最多的对象
通过单位换算,发现内存大小不到1个G,还是差太多。
发现不了,我们前面用过 MAT,我们把内存 dump 下来,放到 MAT 中进行分析。
jmap -dump:live,format=b,file=heap.bin 2741
发现MAT内存大小也对应不上。堆空间也好,其他空间也好,这些都没有说的那么大的内存 1.4G 左右。
这种情况应该是发生了直接内存泄漏。、
如果你有时间,有兴趣,我推荐你使用 perf 这个工具,这个工具安装很容易,但是容易遇到操作系统内核一些功能没支持,
也分析不了,这里我就不多去花时间去分析:
Perf 安装:yum install perf
这个程序可以访问服务器的 8888 端口,这将会把内存使用的阈值增加到 85%,我们的程序会逐渐把这部分内存占满
curl http://127.0.0.1:8888/
问题关键点
GZIPInputStream 使用 Inflater 申请堆外内存、我们没有调用 close() 方法来主动释放。如果忘记关闭,Inflater 对象的生命会延续到下一次 GC,有一点类似堆内的弱引用。在此过程中,堆外内存会一直增长。
由此可以定位到native修饰的本地方法,可见与直接内存有关。
调用 close() 方法来主动释放,放置内存泄漏
Top –p 查看,这个内存和堆内存的情况就比较符合
问题得到解决。
直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小
其他堆外内存(直接内存),主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。这种情况下没有任何参数能够阻挡它们,要么靠它自己去释放一些内存,要么等待操作系统对它来处理。所以如果你对操作系统底层以及内存分配使用不熟悉,最好不要使用这块,尤其是 Unsafe 或者其他 JNI 手段直接直接申请的内存.
那为什么要讲呢,EhCache 这种缓存框架,提供了多种策略,可以设定将数据存储在非堆上。
还有像 RocketMQ 都走了堆外分配,所以我们又必须要去了解他。
通过jvm,openjdk源码分析,直接内存
在这里可以看到,值 新生代的最大值 = 新生代的最大值 - 一个 survivor 的大小。
为什么会这样,因为在新生代采用复制回收算法,一个幸存者区域是浪费的,所以实际空间最大大小要减去一个交换器的大小。
而老年代是没有空间浪费的,所以还是全区域。
也 得出 我们设置的-Xmx 的值里 减去 一个 survivor 的大小就是默认的堆外内存的大小