5. 方法区
5.1. 定义
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
5.2. 方法区的组成
5.2.1. HotSpot 1.6
5.2.2. HotSpot 1.8
元空间使用的是本地内存,原则上还是没有设置上限的,要想演示方法区内存溢出,需要设置虚拟机参数:-XX:MaxMetaspaceSize=10m
。
5.3. 方法区内存溢出
- 1.8以前会导致永久代内存溢出
- 1.8之后会导致元空间内存溢出
问题:方法区就是用来存储类的相关数据呢,类能有多少,它就怎么能导致内存溢出呢?
5.3.1. 模拟方法区内存溢出
5.3.1.1. 使用原生Java
//因为无法打包,故而不实用Java自带的
//import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
//import jdk.internal.org.objectweb.asm.Opcodes;
import org.springframework.asm.ClassWriter;
import org.springframework.asm.Opcodes;
/**
* 演示元空间内存溢出
* -XX:MaxMetaspaceSize=8m
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/10 12:04
*/
public class Demo12 extends ClassLoader{ //可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo12 demo12 = new Demo12() ;
for (int i = 0; i < 10000; i++,j++) {
//ClassWriter作用是生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
// 版本号、public、类名、包名、父类、接口
classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
// 返回 byte[]
byte [] bytes = classWriter.toByteArray();
//执行类的加载
demo12.defineClass("Class"+i,bytes,0,bytes.length);
}
}catch (Exception e){
throw e ;
} finally {
System.out.println(j);
}
}
}
5.3.1.2. 借助cglib
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* -XX:MaxMetaspaceSize=8m
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/10 12:35
*/
public class Demo13 {
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OMMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(objects,args);
}
});
enhancer.create();
}
}
}
class OMMObject{
}
运行结果:
有人会认为上述演示是我们自己频繁、大量加载类,并且设置了元空间造成方法区内存溢出的,那实际项目中肯定不会出现方法区内存溢出的现象?其实这是一个错误的认识,实际项目中有大量的动态生产class,加载类的情景,因为我们用过:Spring、Mybatis,而这些框架中它们都用了字节码技术,比如Spring、Mybatis它们都用了Cglib这门技术,Spring用它生成代理类,这些代理类是Spring AOP的核心,而Mybatis用它来生成Mapper接口的实现类。故而使用不当都会导致方法区内存溢出。
5.4. 运行时常量池
要知道对运行时常量池的概念有所了解,首先要了解什么是常量池。
5.4.1. 常量池(Constant pool
)
演示代码:
/**
* 理解什么是常量池
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/10 14:20
*/
public class Demo14 {
public static void main(String[] args) {
System.out.println("hello world");
}
}
Java类编译之后的二进制字节码包含:类基本信息、常量池(Constant pool
)、类方法定义(包含了虚拟机指令),因为这些数据不利于人类的查看,所以可以使用javap
进行反编译。
使用javap -v Demo14
显示反编译信息如下所示:
执行流程如下所示:
故而可以得出如下结论:
常量池:就是一张常量表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
5.4.2. 运行时常量池定义
运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放在运行时常量池,并把里面的符号地址变为真实地址。
5.5. StringTable
StringTable的特性:
- 常量池中的字符串仅是符号,第一次用到时才变为对象【5.5.5】
- 利用串池的机制,来避免重复创建字符串对象【5.5.4】
- 字符串变量拼接的原理是StringBuffer(1.8)【5.5.3】
- 字符串常量拼接的原理是编译器优化【5.5.4】
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
5.5.1. StringTable面试题
1.8运行结果:false
true
true
false
5.5.2. 常量池与String Table(串池)的关系
例子: ```java /**- 说明常量池和String Table的关系
- @author : gnehcgnaw
- @since : 2020/4/10 16:49
*/
public class Demo15 {
public static void main(String[] args) {
} }//常量池中的信息,都会被加载到运行时常量池中,这是 a、b、ab都是常量池中的符号,还没有变为Java字符串对象
//只有在执行到该行程序的时候才会把a符号变为"a"字符串对象,而这个过程细节操作为:
//首先拿着"a"到String Table中查找,有没有"a",如果有就直接把地址映射到这个对象上,如果没有就在String Table中添加一个,
//String Table其实就是一个hashtable,并且不能扩容
String s1 = "a" ;
String s2 = "b" ;
String s3 = "ab" ;
反编译:<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 />**总结:**
- 常量池中的信息,都会被加载到运行时常量池中,这是 a、b、ab都是常量池中的符号,还没有变为Java字符串对象
- 只有在执行到该行程序的时候才会把a符号变为"a"字符串对象,而这个过程细节操作为:
- **首先拿着"a"到String Table中查找,有没有"a",如果有就直接把地址映射到这个对象上,如果没有就在String Table中添加一个**(String Table其实就是一个hashtable,并且不能扩容)。
<a name="cfXyk"></a>
### 5.5.3. 解读`String s4 = s1+s2`
使用`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 />从上述信息可以看出:
- `String s4 = s1 + s2 ;`等同于`new StringBuffer().append(s1).append(s2);`
- 上述编号`24`,可以看出调用了`StringBuffer#toString()`方法,说明是将拼接的参数的`StringBuffer`对象转换为`String`对象,查看`StringBuffer`源码,可以看到其实是重现创建了一个`String`对象(源码如下所示):
![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
```java
/**
* 解读 String s4 = s1+s2 ;
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/10 17:30
*/
public class Demo16 {
public static void main(String[] args) {
String s1 = "a" ;
String s2 = "b" ;
String s3 = "ab" ;
String s4 = s1+s2 ; // 使用javap可以看出,这句代码其实等同于 new StringBuffer().append(s1).append(s2);
System.out.println(s3 == s4);
}
}
结论:
-
5.5.4. 解读
String s5 = "a"+"b"
(涉及到编译器的优化)使用
javap -v Demo17
得到如下信息:
从上述信息可以看出: String s5 = “a”+”b”,是从String Table中直接拿的“ab”
产生的疑问:**为什么直接找到的是ab,而不是先找a,后找b,然后拼接呢?而且直接找“ab”是怎么做到的呢?
其实这是,javac在编译器的优化,因为都是不变的常量,故而结果在编译器已经确定为“ab”。
故而:System.out.println(_s3 == s5)_; 的运行结果是true。
结论:**
- 执行到断点1(可以看到内存中本身有1480个字符串对象)
- 执行到断点2(增加了10个,目前是1491个)
- 执行到S20(此时个数还是1491个,并没有多10个)
结论:
-
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(){
String s1 = new String ("abc");
String intern = s1.intern();
String s2 = "abc" ;
System.out.println(s1==intern); //false
System.out.println(intern == s2); //true
System.out.println(s1 == s2); //false
}
@Test public void test2(){
String s1 = "abc" ;
String s2 = new String("abc");
String intern = s2.intern();
System.out.println(s1==s2); // false
System.out.println(s1==intern); //true
System.out.println(s2==intern); // false
} }
**回头看看**[**5.5.1**](#dqeNM)**的面试题。**
<a name="HJgqn"></a>
### 5.5.7. StringTable所处位置
**![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)
- 1.8:-Xmx10m -XX:-UseGCOverheadLimit
- 1.6:-XX:MaxPermSize=10m
如果怎么StringTable所处的错误?**根据错误提示判定所处位置。**
- 1.8 错误提示:
_![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%的堆内存。用来避免内存过小造成应用不能正常工作。
- 1.6错误提示:
![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 />**测试代码:**
```java
import java.util.ArrayList;
import java.util.List;
/**
*
* • 1.8:-Xmx10m -XX:-UseGCOverheadLimit
* • 1.6:-XX:MaxPermSize=10m
* 如果怎么StringTable所处的错误?根据错误提示判定所处位置。
*
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/10 22:28
*/
public class Demo20 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0 ;
try{
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++ ;
}
}catch (Throwable throwable){
throwable.printStackTrace();
}finally {
System.out.println(i);
}
}
}
5.5.8. StringTable垃圾回收
-Xmx10m
-XX:+PrintStringTableStatistics
-XX:+PrintGCDetails -verbose:gc
测试代码:
/**
* StringTable垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/10 23:49
*/
public class Demo21 {
public static void main(String[] args) {
int i = 0;
try {
}catch (Throwable throwable){
throwable.printStackTrace();
}finally {
System.out.println(i);
}
}
}
测试结果:
- GC信息,只有一个Heap说明没有发生GC操作;
- SymbolTable的统计数据,symbolic references in Runtime Constant Pool(运行时常量池中的符号引用)
- StringTable的统计数据:StringTable本质是一个HashTable,buckets是桶的个数,entries指的是键个数,literals是字面量个数。如上图所示在不发生GC的时候默认有874对。
修改程序:在try中添加如下程序:
for (int j = 0; j <100000 ; j++) {
String.valueOf(j).intern();
}
运行结果如下所示:(内存分配失败引发了GC,并且StringTable的entries和literals发生了变化)
5.5.9. StringTable性能调优
StringTable的底层是一个HashTable,HashTable的性能其实跟它的大小密切相关,如果HashTable它的桶的个数越多,那么元素相对就比较分散,Hash碰撞的几率就会减少,查找的速度也会变快,返之,HashTable的桶的个数越少,元素就越集中,导致链表就越长,查找的速度就也会受影响。
下面给出一个案例,来进行StringTable性能调优,其实就是调整HashTable的桶的个数。(因为放入HashTable的时候首先要查找,因为不能存入重复元素)
5.5.9.1. 调整-XX:StringTableSize=桶的个数
代码:(程序总共的文件是Mac的字典表,位于/usr/share/dict下的words文件)
/**
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/11 01:24
*/
public class Demo22 {
public static void main(String[] args) {
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/mac.words"),"utf-8"));
String line = null;
long start = System.nanoTime();
while (true){
line = bufferedReader.readLine();
if (line==null){
break;
}
line.intern();
}
System.out.println("cost:"+(System.nanoTime()-start)/1000000);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 添加参数
-XX:+PrintStringTableStatistics
,运行结果如下:
桶的默认值为60013个,存储时间为141毫秒。
- 在1的基础上添加
-XX:StringTableSize=1099
,(最小为1099)运行结果如下:
说明桶的数量越多,读取速度越快。
那么什么时候考虑将字符串对象放出StringTable中呢?(推特案例)
5.5.9.2. 考虑将字符串对象是否入池
- 不入池 ``` 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) {
List<String> stringList = new ArrayList<>();
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
for (int i = 0; i < 10 ; i++) {
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/mac.words"),"utf-8"));
String line = null ;
long start = System.nanoTime();
while (true){
line = bufferedReader.readLine();
if (line == null){
break;
}
stringList.add(line);
}
System.out.println("cost:"+(System.nanoTime()-start)/1000000);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
} }
运行结果:(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. 入池,即将`stringList.add(line)``;`改为`stringList.add(line.intern())``;`
运行结果:(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 />结论:如果你的应用里有大量的字符串,而且可能存在重复的问题,那么我们可以让字符串入池,来减少字符串对象个数,减少堆内存的使用。
<a name="TtdLz"></a>
## 5.6. 直接内存(操作系统内存)
<a name="XkNGO"></a>
### 5.6.1. 定义
Direct Memory
- 常见于NIO(ByteBuffer)操作,用于数据缓冲区【5.6.2】
- 分配回收成本较高,但是读写性能高【5.6.3.1】
- 不受JVM内存回收管理
<a name="vbrjM"></a>
### 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) {
io();
directBuffer();
}
private static void directBuffer() {
long start = System.nanoTime();
try {
FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1MB);
while (true){
int len = from.read(byteBuffer);
if (len == -1){
break;
}
byteBuffer.flip();
to.write(byteBuffer);
byteBuffer.clear();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime() ;
System.out.println("nio 用时 :"+(end -start)/1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try {
FileInputStream fileInputStream = new FileInputStream(FROM);
FileOutputStream fileOutputStream = new FileOutputStream(TO);
byte[] bytes = new byte[_1MB];
while (true){
int len = fileInputStream.read(bytes);
if (len == -1){
break;
}
fileOutputStream.write(bytes,0,len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime() ;
System.out.println("io 用时 :"+(end -start)/1000_000.0);
} }
运行结果如下所示:<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,或者说直接内存,大内存的读写效率更高。**
<a name="jnOSY"></a>
### 5.6.3. 使用直接内存的好处
<a name="seXV0"></a>
#### 5.6.3.1. 文件读写的过程
1. IO
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缓冲区,也就是说每次数据都要读取成两份,造成效率不是很高。
2. NIO
调用了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)_
<a name="ZiREA"></a>
### 5.6.4. 内存溢出
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) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0 ;
try{
while (true){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
}catch (Throwable throwable){
throwable.printStackTrace();
}finally {
System.out.println(i);
}
}
}
运行结果:<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 />说明直接内存会发生内存溢出的问题。
<a name="cTqI4"></a>
### 5.6.5. 释放内存
<a name="BazcB"></a>
#### 5.6.5.1. 分配和释放内存示例演示
既然直接内存,不受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) {
try {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配内存");
System.in.read() ;
System.out.println("开始释放");
byteBuffer = null ;
System.gc();
System.in.read() ;
System.out.println("运行结束");
} catch (IOException e) {
e.printStackTrace();
}
} }
**检测就不能使用Java的检测工具了,因为Java的检测工具只能查看堆内存、栈内存、元空间呀这些内存信息,直接内存是不受Java管理的,要看到内存的变化,需要看操作系统的内存占用情况。**<br />运行结果如下所示:
1. 分配内存
![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)
2. 释放内存
![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 />**发现内存被释放掉了,那其实会产生一个误区,是不是因为垃圾回收导致直接内存被释放掉了呢?而之前定义还说垃圾回收不会管理直接内存,那这是怎么回事呢?**
<a name="WfgoE"></a>
#### 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) {
try {
Unsafe unsafe = getUnsafe();
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base,0,(byte)0);
System.in.read();
unsafe.freeMemory(base);
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
public static Unsafe getUnsafe(){
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
return unsafe ;
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
} }
``` 结合系统的任务管理器,查看程序创建直接内存和释放直接内存,由上述代码可以看出直接内存的分配和释放都需要显示的调用Unsafe中的分配内存和释放内存的方法。
5.6.5.3. 分析ByteBuffer源码,看如何与Unsafe关联
ByteBuffer的子类是DirectByteBuffer,在DirectByteBuffer的构造中有如下代码:
在分配内存的时候确实调用了unsafe.allocateMemory()方法。
Deallocator是一个回调任务对象,因为它实现了Runnable接口,在其run()方法中,调用了释放内存的方法:
关键点就集中到了Cleaner类,在Java类库中是一个特殊的类型,叫虚引用类型,它的特点是当它所关联的对象被垃圾回收时,那么Cleaner就会出发虚引用的clean()方法,然后去执行关联对象(任务对象)的run()方法的,而这个clean()方法不是由主线程执行的,而是由后台的一个referenceHandler线程执行。
结论:直接内存的释放借助了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去手动释放直接内存。
**