1 介绍
定义:java virtual meachine -java运行时环境(java二进制字节码的运行环境)。
好处:
- 一次编写到处运行
- 自动内存管理,垃圾回收
- 数组下标越界检查
- 多态
2. 内存结构
2.1 程序计数器(线程私有)
作用: 记住下一条jvm指令的执行地址。
由于现实中程序往往是多线程协作完成任务的。JVM的多线程是通过 CPU时间片轮转 来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。程序计数器具有线程隔离性,每个线程拥有自己的程序计数器。
特点:
- 线程私有
- 内存中唯一一个不会出现内存溢出的区域
2.2 虚拟机栈
定义: Java Virtual Mechine Stacks(java虚拟机栈)
- 每个线程运行时所需要的内存空间
- 每个栈由多个栈帧(可能存放参数,局部变量,返回值等信息)组成,对应着该线程调用方法占用的内存
- 每个线程都只有一个活动栈帧,对应着线程当前正在执行的方法。
问题
- 垃圾回收是否涉及到栈内存?
答: 不会涉及。因为栈对应着一个程序的执行过程,在执行过程中,活动栈不断随着该方法的运行结束而弹出,直到程序执行完成,栈为空,消失。即一个进程执行完毕,线程栈消失。 - 栈内存越大越好吗?
答: 不是。服务器物理内存是固定的,若栈内存越大,则能够同时运行的线程数量就会减小。栈是由多个栈帧组成,内存增大,只是将可以容纳栈帧的数量增多,除了可以调用更多的方法(递归),没有其他的作用。
-Xss设置栈内存大小,一般-Xss=1M。 - 线程的局部变量是否线程安全?
答: 不一定。方法内的局部变量没有逃离方法的作用范围时,是线程安全的。如果局部变量引用了对象,由于对象存在于堆中,一般其他线程可以访问修改,需要考虑线程安全。
栈内存溢出
- 栈帧过多,栈被撑破了(递归,结束条件有问题,不停的调用自己,导致栈帧过多)
- 栈帧过大(交叉引用)
线程诊断
- cpu占用过多
1.定位进程,top找出占用cpu过多的进程PID
2.ps -H -eo pid,tip,%cpu | grep PID 找出该进程下占用过大的线程 TID
3.jstack pid 查看进程中各线程的详细信息,可以看出问题所在。注意:这里TID为十六进制,需要讲上一步ps查出来的TID转换为16进制。
- 运行迟迟没结果
jstack pid 查看进程中各线程的详细信息,拉到最后,发现死锁问题。
2.3 本地方法栈(线程私有)
本地方法栈类似于虚拟机栈,也是线程私有。
不同点:本地方法栈服务的对象是jvm运行的native方法,而虚拟机栈服务的是jvm执行的java方法。
2.4 堆(线程共享)
定义: Heap,通过new关键字创建的对象,都存放在堆内存中。
特点:
- 线程共享,堆中的对象都存在线程安全的问题
- 垃圾回收,垃圾回收机制重点区域。
堆内存溢出
java.lang.OutOfMemeryError:Java Heap space
以下示例在程序执行过程中,由于a字符串不断地累加,并且往list中添加导致list规模越来越大,垃圾回收机制认为它是有用的,所以不会被回收,最终造成堆内存溢出:
堆内存诊断
- jps
查看系统有哪些进程。 - jmap
查看某一时刻堆内存使用情况 jmap -heap PID - jconsole
图形界面,多功能检测工具,连续监测。直接在控制台输入jconsole,选择连接想要查看的PID。 - jvistualvm
图形界面,多功能检测工具,可以直接定位到内存占用过高的位置。
一个简单的案例:
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
List<student> list = new ArrayList<>();
for (int i = 0; i < 200;i++){
list.add(new student());
}
Thread.sleep(10000000000L);
}
}
class student{
private byte[] big = new byte[1024 * 1024];
}
- 打开jvistualvm,在1处选择对应进程,2处选监视,3处点“dump”。
- 点击“查找”,点击第一条占用内存最大的记录。
- 找到问题所在,list中有过多大对象student,无法被清除。
2.5 方法区(线程共享)
定义: 主要存储class文件的信息和运行时常量池,class文件的信息包括类信息和class文件常量池。
组成与实现
jdk6(永久代实现)和jdk8(元空间实现)中方法区的区别,其中最主要的区别是8中将方法区转移到本地内存中,且常量池分为运行时常量池和字符串常量池;且字符串常量池被留在内存中的堆中。
方法区内存溢出
- jdk1.6 永久代
-
jdk1.8 元空间
jdk1.8元空间溢出
场景
- spring(cglib动态生成代理类,可能造成方法区内存溢出)
- mybatis(同上)
运行时常量池
常量池:
- 字符串常量池(string pool)
字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。string pool在每个HotSpot VM的实例只有一份,被所有的类共享。在jdk1.8后,将String常量池放到了堆中。 - class常量池
当java文件被编译成class文件之后,会在class文件中生成我们所说的class常量池,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(文本字符串、被声明为final的常量、基本数据类型的值)和符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)。
运行时常量池:
- jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。
- 当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。
- class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
总结:
- 全局字符串池每个虚拟机只有一个,存储字符串常量的引用值;
- class常量池是java程序编译之后才有的,每个类都有,存放字面值和符号引用常量;
- 运行时常量池是在类加载完之后,常量池内容存储在运行时常量池中,每个类都有一个,且常量池中符号引用转换为直接引用,与全局字符串池中保持一致。
串池StringTable
- 编译期优化
public class Demo3 {
public static void main(String[] args) {
String s1 = "a";//a的索引放入串池
String s2 = "b";//b的索引放入串池
/*
new StringBuilder().append("a").append("b").toString();
s1 s2 为变量,其值没有确定
*/
String s4 = s1 + s2;//s4放入堆中,因为是变量相加,后面可能会变,并没有将ab索引放入串池
System.out.println(s4 == "ab");//false
String s5 = "a" + "b";//直接去寻找串池中是否有ab的索引,有直接返回,没有就将ab索引放入串池
System.out.println("ab" == s5);//true
}
}
- 字符串延迟加载(惰性)
//运行到才会将字符串写入串池,不提前放
String s1 = "a";
//java.lang.string.count = 2361
String s2 = "b";
//java.lang.string.count = 2362
String s3 = "ab";
//java.lang.string.count = 2363
- 常量池和串池的关系
- 常量池中的都会被加载到运行时常量池中,此时字符串都是常量池中的符号,尚且未转化为对象
- 运行时,将String字面量符号“a”转化为字符串对象,并在StringTable中寻找“a”,如多没有则添加“a”字符串对象。
- StringTable 结构为HashTable,且不能扩容。
- String table中存储的并不是String类型的对象,存储的而是指向String对象的索引,真实对象还是存储在堆中。
- intern方法的运用(jdk1.8)
public class Demo4 {
public static void main(String[] args) {
//new String("a") new String("b") new String("ab")
//StringTable["a","b","ab"]
String s = new String("a") + new String("b");
//使用intern方法 在StringTable中放入此字符串对象,如果有则返回给字符串对象,没有会把s对象放入串池
String s2 = s.intern();
//s2指向的是串池中字符串对象 s未被放入串池,还在堆中
String s3 = "ab";
System.out.println(s3==s2);//true
System.out.println(s3==s);//true
}
}
- intern方法的运用jdk1.6
public class Demo4 {
public static void main(String[] args) {
//new String("a") new String("b") new String("ab")
//StringTable["a","b"]
String s = new String("a") + new String("b");
//使用intern方法 在StringTable中放入此字符串对象,如果有则返回给字符串对象,没有则++复制一份后放入++
String s2 = s.intern();
//s2指向的是串池中字符串对象(s复制后的对象) ,s未被放入串池,还在堆中
//StringTable["a","b","ab"]
String s3 = "ab";
System.out.println(s3==s2);//true
System.out.println(s3==s);//false
}
}
- 测试
public class Demo2 {
public static void main(String[] args) {
//StringTable:"a"
String s1 = "a";
//StringTable:"a","b"
String s2 = "b";
//StringTable:"a","b","ab"
String s3 = "a" + "b";
//StringTable:"a","b","ab"
//s4在堆中
String s4 = s1 + s2;
//StringTable:"a","b","ab"
//此时串池已有了,直接返回
String s5 = "ab";
//StringTable:"a","b","ab"
//此时串池已有了,直接返回
String s6 = s4.intern();
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true;
////StringTable:"a","b","ab","c","d"
String x2 = new String("c") + new String("d");
//StringTable:"a","b","ab","c","d","cd"
//发现串池中没有cd索引,则将cd索引放入串池,
// jdk1.8则直接将x2对象指向串池cd索引,
// jdk1.6会复制一份对象来指向串池中的cd索引
x2.intern();
String x1 = "cd";
System.out.println(x1 == x2);//jdk1.8:true,jdk1.6:false
}
}
- StringTable位置
- jdk1.6及之前,StringTable与方法区一同在永久代中。
- jdk1.8之后,方法区转移到本地内存中,但是将StringTable转移到堆内存中。
原因:
1). StringTable中存在大量的字符串对象,运行时间增长永久代内存占用过多,且永久代只有在触发FULL GC时才进行垃圾回收,回收频率过慢。
2). 转移到堆中可以利用虚拟机在堆内存中频繁的垃圾回收,处理StringTable中对象过多情况。
- StringTable垃圾回收
//-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
public class stringTableGc {
public static void main(String[] args) throws InterruptedException{
int i = 0;
try {
for (int j = 0;j< 10000; j++){
String.valueOf(j).intern();//不停的加入到串池
}
}
catch (Exception e){
e.printStackTrace();
}
finally {
System.out.println();
}
}
}
通过限定jvm内存大小和打印串池统计,我们发现,GC (Allocation Failure),由于内存不足触发了垃圾回收机制,回收了串池中的部分字符串。
"C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc "-javaagent:D:\IntelliJ IDEA 2019.3\lib\idea_rt.jar=8211:D:\IntelliJ IDEA 2019.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;D:\IdeaProjects\test\target\classes;C:\Users\love\.m2\repository\org\projectlombok\lombok\1.18.10\lombok-1.18.10.jar;C:\Users\love\.m2\repository\cglib\cglib\3.3.0\cglib-3.3.0.jar;C:\Users\love\.m2\repository\org\ow2\asm\asm\7.1\asm-7.1.jar;C:\Users\love\.m2\repository\org\apache\rocketmq\rocketmq-client\4.7.0\rocketmq-client-4.7.0.jar;C:\Users\love\.m2\repository\org\apache\rocketmq\rocketmq-common\4.7.0\rocketmq-common-4.7.0.jar;C:\Users\love\.m2\repository\org\apache\rocketmq\rocketmq-remoting\4.7.0\rocketmq-remoting-4.7.0.jar;C:\Users\love\.m2\repository\com\alibaba\fastjson\1.2.62\fastjson-1.2.62.jar;C:\Users\love\.m2\repository\io\netty\netty-all\4.0.42.Final\netty-all-4.0.42.Final.jar;C:\Users\love\.m2\repository\org\apache\rocketmq\rocketmq-logging\4.7.0\rocketmq-logging-4.7.0.jar;C:\Users\love\.m2\repository\io\netty\netty-tcnative-boringssl-static\1.1.33.Fork26\netty-tcnative-boringssl-static-1.1.33.Fork26.jar;C:\Users\love\.m2\repository\commons-validator\commons-validator\1.6\commons-validator-1.6.jar;C:\Users\love\.m2\repository\commons-beanutils\commons-beanutils\1.9.2\commons-beanutils-1.9.2.jar;C:\Users\love\.m2\repository\commons-digester\commons-digester\1.8.1\commons-digester-1.8.1.jar;C:\Users\love\.m2\repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;C:\Users\love\.m2\repository\commons-collections\commons-collections\3.2.2\commons-collections-3.2.2.jar;C:\Users\love\.m2\repository\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar" jvm.stringTableGc
[GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->776K(9728K), 0.0017733 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 2560K, used 1197K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 33% used [0x00000000ffd00000,0x00000000ffdad5a0,0x00000000fff00000)
from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e030,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 272K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 3% used [0x00000000ff600000,0x00000000ff644000,0x00000000ffd00000)
Metaspace used 3444K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 374K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 14138 = 339312 bytes, avg 24.000
Number of literals : 14138 = 601448 bytes, avg 42.541
Total footprint : = 1100848 bytes
Average bucket size : 0.707
Variance of bucket size : 0.711
Std. dev. of bucket size: 0.843
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000 #桶个数
Number of entries : 11694 = 280656 bytes, avg 24.000 #实例个数
Number of literals : 11694 = 635080 bytes, avg 54.308
Total footprint : = 1395840 bytes
Average bucket size : 0.195
Variance of bucket size : 0.209
Std. dev. of bucket size: 0.457
Maximum bucket size : 3
9.StringTable调优
- 调整hash表中桶子个数,-XX:StringTableSize=桶个数(桶个数越大性能越好)
- 考虑字符串是否入池(如果含有大量重复字符串,由于串池不会重复,可以去重引用,节省堆内存的占用)
2.6 直接内存
- 常见于NIO操作中,用于数据缓冲
NIO:面向块的方式处理数据(数据块的移动,一次操作产生或者消费一个数据块,将最耗时的 I/O 操作–填充和提取缓冲区内容操作转移回操作系统) - 分配回收成本高,但读写能力强
- 不受JVM内存回收管理
直接内存使用前和使用后
- 使用前
- 因为java无法操作本地文件,所以需要在java堆内存中划出java缓冲区;
- 从用户态转移到内核态,本地方法在系统内存中划出一段系统缓冲区,将磁盘文件分部分缓冲到系统缓冲区中,系统缓冲区在将数据复制到java缓冲区中;
- 内核态转到用户态,调用输出流写入操作,将文件copy到另一个位置,循环copy,直到全部复制完成。
- 使用后
- ByteBuffer.allocateDirect(_size),在系统内存中分配直接内存;
- 系统方法和java方法都可以访问直接内存;
- 与不使用直接内存相比,减少了一次从系统缓存区向java缓冲区复制的操作,复制效率成倍上升。
直接内存溢出
public class Demo3 {
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while(true){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100*1024*1024);
list.add(byteBuffer);
i++;
}
}finally {
System.out.println(i);
}
}
}
36 3.6G
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.example.jvm.Demo3.main(Demo3.java:14)
分配和回收原理
- 使用Unsafe对象实现直接内存的分配回收,回收主要使用的是freeMemory方法,**而不是通过GC回收的。 **
- ByteBuffer类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦对象被回收,就会由ReferenceHandler线程通过Cleaner的clean对象调用fUnsafe的reeMenory来释放直接内存。
- -XX:+DisableExplicitGC 显式的System.gc()显式的垃圾回收 FULL GC,被禁用。
- 因为考虑到系统性能,FULL GC时间够长,会严重影响性能。所以涉及到直接内存的使用,释放内存使用Unsafe.freeMemory,不建议使用System.gc()。