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() {@Overridepublic 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运行结果:falsetruetruefalse
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 /><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 /><br />从上述信息可以看出:- `String s4 = s1 + s2 ;`等同于`new StringBuffer().append(s1).append(s2);`- 上述编号`24`,可以看出调用了`StringBuffer#toString()`方法,说明是将拼接的参数的`StringBuffer`对象转换为`String`对象,查看`StringBuffer`源码,可以看到其实是重现创建了一个`String`对象(源码如下所示):<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); //falseSystem.out.println(intern == s2); //trueSystem.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); // falseSystem.out.println(s1==intern); //trueSystem.out.println(s2==intern); // false
} }
**回头看看**[**5.5.1**](#dqeNM)**的面试题。**<a name="HJgqn"></a>### 5.5.7. StringTable所处位置**- 1.8:-Xmx10m -XX:-UseGCOverheadLimit- 1.6:-XX:MaxPermSize=10m如果怎么StringTable所处的错误?**根据错误提示判定所处位置。**- 1.8 错误提示:__<br />如果不设置:-XX:-UseGCOverheadLimit(即不关掉开关)出现以下错误:<br /><br />UseGCOverheadLimit:通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生。Sun 官方对此的定义是:“并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。- 1.6错误提示:<br />**测试代码:**```javaimport 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 />2. 入池,即将`stringList.add(line)``;`改为`stringList.add(line.intern())``;`运行结果:(String占比21.5%,char占比35.5%)<br /><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 /><br />**那么为什么NIO要比传统IO速度更快呢?也就是说为什么使用了ByteBuffer,或者说直接内存,大内存的读写效率更高。**<a name="jnOSY"></a>### 5.6.3. 使用直接内存的好处<a name="seXV0"></a>#### 5.6.3.1. 文件读写的过程1. IOJava本身并不具备磁盘读写的能力,它要调用磁盘读写,必须调用操作系统的操作磁盘的函数,也就是说从Java的方法调用到了本地方法,CPU的运行状态从:Java的用户态切换到了操作系统的内核态,其次,内存这边也会有一些相关的操作:当切换到内核态之后,就可以读写磁盘文件了,一次一次的读进来,然后放在操作系统的内存中划分出一块系统缓冲区中,但是系统缓冲区的数据,Java代码是不能直接读取的,所以Java这边会在堆内存中开辟一块Java缓冲区byte[],Java代码要想读数据,必须将数据从系统缓冲区读入到Java缓冲区,然后再到用户态,再用输出流读取出数据。反复进行读写读写。<br /><br />但是我们发现一个问题:有两块缓冲区,一块是系统缓存区,一块是Java缓冲区,也就是说每次数据都要读取成两份,造成效率不是很高。2. NIO调用了ByteBuffer.allocateDirect(),也就还是说在操作系统上开辟了一块直接内存,这块内存Java代码可以操作,系统也可以操作,对于两段代码都是共享的。这样就比上述少了一次缓存区的操作,所以速度有了很大的提升。<br />_<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 /><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. 分配内存2. 释放内存<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去手动释放直接内存。
**
