直接内存
直接内存(堆外内存)
-
直接内存和堆外内存可以是认为是等同的,直接内存是直接向操作系统申请的,而堆是JVM提前申请的一块内存
-
直接内存有哪些?
-
java的unsafe类
package ex15; import sun.misc.Unsafe; import java.lang.reflect.Field; /** * @author King老师 * 参数无效:-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); } }
使用unsafe类后,-XX:MaxDirectMemorySize这个虚拟机参数就无效了,就是这个参数设置的直接内存限制对unsafe无效,同时jdk不允许直接使用unsafe类,只能通过反射的方式来使用
-
netty中ByteBuffer,底层会去和操作系统发生调用
package ex15; import java.nio.ByteBuffer; /** * @author King老师 * 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); } }
ByteBuffer是收到-XX:MaxDirectMemorySize的限制的,超过后会抛OOM:direct memory
-
JNI或者JNA,JNI–java本地接口,java自带的native方法,底层是一个dll,JNA–java本地访问,JNI的封装,使用起来可以不用编写native方法,我们在java接口中间描述一个native函数的结构,JNA会自动封装一个到本地函数的映射
-
为什么要使用直接内存
-
不受JVM控制
-
为什么?
-
1.减少垃圾回收的工作,STW
-
2.加快复制的速度,网络通讯时,堆上的数据需要被复制到直接内存上,这件事是操作系统底层做的,零拷贝
-
3.进程间的共享
-
4.堆受制于虚拟化的程度,如果虚拟机的内存是1tb或者以上,因为做虚拟化开销很大,会有性能损耗,还不如使用直接内存
-
直接内存的另一面
- 1.难以控制,出现内存泄漏,很难排查
- 2.相对于堆,不适合存储复杂对象,适用于简单对象
直接内存
内存泄漏案例
-
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 King老师 * * -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); } } } }
-
GZIPOutputStream,java自带的类,可以对数据进行压缩和解压缩
-
java -cp ref-jvm3.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10M e15.LeakProblem
- -XX:AlwaysPreTouch,jvm启动时,为所有的内存都进行了系统分配,但是这个内存分配不一定及时提交了,启用这个参数后会立即提交
常规排查方式
-
top
-
然后top -p pid,只看指定进程的信息
-
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3555 root 20 0 5452628 1.541g 12140 S 0.3 41.6 0:19.05 java
- RES,常驻内存,resident memory usage,进程申请内存申请100M,但是实际只使用了10M,此时RES就是10M,如果没有标明单位,默认是比特
- VIRT,虚拟内存,virtual memory usage,进程申请内存申请100M,但是实际只使用了10M,此时VIRT就是100M
-
发现启动的java程序的cpu使用率不高,但是mem使用率很高,并且不断增加
-
RES是1.54g,但是只设置1g,结果不正常?
-
案例代码中,如果内存使用率超过60%,线程就会睡眠
-
使用jmap -heap,发现堆确实只使用了1个g,使用jstack xxx,发现也只有几个线程,那样占的内存也只有几M而已,那合起来还是不对
-
jmap -histo xxx | head -20,发现最大的实例对象也只有300M,还是不对
-
使用mat来分析内存,生成一个dump.bin文件,在mat中打开
-
首先mat猜想是类加载器导致的,但是只有30M左右的大小,是不对的
-
这个时候就要怀疑是否是直接内存泄漏,java自带一个工具–NMT,native memory tracking,需要加一个虚拟机配置参数,-XX:NativeMemoryTracking=detail,这个参数不是可配置的(product是生产,这种是不可动态配置的,manageable是可动态配置),并且默认是关闭的,所以需要重启服务(使用命令查看是否可配置–java -XX:+PrintFlagsFinal -version)
-
-
使用工具排查
内存泄漏问题解决
- 修改启动命令
- ps -ef | grep java
- kill -9 3555
- ls,发现已杀死的命令
- java -cp ref-jvm3.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10M -XX:NativeMemoryTracking=detail e15.LeakProblem
- 重启完成后,发现RES在不断变大
- 输入命令,jcmd xxx VM.native_memory summary
- 发现native方法也只用了几百KB,反应不出来问题
- 这个时候需要用到更专业的运维来处理,专业的查看直接内存泄漏的工具perf(坑,很多时候操作系统内存功能没开启,没支持,也分析不出来,需要开启)
- 这个时候考虑直接内存在java中只有三种方式,而三种中更可能的是JNI方法,而gzip中使用了JNI方法
- GZIPInputStram类,Inflater类,init方法—去申请内存
- 对于这种输入输出流,使用完后要记得关闭,如果没关闭,它的生命周期可以活到gc,因为Inflater是java类,但是是直接内存,虚拟机不会触发垃圾回收,所以直接内存会一直增长
内存泄漏问题解决
-
在解压方法里面,进行close
-
//解压 public static String decompress(byte[] input) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(input)); try { copy(gzip, out); return new String(out.toByteArray()); }finally { try{ gzip.close(); }catch (Exception ex){ } try{ out.close(); }catch (Exception ex){ } } }
JVM源码分析
JVM源码如何查看!
- https://cloud.tencent.com/developer/article/1585224
场景分析:堆外内存默认值是多大?
- 堆外内存默认值是堆空间的可使用值,要把survivor区预留的那一部分扣减掉
源码追踪
-
package ex15; import java.nio.ByteBuffer; /** * @author King老师 * 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); } }
ByteBuffer.allocateDirect
-
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
new DirectByteBuffer(capacity)
-
DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
Bits.reserveMemory(size, cap)
-
static void reserveMemory(long size, int cap) { if (!memoryLimitSet && VM.isBooted()) { maxMemory = VM.maxDirectMemory(); memoryLimitSet = true; } // optimist! if (tryReserveMemory(size, cap)) { return; } final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); // retry while helping enqueue pending Reference objects // which includes executing pending Cleaner(s) which includes // Cleaner(s) that free direct buffer memory while (jlra.tryHandlePendingReference()) { if (tryReserveMemory(size, cap)) { return; } } // trigger VM's Reference processing System.gc(); // a retry loop with exponential back-off delays // (this gives VM some time to do it's job) boolean interrupted = false; try { long sleepTime = 1; int sleeps = 0; while (true) { if (tryReserveMemory(size, cap)) { return; } if (sleeps >= MAX_SLEEPS) { break; } if (!jlra.tryHandlePendingReference()) { try { Thread.sleep(sleepTime); sleepTime <<= 1; sleeps++; } catch (InterruptedException e) { interrupted = true; } } } // no luck throw new OutOfMemoryError("Direct buffer memory"); } finally { if (interrupted) { // don't swallow interrupts Thread.currentThread().interrupt(); } } }
-
maxMemory–判断oom的重要依据
maxMemory = VM.maxDirectMemory()
-
public static long maxDirectMemory() { return directMemory; }
-
public static void saveAndRemoveProperties(Properties var0) { if (booted) { throw new IllegalStateException("System initialization has completed"); } else { savedProps.putAll(var0); String var1 = (String)var0.remove("sun.nio.MaxDirectMemorySize"); if (var1 != null) { if (var1.equals("-1")) { directMemory = Runtime.getRuntime().maxMemory(); } else { long var2 = Long.parseLong(var1); if (var2 > -1L) { directMemory = var2; } } } var1 = (String)var0.remove("sun.nio.PageAlignDirectMemory"); if ("true".equals(var1)) { pageAlignDirectMemory = true; } var1 = var0.getProperty("sun.lang.ClassLoader.allowArraySyntax"); allowArraySyntax = var1 == null ? defaultAllowArraySyntax : Boolean.parseBoolean(var1); var0.remove("java.lang.Integer.IntegerCache.high"); var0.remove("sun.zip.disableMemoryMapping"); var0.remove("sun.java.launcher.diag"); var0.remove("sun.cds.enableSharedLookupCache"); } }
-
System这个类加载时,这个静态方法会被显示调用
-
directMemory = Runtime.getRuntime().maxMemory();
-
maxMemory是一个native方法
-
java中的类Runtime,在c中是Java_java_lang_Runtime_maxMemory,对应的文件是Runtime.c
-
Runtime.c
-
Java_java_lang_Runtime_maxMemory(JNIEnv *env, jobject this) { return JVM_MaxMemory(); }
-
JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void)) JVMWrapper("JVM_MaxMemory"); size_t n = Universe::heap()->max_capacity(); return convert_size_t_to_jlong(n); JVM_END
-
size_t GenCollectedHeap::max_capacity() const { size_t res = 0; for (int i = 0; i < _n_gens; i++) { res += _gens[i]->max_capacity(); } return res; }
- res是实际使用内存
- gens包括新生代和老年代
-
size_t DefNewGeneration::max_capacity() const { const size_t alignment = GenCollectedHeap::heap()->collector_policy()->space_alignment(); const size_t reserved_bytes = reserved().byte_size(); return reserved_bytes - compute_survivor_size(reserved_bytes, alignment); }
- 新生代大小=总大小-survivor区的大小
总结
- JVM门槛很高,体系过多,最好是根据场景切入来阅读源码
- 比如动态年龄判断