JVM系列文章目录
前言
本文基于JDK1.8,Hotspot版本的JVM。本博文只介绍我们作为Java程序员该从什么角度,通过什么样的流程去分析JVM源码(而不是上来就直接解读JVM源码,直接干源码属实容易劝退大部分人)。
直接内存
直接内存(又叫堆外内存)指的是操作系统除了被虚拟机虚拟化的那些堆内存外,其他Java程序可以直接从操作系统申请的内存。
Java中直接内存的方式
-
通过使用Unsafe类,做本地内存的操作。(这个一般情况下如果你不做架构的话用不上,而且现在不能直接使用,得通过反射的方式来调用;Oracle也不提倡你用Unsafe。)
Unsafe类使用。我这里使用-XX:MaxDirectMemorySize
参数来限制直接内存大小为10M,然后程序直接使用100M的内存。import sun.misc.Unsafe; import java.lang.reflect.Field; /** * @author Abfeathers * @date 2021/3/25 * @Description Unsafe类使用直接内粗 * -XX:MaxDirectMemorySize=10m 限制直接内存大小,对 Unsafe无效 * */ 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); } }
运行发现没有报OOM,由此可见Unsafe甚至不受直接内存大小设置限制,也太恐怖了。
-
Netty的直接内存或者使用ByteBuffer,底层会调用操作系统的malloc函数。
我这里以ByteBuffer作为例子来展示,我这里也用-XX:MaxDirectMemorySize
参数来限制直接内存大小为100M,然后我使用128M的内存。import java.nio.ByteBuffer; /** * @author Abfeathers * @date 2021/3/25 * @Description byteBuffer使用直接内存 * 限制最大直接内存大小100m-XX:MaxDirectMemorySize=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); } }
我运行程序,发现OOM了,这样看起来,ByteBuffer起码还受直接内存大小参数限制。
-
JNI或者JNA程序,直接操作本地内存,比如一些加密库。
JNI全称是Java Native Interface,通过使用Java本地接口书写程序,可以确保代码在不同平台上移植。
JNA(Java Native Access)则是JNI的一种封装,这种方式更方便开发人员去调用动态链接库中的函数。
(这个上面的ByteBuffer、Unsafe都是通过调用JNI来实现的,我这里就不举例说明了。)
直接内存的优缺点
优点:
- 可以在一定程度上减少GC,因为它都不受JVM控制,自然也就不会有垃圾回收器去管它。
- 可以加快复制速度。比如说我们将堆中的对象推送到远端,流程是对象先从堆复制到直接内存,然后再通过网络发送到远端,而将对象直接放到直接内存就可以省略从堆复制到直接内存这个步骤(也就是我们说的零拷贝)。
- 可以在不同的进程之间共享,因为对象直接分配在直接内存上,所有的进程都可以通过物理地址去调用,也方便实现JVM分割部署。
- 可以更方便的扩展更大内存空间,比如说直接给堆外内存分配1TB以上甚至比主存更大的空间。
缺点:
- 堆外内存难以控制,出现内存泄漏难以排查(这一点会在下面排查直接内存溢出的时候体现出来)。
- 堆外内存不适合存储很复杂的对象,只能存储一些相对简单的对象,毕竟它不是JVM虚拟化的内存,很多数据结构都会受到限制。
直接内存内存泄漏分析
我这里模拟一个 压缩存储对象再解压的场景。Java程序会先申请1Kb的随机字符串,然后不用的解压。然后我这里为了避免操作系统假死,预先设置了一个阀置60%,每次解压的时候都先判断操作系统的内存使用率是不是达到了60%,如果达到了,我就挂起程序,不进行解压,只是不断地让线程休眠。
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 Abfeathers
* @date 2021/3/25
* @Description 直接内存泄漏问题排查
*
* VM args
* -XX:+PrintGC -Xmx1G -Xmn1G
* -XX:+AlwaysPreTouch
* -XX:MaxMetaspaceSize=10M
*/
public class LeakProblem {
/**
* @author Abfeathers
* @Description 生成随机字符串
* @date 2021/3/25
* @param
* @return
*/
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();
}
/**
* @author Abfeathers
* @Description 复制方法
* @date 2021/3/25
* @param
* @return
*/
public static int copy(InputStream input, OutputStream output) throws IOException {
long count = copyLarge(input, output);
return count > 2147483647L ? -1 : (int) count;
}
/**
* @author Abfeathers
* @Description 复制方法
* @date 2021/3/25
* @param
* @return
*/
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;
}
/**
* @author Abfeathers
* @Description 解压
* @date 2021/3/25
* @param
* @return
*/
public static String decompress(byte[] input) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
copy(new GZIPInputStream(new ByteArrayInputStream(input)), out);
return new String(out.toByteArray());
}
/**
* @author Abfeathers
* @Description 压缩
* @date 2021/3/25
* @param
* @return
*/
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();
/**
* @author Abfeathers
* @Description 通过MXbean来判断获取内存使用率(系统)
* @date 2021/3/25
* @param
* @return
*/
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 = "success!";
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);
}
}
}
}
我这里设置了一些VM参数
-XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M
将堆内存最大值设置为1G,新生代直接铺满这1G内存,元空间限制大小为10M。
设置这个参数AlwaysPreTouch就可以在JVM启动的时候把所有的内存都在操作系统分配了,我这里启用是为了减少内存动态分配到影响。
顺便我还开启了GC日志输出。
运行程序很快就发现内存占用达到阀值了,而且一直没有消减下去。
我们按照一般流程进行排查
-
使top指令查看(由于我这里是mac系统,top能看到的指标跟linux不一样,我这里用linux跑了一个来演示)
我们要注意两个指标
VIRT:virtual memory usage 虚拟内存,只程序所需要的虚拟内存,包括包括进程使用的库、代码、数据等。如果进程申请100M内存,那这申请的100M内存就会加到VIRT上。
RES:resident memory usage 常驻内存,同样的进程申请100M内存,使用10M,那这个使用的10M就是常驻内存。
在Linux上我们进行排查的话就可以看到这样的现象,内存使用达到1.5G了,好像与我们的设置有点出入。
-
这样的话我们来看看这个堆栈信息
jmap -heap <pid>
,发现堆栈加起来没有不够1.5G。
-
那就看看是不是虚拟机栈占用内存过高,
jstack <pid>
,发现线程也就11个,加起来占用内存也就11M左右,那问题不是在这。
-
那在看看哪些对象占用内存过高
jmap -histo <pid> | head -20
显示占用内存最多的对象。发现加起来也远远不够。
-
通过指令没办法分析了,那就只能导出dump日志来通过MAT来分析了。
看了一下三个问题分析,明显也不是。
到这一步其实就发现不是堆空间的问题了,应该是直接内存泄漏。(一般我们分析到这一步就够了,已经足够推导出是直接内存泄漏的问题了。) -
当然JDK也为我们提供了一种工具NMT来进行直接内存的内存分析,但是说实话没什么用。
首先我们需要重启我们刚刚的程序,因为要加入参数-XX:NativeMemoryTracking=detail
,如果你生产环境没有这个参数的话,那你的确要重启,但是你想象重启了之后你还能立即复现问题么?毕竟生产环境一般都比较复杂。
当然如果你已经启用了这个参数,那你就可以使用jcmd <pid> VM.native_memory summary
来分析。(其实你也分析不出说明东西来。这个占用属实太小了)
当然我这里可以给各位推荐一个工具perf,但是要用这个的话,你得要有一定的操作系统底层基础,如果有兴趣的话可以了解一下。 -
好了问题分析出来是直接内存泄漏,那我们就来找一下是代码看看是哪里调用了JNI(直接内存泄漏一般是上面说的那三种,各位可以根据自己的代码去进行分析)。
-
看来看去也就只有GZIP这个工具了,我们来看一看。
我们发现我们在调用GZIPInputStream的时候,它底层会通过Inflater去调用native方法去直接操作内存。而我们在调用完之后又没有关闭这个流,Inflater 对象的生命会延续到下一次 GC,如果一直不GC的话,直接内存占用就会一直在增加。所以我们只要结束的时候关闭流就可以规避了。
JVM 源码解析
这里我会从直接内存默认大小这个场景切入去解析源码,其实解析JVM源码最好先确定场景,然后由场景入手,最后定位到JVM的源码。(这里使用OpenJDK的源码包)。
查看源码工具
window的话我推荐使用SourceInsight,由于我这里是mac,所以我用Sublime来查看了。
分析直接内存默认大小
大家都知道直接内存默认大小与堆内存大小一致,是不是真的是这样呢?我们来分析一下。
案例演示
还是上面的案例代码
import java.nio.ByteBuffer;
/**
* @author Abfeathers
* @date 2021/3/25
* @Description byteBuffer使用直接内存
* 限制最大直接内存大小100m-XX:MaxDirectMemorySize=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);
}
}
-
我设置VM参数
-XX:MaxDirectMemorySize=128m
直接显示的设置直接内存大小,运行程序,没有出现OOM。
-
现在我是用另外一个VM参数
-Xmx128m
,我设置堆的最大值为128M,运行程序,发生OOM了。
-
再换参数,
-Xmx135m -Xmn100m -XX:SurvivorRatio=8
,我设置堆最大值为135M,新生代为100M,新生代内存比例为8:1:1,运行程序,发生OOM了。
-
使用参数
-Xmx138m -Xmn100m -XX:SurvivorRatio=8
,设置堆最大值为138M,新生代为100M,新生代内存比例为8:1:1,运行程序,没有发生OOM。
由此可见,直接内存默认大小与堆内存大小一致这句话并不严谨。
源码分析
源码分析不能直接就怼到JVM源码,那玩意儿对单纯的Java程序原来说挑战有点大,我们先从Java程序触发,一步步定位到JNI方法。
-
上面的代码,我们是通过
ByteBuffer.allocateDirec()
来直接使用直接内存的,我们点进去看一看。
-
在点进DirectByteBuffer,我们发现了
华点reserveMemory。
-
点进reserveMemory方法。
-
在进入maxDirectMemory,发现居然是个静态变量,但是这个大小不会啊,怎么才60几M。
-
那我们只能从java.lang.System初始化去找怎么设置这个值的了。
-
终于找到native方法了,现在我们可以根据对应关系,去JVM源码里找实现了。
对应关系:在 JVM 的源码中一般都是会把包名加上,因为是给 java 用的所以前缀上还有一个 java,那我们这个对应的函数就是Java_java_lang_Runtime_maxMemory。
-
直接全局搜索到方法Java_java_lang_Runtime_maxMemory
-
找到引用它到地方
-
我们直接找gen堆栈的实现,发现默认直接内存大小是各个分代到累加。
-
我们看看默认新生代是怎么实现的(这里看新生代是因为老年代就一个区没有Surivior浪费,所以我这里直接看新生代了),发现最后的返回结果是堆大小-一个surivior区大小。
所以到这里可以得出结论了:
直接内存的默认大小是与堆的可用内存大小一致(堆大小 - 一个被浪费调的Surivior区大小)。
上一篇:JVM调优之预估调优与问题排查
下一篇: JVM及时编译器