文章目录
- 数据区解释
- 方法区
- 运行时常量池和串池解读
- 堆
- 堆内存的逻辑分区
- 垃圾回收机制
- 触发垃圾回收的时机
- 垃圾回收器版本
- 查看jvm使用的哪个版本的垃圾回收器
- MAT
- 导入过大的文件异常解决
- 虚拟机栈
- 局部变量表
- 本地方法栈
- 程序计数器
- 直接内存(物理内存)
- 常用命令和解决问题
- 堆内存转储
- jmap 工具
JVM运行时数据区图
从图中我们可以看出方法区和堆是属于所有线程共享的数据区,这里的方法区不是执行方法的地方,而是一个存放类信息的区域,虚拟机栈和本地方法栈是属于线程私有的区域。
数据区解释
方法区
如图中所述,但是在细分的话方法区还有个运行时常量池,运行时常量池是方法区的一部分。Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
常见异常:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
导致异常原因:加载类
过多,此处要注意是加载的类,而不是对象
代码案例
package jvm;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
/**
* 启动添加启动参数,为了方便测试出因为方法区
* 导致的内存溢出 -XX:MaxMetaspaceSize=8m
*/
public class MethodArea extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
MethodArea methodArea = new MethodArea();
for (int i = 0; i < 10000; i++, j++) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] code = classWriter.toByteArray();
methodArea.defineClass("Class" + i, code, 0, code.length);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(j);
}
}
}
运行时常量池和串池解读
可以先通过javap -v xxx.class
命令反编译class文件,-v表示打印详细信息
信息中的有Constant pool:
,此信息为常量池
,常量池就是一张符号表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息,运行时常量池
即当该类被加载,它的常量池信息就会放入运行时常量池,并符号表里的符号变为内存的真实地址
。
字符串池StringTable
简称串池,是保存字符串的区域,数据结构类似hashtable,
案例代码
package jvm;
public class StringPool {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
}
}
class文件反编译后的
由此可见s4是StringBuilder
生成的,是一个新的对象,而s5是直接用的s3的ab常量,为什么呢,因为编译期优化的结果,“a” + "b"的结果是可以在编译器得出的,所以会相加后再去串池中查找是否已经存在此串,而s1 + s2,是两个变量相加,运行时变量是会变化的,所以相加的结果是不能确定的,所以用StringBuilder
生成的新的字符串对象
。
堆
java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配
常见异常
java.lang.OutOfMemoryError 出现的原因是很多对象一直被占用虚拟机无法及时回收导致内存溢出
- 代码案例
package jvm;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
public class Demo2 {
public static void main(String[] args) throws Exception {
int i = 0;
try {
List<Map> list = new ArrayList<>();
while (true) {
Map<String,Object> map = new HashMap<>();
map.put("1",1);
map.put("1","lihessd我是沈");
map.put("1","上岛咖啡撒了房间爱丽丝代理费按说");
list.add(map);
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
往往我们开启Java程序都开启OOM打印快照,当出现OOM时,会生成一个hprof
格式的文件,可以通过jhat命令查看,但是一般此文件过大,使用此命令会出现不方便和卡的情况,可以使用MAT
开分析,
堆内存的逻辑分区
jvm默认:新生代和老年代比例为1:2,新生代又分为一个Eden和两个Survivor,Eden和Survivor又将新生代按8:1:1划分
以上比例都可以通过jvm参数调整
垃圾回收机制
垃圾回收期将内存分为三个区域,Eden区、Survivor和old区,分别翻译为新生代、幸存区和老年代区,
- Eden(新生代):新创建的对象都先在此区域
- Survivor(幸存者区):经过在新生代区GC后没有回收的对象进入此区域
- old区:在Survivor经过15次GC没有被回收的对象进入old区,次数这个阈值是可以设置的:
-XX:MaxTenuringThreshold
(默认是15),此区域一般为常用对象和大对象
触发垃圾回收的时机
- Young GC(Minor GC):大多数情况下,对象在年轻代中的Eden区进行分配,若Eden区没有足够空间,就会触发YGC
- Full GC(Major GC):FGC处理的区域包括新生代和老年代。
- System.gc():代码强行垃圾回收,提醒虚拟机进行垃圾回收,回不回收由虚拟机决定。若虚拟机决定回收,也不是立刻进行回收,它是
异步
的。
垃圾回收器版本
所有垃圾回收器的在处理垃圾回收时都会暂停所有用户线程
(stop the world简称STD),
- serial:单线程处理垃圾回收
- parallel:多线程处理垃圾回收
- CMS:在初始阶段只对垃圾进行标记,在恢复处理用户线程时也会同时进行重新标记然后并发清理垃圾
- G1:jdk1.9以后默认的垃圾回收器,利用空间换时间,即会耗费内存以达到快速清理垃圾
查看jvm使用的哪个版本的垃圾回收器
java -XX:+PrintCommandLineFlags -version
MAT
下载地址,选择Memory Analyzer 1.7.0 Release
下载,最新版本的jdk1.8不支持,需要jdk11,所以选择1.7.0版本即可
https://www.eclipse.org/mat/previousReleases.phpMAT的使用
打开点击导航栏File --》Open Heap Dump…
打开后会生成许多中间文件
如图在Overview
选择Histogram
,会看到第一类就是导致内存异常的类,也可以选择Histogram下面的Dominator Tree
,会看到那个线程占用内存百分比,线程下是那个类导致的内存溢出
Dominator Tree
Dominator Tree 面板介绍
- Classe Name:类名。
- Shallow Heap:对象自身占用的内存大小,不包括它引用的对象。如果是数组类型的对象,它的大小是数组元素的类型和数组长度决定。如果是非数组类型的对象,它的大小由其成员变量的数量和类型决定。
- Retained Heap:一个对象的Retained Set所包含对象所占内存的总大小。换句话说,Retained Heap就是当前对象被GC后,从Heap上总共能释放掉的内存。
- Percentage:内存占比。
导入过大的文件异常解决
报错信息:An internal error occurred during: "Parsing heap dump from “。。。”
原因:
当你导出的dump文件的大小大于你配置的1024m,就会报上述错误。
解决方法:
- 打开MAT所在目录下的 MemoryAnalyzer.ini文件
- 修改内存参数 默认1024m大小为 3000m(大于要打开的dump文件大小)即可
- 修改之后重新启动MemoryAnalyzer即可解决!
虚拟机栈
和程序计数器一样,也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口
等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程;
- 栈 单个线程运行时需要的内存
- 栈帧 每个方法运行时需要的内存
常见异常
栈内存溢出 java.lang.StackOverflowError,
一般引起这种异常的是因为方法调用过多,不正确的使用递归会出现此种异常,或者单个栈帧过大,不过这种很少出现
案例代码
package jvm;
public class JVMParamTest {
public static int count = 0;
public static void main(String[] args) {
try {
test1();
} catch (Throwable throwable) {
System.out.println(throwable);
System.out.println(count);
}
}
public static void test1() {
count++;
test1();
}
}
局部变量表
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同与对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
本地方法栈
本地方法栈则为虚拟机使用到的Native方法服务
程序计数器
它可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支循环、跳转、异常、处理、线程回复等基础功能都需要依赖这个计数器来完成,总结为记住下一条JVM指令的执行地址,相当于CPU的告诉缓存
直接内存(物理内存)
此内存是物理内存,不属于虚拟机内存
Java代码使用案例
package jvm;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class DirectBuffer {
public static void main(String[] args) {
try (FileChannel read = new FileInputStream("要读入的文件路径").getChannel();
FileChannel write = new FileOutputStream("要写入的文件路径").getChannel()) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024);
while (read.read(byteBuffer) != -1) {
byteBuffer.flip();
write.write(byteBuffer);
byteBuffer.clear();
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
内存溢出代码案例
package jvm;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class DirectBuffer {
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
list.add(byteBuffer);
i++;
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("===" + i);
} finally {
System.out.println(i);
}
}
}
异常信息
Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory
常用命令和解决问题
堆内存转储
堆内存转储(Heap Dump),是指JVM堆内存在某一个时刻的快照,一般使用 hprof 格式的二进制文件来保存。 可用于分析内存泄漏问题
,以及Java程序的内存使用优化。
常见的内存转储分析工具包括: jhat, JVisualVM, 以及基于Eclipse的 MAT工具.
jmap 工具
jmap有个致命缺陷,执行jmap命令时会停止jvm的工作,让后再进行堆内存转储,所以这个在线上环境禁止使用
jmap 可用来输出JVM内存的统计信息,支持访问本地JVM,以及远程JVM实例。
使用 -dump 选项来获取堆内存转储,命令为:
jmap -dump:[live],format=b,file=<file-path> <pid>
在 -dump
: 选项后面, 可以指定以下参数:
live
: 可选参数;表示只输出存活对象,也就是会先执行一次FullGC来清除可以被回收的部分。format=b
: 可选参数, 指定 dump 文件为二进制格式(binary format). 在堆内存转储时,默认就是二进制格式。file
: 指定转储文件的保存路径。pid
: 指定Java进程的pid。
使用示例如下:
jmap -dump:live,format=b,file=/tmp/dump.hprof 12587
# 或者
jmap -dump:file=/tmp/dump.hprof 12587
使用jmap查看占用内存最高的类
jmap -histo 1574 | head 20
- 1574:进程id
- 20:查看前20个占用最高的