5. 方法区

image.png

5.1. 定义

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

5.2. 方法区的组成

5.2.1. HotSpot 1.6

image.png

5.2.2. HotSpot 1.8

image.png
元空间使用的是本地内存,原则上还是没有设置上限的,要想演示方法区内存溢出,需要设置虚拟机参数:
-XX:MaxMetaspaceSize=10m

5.3. 方法区内存溢出

  • 1.8以前会导致永久代内存溢出
  • 1.8之后会导致元空间内存溢出

问题:方法区就是用来存储类的相关数据呢,类能有多少,它就怎么能导致内存溢出呢?

5.3.1. 模拟方法区内存溢出

5.3.1.1. 使用原生Java

  1. //因为无法打包,故而不实用Java自带的
  2. //import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
  3. //import jdk.internal.org.objectweb.asm.Opcodes;
  4. import org.springframework.asm.ClassWriter;
  5. import org.springframework.asm.Opcodes;
  6. /**
  7. * 演示元空间内存溢出
  8. * -XX:MaxMetaspaceSize=8m
  9. * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
  10. * @since : 2020/4/10 12:04
  11. */
  12. public class Demo12 extends ClassLoader{ //可以用来加载类的二进制字节码
  13. public static void main(String[] args) {
  14. int j = 0;
  15. try {
  16. Demo12 demo12 = new Demo12() ;
  17. for (int i = 0; i < 10000; i++,j++) {
  18. //ClassWriter作用是生成类的二进制字节码
  19. ClassWriter classWriter = new ClassWriter(0);
  20. // 版本号、public、类名、包名、父类、接口
  21. classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
  22. // 返回 byte[]
  23. byte [] bytes = classWriter.toByteArray();
  24. //执行类的加载
  25. demo12.defineClass("Class"+i,bytes,0,bytes.length);
  26. }
  27. }catch (Exception e){
  28. throw e ;
  29. } finally {
  30. System.out.println(j);
  31. }
  32. }
  33. }

运行结果:
image.png

5.3.1.2. 借助cglib

  1. import org.springframework.cglib.proxy.Enhancer;
  2. import org.springframework.cglib.proxy.MethodInterceptor;
  3. import org.springframework.cglib.proxy.MethodProxy;
  4. import java.lang.reflect.Method;
  5. /**
  6. * -XX:MaxMetaspaceSize=8m
  7. * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
  8. * @since : 2020/4/10 12:35
  9. */
  10. public class Demo13 {
  11. public static void main(String[] args) {
  12. while (true){
  13. Enhancer enhancer = new Enhancer();
  14. enhancer.setSuperclass(OMMObject.class);
  15. enhancer.setUseCache(false);
  16. enhancer.setCallback(new MethodInterceptor() {
  17. @Override
  18. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
  19. return methodProxy.invokeSuper(objects,args);
  20. }
  21. });
  22. enhancer.create();
  23. }
  24. }
  25. }
  26. class OMMObject{
  27. }

运行结果:
image.png
有人会认为上述演示是我们自己频繁、大量加载类,并且设置了元空间造成方法区内存溢出的,那实际项目中肯定不会出现方法区内存溢出的现象?其实这是一个错误的认识,实际项目中有大量的动态生产class,加载类的情景,因为我们用过:Spring、Mybatis,而这些框架中它们都用了字节码技术,比如Spring、Mybatis它们都用了Cglib这门技术,Spring用它生成代理类,这些代理类是Spring AOP的核心,而Mybatis用它来生成Mapper接口的实现类。故而使用不当都会导致方法区内存溢出。

5.4. 运行时常量池

要知道对运行时常量池的概念有所了解,首先要了解什么是常量池。

5.4.1. 常量池(Constant pool

演示代码:

  1. /**
  2. * 理解什么是常量池
  3. * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
  4. * @since : 2020/4/10 14:20
  5. */
  6. public class Demo14 {
  7. public static void main(String[] args) {
  8. System.out.println("hello world");
  9. }
  10. }

Java类编译之后的二进制字节码包含:类基本信息、常量池(Constant pool)、类方法定义(包含了虚拟机指令),因为这些数据不利于人类的查看,所以可以使用javap进行反编译。
使用javap -v Demo14显示反编译信息如下所示:
image.png
执行流程如下所示:
image.png
故而可以得出如下结论:
常量池:就是一张常量表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。

5.4.2. 运行时常量池定义

运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放在运行时常量池,并把里面的符号地址变为真实地址。

5.5. StringTable

StringTable的特性:

  1. 反编译:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586509326092-4b13b851-e0c3-4671-84f6-18663e94626b.png#align=left&display=inline&height=442&margin=%5Bobject%20Object%5D&name=image.png&originHeight=884&originWidth=896&size=89919&status=done&style=shadow&width=448)<br />**总结:**
  2. - 常量池中的信息,都会被加载到运行时常量池中,这是 abab都是常量池中的符号,还没有变为Java字符串对象
  3. - 只有在执行到该行程序的时候才会把a符号变为"a"字符串对象,而这个过程细节操作为:
  4. - **首先拿着"a"String Table中查找,有没有"a",如果有就直接把地址映射到这个对象上,如果没有就在String Table中添加一个**(String Table其实就是一个hashtable,并且不能扩容)。
  5. <a name="cfXyk"></a>
  6. ### 5.5.3. 解读`String s4 = s1+s2`
  7. 使用`javap -v Demo16`得到如下信息:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586511185005-70da908b-2a9b-49d3-a23d-bde50de11931.png#align=left&display=inline&height=313&margin=%5Bobject%20Object%5D&name=image.png&originHeight=800&originWidth=1906&size=131248&status=done&style=shadow&width=746)<br />从上述信息可以看出:
  8. - `String s4 = s1 + s2 ;`等同于`new StringBuffer().append(s1).append(s2);`
  9. - 上述编号`24`,可以看出调用了`StringBuffer#toString()`方法,说明是将拼接的参数的`StringBuffer`对象转换为`String`对象,查看`StringBuffer`源码,可以看到其实是重现创建了一个`String`对象(源码如下所示):
  10. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586511653334-b1e21ebd-1927-4888-94db-14d534e108f5.png#align=left&display=inline&height=134&margin=%5Bobject%20Object%5D&name=image.png&originHeight=268&originWidth=1030&size=42189&status=done&style=shadow&width=515)<br />故而,以下程序输出的是:false
  11. ```java
  12. /**
  13. * 解读 String s4 = s1+s2 ;
  14. * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
  15. * @since : 2020/4/10 17:30
  16. */
  17. public class Demo16 {
  18. public static void main(String[] args) {
  19. String s1 = "a" ;
  20. String s2 = "b" ;
  21. String s3 = "ab" ;
  22. String s4 = s1+s2 ; // 使用javap可以看出,这句代码其实等同于 new StringBuffer().append(s1).append(s2);
  23. System.out.println(s3 == s4);
  24. }
  25. }

结论:

  • 字符串变量拼接的原理是StringBuffer(1.8)

    5.5.4. 解读String s5 = "a"+"b"(涉及到编译器的优化)

    使用javap -v Demo17得到如下信息:
    image.png
    从上述信息可以看出:

  • String s5 = “a”+”b”,是从String Table中直接拿的“ab”

产生的疑问:**为什么直接找到的是ab,而不是先找a,后找b,然后拼接呢?而且直接找“ab”是怎么做到的呢?
其实这是,javac在编译器的优化,因为都是不变的常量,故而结果在编译器已经确定为“ab”。
故而:System.out.println(_s3 == s5)_; 的运行结果是true。
结论:**

  • 字符串常量拼接的原理是编译器优化
  • 利用串池的机制,来避免重复创建字符串对象

    5.5.5. 验证:字符串的延迟加载

    首次使用才将常量池中的字符串符号变成对象,
  1. 执行到断点1(可以看到内存中本身有1480个字符串对象)

image.png

  1. 执行到断点2(增加了10个,目前是1491个)

image.png

  1. 执行到S20(此时个数还是1491个,并没有多10个)

image.png
结论:

  • 常量池中的字符串仅是符号,第一次用到才变为对象

    5.5.6. String的intern方法

    可以使用intern方法,主动将串池中没有的字符串放入串池

  • 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。 ```java import org.junit.Test;

/**

  • 验证:可以使用intern方法,主动将串池中没有的字符串放入串池
  • 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。
  • @author : gnehcgnaw
  • @since : 2020/4/10 21:44 */ public class Demo19 { @Test public void test1(){

    1. String s1 = new String ("abc");
    2. String intern = s1.intern();
    3. String s2 = "abc" ;
    4. System.out.println(s1==intern); //false
    5. System.out.println(intern == s2); //true
    6. System.out.println(s1 == s2); //false

    }

    @Test public void test2(){

    1. String s1 = "abc" ;
    2. String s2 = new String("abc");
    3. String intern = s2.intern();
    4. System.out.println(s1==s2); // false
    5. System.out.println(s1==intern); //true
    6. System.out.println(s2==intern); // false

    } }

  1. **回头看看**[**5.5.1**](#dqeNM)**的面试题。**
  2. <a name="HJgqn"></a>
  3. ### 5.5.7. StringTable所处位置
  4. **![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586528439816-2221960a-af05-44b4-9af3-7b0bde7a5633.png#align=left&display=inline&height=502&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1235&originWidth=1836&size=679845&status=done&style=shadow&width=746)
  5. - 1.8:-Xmx10m -XX:-UseGCOverheadLimit
  6. - 1.6:-XX:MaxPermSize=10m
  7. 如果怎么StringTable所处的错误?**根据错误提示判定所处位置。**
  8. - 1.8 错误提示:
  9. _![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586529165349-4c165484-53cc-40eb-9179-4435505d40e7.png#align=left&display=inline&height=144&margin=%5Bobject%20Object%5D&name=image.png&originHeight=288&originWidth=1860&size=61088&status=done&style=none&width=930)_<br />如果不设置:-XX:-UseGCOverheadLimit(即不关掉开关)出现以下错误:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586530222404-dfc559ba-ad56-4b17-ac33-a2831c448e99.png#align=left&display=inline&height=170&margin=%5Bobject%20Object%5D&name=image.png&originHeight=340&originWidth=1134&size=56955&status=done&style=shadow&width=567)<br />UseGCOverheadLimit:通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生。Sun 官方对此的定义是:“并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。
  10. - 1.6错误提示:
  11. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586529209593-a2aeae66-e99f-42e8-a780-95818e70dc3c.png#align=left&display=inline&height=81&margin=%5Bobject%20Object%5D&name=image.png&originHeight=162&originWidth=837&size=165806&status=done&style=shadow&width=419)<br />**测试代码:**
  12. ```java
  13. import java.util.ArrayList;
  14. import java.util.List;
  15. /**
  16. *
  17. * • 1.8:-Xmx10m -XX:-UseGCOverheadLimit
  18. * • 1.6:-XX:MaxPermSize=10m
  19. * 如果怎么StringTable所处的错误?根据错误提示判定所处位置。
  20. *
  21. * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
  22. * @since : 2020/4/10 22:28
  23. */
  24. public class Demo20 {
  25. public static void main(String[] args) {
  26. List<String> list = new ArrayList<>();
  27. int i = 0 ;
  28. try{
  29. for (int j = 0; j < 260000; j++) {
  30. list.add(String.valueOf(j).intern());
  31. i++ ;
  32. }
  33. }catch (Throwable throwable){
  34. throwable.printStackTrace();
  35. }finally {
  36. System.out.println(i);
  37. }
  38. }
  39. }

5.5.8. StringTable垃圾回收

-Xmx10m
-XX:+PrintStringTableStatistics
-XX:+PrintGCDetails -verbose:gc
测试代码:

  1. /**
  2. * StringTable垃圾回收
  3. * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
  4. * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
  5. * @since : 2020/4/10 23:49
  6. */
  7. public class Demo21 {
  8. public static void main(String[] args) {
  9. int i = 0;
  10. try {
  11. }catch (Throwable throwable){
  12. throwable.printStackTrace();
  13. }finally {
  14. System.out.println(i);
  15. }
  16. }
  17. }

测试结果:
image.png

  1. GC信息,只有一个Heap说明没有发生GC操作;
  2. SymbolTable的统计数据,symbolic references in Runtime Constant Pool(运行时常量池中的符号引用)
  3. StringTable的统计数据:StringTable本质是一个HashTable,buckets是桶的个数,entries指的是键个数,literals是字面量个数。如上图所示在不发生GC的时候默认有874对。

修改程序:在try中添加如下程序:

  1. for (int j = 0; j <100000 ; j++) {
  2. String.valueOf(j).intern();
  3. }

运行结果如下所示:(内存分配失败引发了GC,并且StringTable的entries和literals发生了变化)
image.png

5.5.9. StringTable性能调优

StringTable的底层是一个HashTable,HashTable的性能其实跟它的大小密切相关,如果HashTable它的桶的个数越多,那么元素相对就比较分散,Hash碰撞的几率就会减少,查找的速度也会变快,返之,HashTable的桶的个数越少,元素就越集中,导致链表就越长,查找的速度就也会受影响。
下面给出一个案例,来进行StringTable性能调优,其实就是调整HashTable的桶的个数。(因为放入HashTable的时候首先要查找,因为不能存入重复元素)

5.5.9.1. 调整-XX:StringTableSize=桶的个数

代码:(程序总共的文件是Mac的字典表,位于/usr/share/dict下的words文件)

  1. /**
  2. * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
  3. * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
  4. * @since : 2020/4/11 01:24
  5. */
  6. public class Demo22 {
  7. public static void main(String[] args) {
  8. try {
  9. BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/mac.words"),"utf-8"));
  10. String line = null;
  11. long start = System.nanoTime();
  12. while (true){
  13. line = bufferedReader.readLine();
  14. if (line==null){
  15. break;
  16. }
  17. line.intern();
  18. }
  19. System.out.println("cost:"+(System.nanoTime()-start)/1000000);
  20. } catch (FileNotFoundException e) {
  21. e.printStackTrace();
  22. } catch (UnsupportedEncodingException e) {
  23. e.printStackTrace();
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  1. 添加参数-XX:+PrintStringTableStatistics,运行结果如下:

image.png
桶的默认值为60013个,存储时间为141毫秒。

  1. 在1的基础上添加-XX:StringTableSize=1099,(最小为1099)运行结果如下:

image.png
说明桶的数量越多,读取速度越快。
那么什么时候考虑将字符串对象放出StringTable中呢?(推特案例)

5.5.9.2. 考虑将字符串对象是否入池

  1. 不入池 ``` import java.io.*; import java.util.ArrayList; import java.util.List;

/**

  • @author : gnehcgnaw
  • @since : 2020/4/11 01:52 */ public class Demo23 { public static void main(String[] args) {

    1. List<String> stringList = new ArrayList<>();
    2. try {
    3. System.in.read();
    4. } catch (IOException e) {
    5. e.printStackTrace();
    6. }
    7. for (int i = 0; i < 10 ; i++) {
    8. BufferedReader bufferedReader = null;
    9. try {
    10. bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/mac.words"),"utf-8"));
    11. String line = null ;
    12. long start = System.nanoTime();
    13. while (true){
    14. line = bufferedReader.readLine();
    15. if (line == null){
    16. break;
    17. }
    18. stringList.add(line);
    19. }
    20. System.out.println("cost:"+(System.nanoTime()-start)/1000000);
    21. } catch (UnsupportedEncodingException e) {
    22. e.printStackTrace();
    23. } catch (FileNotFoundException e) {
    24. e.printStackTrace();
    25. } catch (IOException e) {
    26. e.printStackTrace();
    27. }
    28. }
    29. try {
    30. System.in.read();
    31. } catch (IOException e) {
    32. e.printStackTrace();
    33. }

    } }

  1. 运行结果:(String占比34.5%,char占比55.2%)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586543230203-8c5276f9-173b-475e-b559-2a01a5ada95f.png#align=left&display=inline&height=943&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1886&originWidth=3360&size=647828&status=done&style=none&width=1680)
  2. 2. 入池,即将`stringList.add(line)``;`改为`stringList.add(line.intern())``;`
  3. 运行结果:(String占比21.5%,char占比35.5%)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586543438790-84249655-5998-4565-81f9-c3a9608c822e.png#align=left&display=inline&height=943&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1886&originWidth=3360&size=660113&status=done&style=none&width=1680)<br />**看到堆内存占用减少**<br />结论:如果你的应用里有大量的字符串,而且可能存在重复的问题,那么我们可以让字符串入池,来减少字符串对象个数,减少堆内存的使用。
  4. <a name="TtdLz"></a>
  5. ## 5.6. 直接内存(操作系统内存)
  6. <a name="XkNGO"></a>
  7. ### 5.6.1. 定义
  8. Direct Memory
  9. - 常见于NIOByteBuffer)操作,用于数据缓冲区【5.6.2
  10. - 分配回收成本较高,但是读写性能高【5.6.3.1
  11. - 不受JVM内存回收管理
  12. <a name="vbrjM"></a>
  13. ### 5.6.2. IO与NIO性能对比

import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel;

/**

  • NIO 与 IO性能对比
  • @author : gnehcgnaw
  • @since : 2020/4/11 02:41 / public class Demo24 { static final String FROM = “/Users/gnehcgnaw/Downloads/课件笔记源码资料.zip” ; static final String TO = “/Users/gnehcgnaw/Desktop/课件笔记源码资料.zip” ; static final int _1MB = 10241024 ; public static void main(String[] args) {

    1. io();
    2. directBuffer();

    }

    private static void directBuffer() {

    1. long start = System.nanoTime();
    2. try {
    3. FileChannel from = new FileInputStream(FROM).getChannel();
    4. FileChannel to = new FileOutputStream(TO).getChannel();
    5. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1MB);
    6. while (true){
    7. int len = from.read(byteBuffer);
    8. if (len == -1){
    9. break;
    10. }
    11. byteBuffer.flip();
    12. to.write(byteBuffer);
    13. byteBuffer.clear();
    14. }
    15. } catch (FileNotFoundException e) {
    16. e.printStackTrace();
    17. } catch (IOException e) {
    18. e.printStackTrace();
    19. }
    20. long end = System.nanoTime() ;
    21. System.out.println("nio 用时 :"+(end -start)/1000_000.0);

    }

    private static void io() {

    1. long start = System.nanoTime();
    2. try {
    3. FileInputStream fileInputStream = new FileInputStream(FROM);
    4. FileOutputStream fileOutputStream = new FileOutputStream(TO);
    5. byte[] bytes = new byte[_1MB];
    6. while (true){
    7. int len = fileInputStream.read(bytes);
    8. if (len == -1){
    9. break;
    10. }
    11. fileOutputStream.write(bytes,0,len);
    12. }
    13. } catch (FileNotFoundException e) {
    14. e.printStackTrace();
    15. } catch (IOException e) {
    16. e.printStackTrace();
    17. }
    18. long end = System.nanoTime() ;
    19. System.out.println("io 用时 :"+(end -start)/1000_000.0);

    } }

  1. 运行结果如下所示:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586544730572-43f3d23c-f92f-4a11-b7dd-835882303903.png#align=left&display=inline&height=129&margin=%5Bobject%20Object%5D&name=image.png&originHeight=258&originWidth=1158&size=32054&status=done&style=shadow&width=579)<br />**那么为什么NIO要比传统IO速度更快呢?也就是说为什么使用了ByteBuffer,或者说直接内存,大内存的读写效率更高。**
  2. <a name="jnOSY"></a>
  3. ### 5.6.3. 使用直接内存的好处
  4. <a name="seXV0"></a>
  5. #### 5.6.3.1. 文件读写的过程
  6. 1. IO
  7. Java本身并不具备磁盘读写的能力,它要调用磁盘读写,必须调用操作系统的操作磁盘的函数,也就是说从Java的方法调用到了本地方法,CPU的运行状态从:Java的用户态切换到了操作系统的内核态,其次,内存这边也会有一些相关的操作:当切换到内核态之后,就可以读写磁盘文件了,一次一次的读进来,然后放在操作系统的内存中划分出一块系统缓冲区中,但是系统缓冲区的数据,Java代码是不能直接读取的,所以Java这边会在堆内存中开辟一块Java缓冲区byte[],Java代码要想读数据,必须将数据从系统缓冲区读入到Java缓冲区,然后再到用户态,再用输出流读取出数据。反复进行读写读写。<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586545515096-d5567a76-9322-4e1d-8f85-85b674165aa5.png#align=left&display=inline&height=427&margin=%5Bobject%20Object%5D&name=image.png&originHeight=918&originWidth=1265&size=316995&status=done&style=shadow&width=588)<br />但是我们发现一个问题:有两块缓冲区,一块是系统缓存区,一块是Java缓冲区,也就是说每次数据都要读取成两份,造成效率不是很高。
  8. 2. NIO
  9. 调用了ByteBuffer.allocateDirect(),也就还是说在操作系统上开辟了一块直接内存,这块内存Java代码可以操作,系统也可以操作,对于两段代码都是共享的。这样就比上述少了一次缓存区的操作,所以速度有了很大的提升。<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586545757854-4749127b-3a08-45dc-b946-16574a2b1cf5.png#align=left&display=inline&height=1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=2&originWidth=2&size=157&status=done&style=none&width=1)![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586545763872-83660141-c726-4c90-809e-f93ed9a1cba9.png#align=left&display=inline&height=463&margin=%5Bobject%20Object%5D&name=image.png&originHeight=925&originWidth=1265&size=372217&status=done&style=shadow&width=633)![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586545750823-0748f589-4638-4dcb-aed7-9393507381e9.png#align=left&display=inline&height=1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=2&originWidth=2&size=157&status=done&style=none&width=1)_
  10. <a name="ZiREA"></a>
  11. ### 5.6.4. 内存溢出
  12. Direct Memory,不受JVM内存回收管理,也就是说Java不会直接回收直接内存的空间,那么Direct Memory会发生内存溢出呢?<br />例子:

import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List;

/**

  • 演示直接内存溢出
  • @author : gnehcgnaw
  • @since : 2020/4/11 03:17 / public class Demo25 { static int _100Mb = 10241024*100 ; public static void main(String[] args) {

    1. List<ByteBuffer> list = new ArrayList<>();
    2. int i = 0 ;
    3. try{
    4. while (true){
    5. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
    6. list.add(byteBuffer);
    7. i++;
    8. }
    9. }catch (Throwable throwable){
    10. throwable.printStackTrace();
    11. }finally {
    12. System.out.println(i);
    13. }

    }

}

  1. 运行结果:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586546745243-29de0d94-c770-4deb-bca5-99fd21308a55.png#align=left&display=inline&height=164&margin=%5Bobject%20Object%5D&name=image.png&originHeight=328&originWidth=1108&size=71059&status=done&style=shadow&width=554)<br />说明直接内存会发生内存溢出的问题。
  2. <a name="cTqI4"></a>
  3. ### 5.6.5. 释放内存
  4. <a name="BazcB"></a>
  5. #### 5.6.5.1. 分配和释放内存示例演示
  6. 既然直接内存,不受JVM的内存管理,那么它分配的内存会不会被正确回收呢?那它的底层是怎么实现的呢?<br />示例:

import java.io.IOException; import java.nio.ByteBuffer;

/**

  • @author : gnehcgnaw
  • @since : 2020/4/11 03:31 / public class Demo26 { static int _1Gb = 10241024*1024 ;

    public static void main(String[] args) {

    1. try {
    2. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
    3. System.out.println("分配内存");
    4. System.in.read() ;
    5. System.out.println("开始释放");
    6. byteBuffer = null ;
    7. System.gc();
    8. System.in.read() ;
    9. System.out.println("运行结束");
    10. } catch (IOException e) {
    11. e.printStackTrace();
    12. }

    } }

  1. **检测就不能使用Java的检测工具了,因为Java的检测工具只能查看堆内存、栈内存、元空间呀这些内存信息,直接内存是不受Java管理的,要看到内存的变化,需要看操作系统的内存占用情况。**<br />运行结果如下所示:
  2. 1. 分配内存
  3. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586547702553-60b266c1-21c1-4d93-b08f-268fada81cf7.png#align=left&display=inline&height=497&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1280&originWidth=1920&size=454199&status=done&style=shadow&width=746)
  4. 2. 释放内存
  5. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1586547721393-bac6923b-fa1f-46d9-abf4-358a5d1ee17c.png#align=left&display=inline&height=497&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1280&originWidth=1920&size=515038&status=done&style=shadow&width=746)<br />**发现内存被释放掉了,那其实会产生一个误区,是不是因为垃圾回收导致直接内存被释放掉了呢?而之前定义还说垃圾回收不会管理直接内存,那这是怎么回事呢?**
  6. <a name="WfgoE"></a>
  7. #### 5.6.5.2.直接内存的分配和释放原理(Unsafe)

import java.io.IOException; import java.lang.reflect.Field;

/**

  • 直接内存的分配和释放
  • @author : gnehcgnaw
  • @since : 2020/4/11 03:51 / public class Demo27 { static int _1Gb = 10241024*1024 ;

    public static void main(String[] args) {

    1. try {
    2. Unsafe unsafe = getUnsafe();
    3. long base = unsafe.allocateMemory(_1Gb);
    4. unsafe.setMemory(base,0,(byte)0);
    5. System.in.read();
    6. unsafe.freeMemory(base);
    7. System.in.read();
    8. } catch (IOException e) {
    9. e.printStackTrace();
    10. }

    }

    public static Unsafe getUnsafe(){

    1. try {
    2. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    3. theUnsafe.setAccessible(true);
    4. Unsafe unsafe = (Unsafe) theUnsafe.get(null);
    5. return unsafe ;
    6. } catch (NoSuchFieldException e) {
    7. throw new RuntimeException(e);
    8. } catch (IllegalAccessException e) {
    9. throw new RuntimeException(e);
    10. }

    } }

``` 结合系统的任务管理器,查看程序创建直接内存和释放直接内存,由上述代码可以看出直接内存的分配和释放都需要显示的调用Unsafe中的分配内存和释放内存的方法。

5.6.5.3. 分析ByteBuffer源码,看如何与Unsafe关联

ByteBuffer的子类是DirectByteBuffer,在DirectByteBuffer的构造中有如下代码:
image.png
在分配内存的时候确实调用了unsafe.allocateMemory()方法。
Deallocator是一个回调任务对象,因为它实现了Runnable接口,在其run()方法中,调用了释放内存的方法:
image.png
关键点就集中到了Cleaner类,在Java类库中是一个特殊的类型,叫虚引用类型,它的特点是当它所关联的对象被垃圾回收时,那么Cleaner就会出发虚引用的clean()方法,然后去执行关联对象(任务对象)的run()方法的,而这个clean()方法不是由主线程执行的,而是由后台的一个referenceHandler线程执行。
image.png
结论:直接内存的释放借助了Java四大应用中的虚引用——**Cleaner**类。
总结:

  • 使用了**Unsafe**对象完成直接内存的分配回收,并且回收需要主动调用**freeMemory**方法;
  • **ByteBuffer**的实现类内部,使用了**Cleaner**(虚引用)来检测ByteBuffer对象,一旦**ByteBuffer**对象被垃圾回收,那么就会有**ReferenceHandler**(**守护线程**)线程通过**CLeaner****clean**方法调用**freeMemory**来释放直接内存。

    5.6.5.4. 禁用显示回收对直接内存的影响

    那么就会有一个问题,在做JVM的调优时,经常会加一个虚拟机参数:-XX:+DisableExplicitGC (关闭显示的GC),意思即使让代码中的System.gc()无效。【System.gc()是一个Full GC,不仅要回收新生代还要回收老年代,会造成程序的时间暂停时间长,故而加虚拟机参数禁用显示的GC调用。】
    禁用显示的GC调用,会对直接内存的内存释放产生了影响,那么如果释放直接内存呢?那就回到了直接内存的原理,使用Unsafe去手动释放直接内存。
    **