
JVM总结-01_内存结构

如图,学习路线
java 类
类加载器
jvm内存结构
-
方法区
-
堆
-
虚拟机栈
-
程序技术器
-
本地方法栈
执行引擎
-
解释器
-
即时编译器
-
GC垃圾回收
本地方法接口
JVM内存结构
1.程序计数器
作用:记录下一条jvm指令的地址,以在cpu时间片调度时能够找到下一条指令的位置
特点:
-
唯一不会OOM的区
-
线程私有,每个线程都有自己私有的程序计数器,不会影响别的线程
2.虚拟机栈
栈:每个线程运行时需要的内存空间,线程私有
栈帧:每个栈内有多个栈帧,每个栈帧包含一个方法调用时需要的内存空间,内存中存放例如变量,地址啊什么的
-
局部变量表
-
操作数栈
-
动态链接
-
方法返回地址
-
附加信息
每个线程只能有一个活动栈帧,代表当前正在执行的方法。
(栈 -> 弹匣,栈帧 ->子弹,活动栈帧 -> 等待击发的那颗子弹)
- a调b,b调c,c执行完后返回值返回给b,b执行完后返回给a,最终a执行完毕,栈很适合方法调用这种场景
问题
垃圾回收会回收栈内存吗?
垃圾回收只回收堆内存空间
栈内存设置越大越好吗?
vmOptions中 --Xss设置栈内存大小,默认都是1042kb,win中根据虚拟内存决定。每个线程都有一个栈,栈内存设置的越大,对应的线程数就变少了,因为物理内存是恒定的。
方法内局部变量是否线程安全?
如果局部变量没有逃逸方法的作用范围,则线程安全,(对象在方法内定义,且不会返回出去)
如static全局变量,其他引用类型作为入参或返回值,则逃逸了该方法,则不是线程安全
如果是值类型则线程安全
栈内存溢出
-
栈帧过多(递归调用,常见),错误的递归条件导致只入栈不出栈
Stack OverflowError
-
栈帧过大(一个方法特别大撑爆内存,罕见),OutOfMemoryError
线程诊断
cpu占用过高排查
可能是死循环
top 看pid
ps H -eo pid, tid,%cpu | grep 具体的pid
jstack 进程tid,注意线程id进制问题,找到具体的行号
线程迟迟没有结果
可能是线程死锁,解决互相等待冲突条件,步骤同上
3. 本地方法栈
专门存放本地方法的内存空间,线程私有。
本地方法:native方法,调用底层c实现的方法。如Object中的clone(),wait(),notify(),hashCode(),ArrayList的copy()等
虚拟机栈,程序计数器,本地方法栈,都是线程私有的。
堆和方法区都是线程共享的,垃圾回收以及jvm调优都是基于这两块进行实现,
4.堆
堆Heap,线程共享的一片内存。
通过new 关键字创建的对象,都会被存放到堆中
-
线程共享,堆中的对象需要考虑线程安全的问题
-
有垃圾回收机制
堆内存溢出
不断的创建对象而没有及时被回收,OutOfMemoryError: Java Heap Space
通过设置 --Xmx 设置堆最大内存空间
(这种问题不便排查,调试时可以故意把堆内存设置小一点,问题好暴露出来)
堆内存调试工具
-
jps
- 查看当前系统中的java进程
-
jmap
- 查看堆内存情况(jdk8需要使用:jhsdb jmap --heap --pid 进程id)
-
jconsole
- 图形界面的堆内存检测工具
-
jvirtualvm
- 比jconsole更友好的图形化监测工具(jdk9以后不支持,需要另外下载)
问题
垃圾回收后,堆内存占用迟迟不降
可能是长时间存活大对象,在老年代中。
5.方法区
方法区是jvm规范提出的逻辑上的概念,用于存储类信息,常量池,JIT编译后的代码等数据,不同的厂商有不同的实现
1.8以前:永久代,堆内存的一片空间
1.8以后:取消永久代,使用元空间,使用的本机内存。
元空间:
-
Class类对象信息
-
ClassLoader类加载器
-
常量池
-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置Metaspace的最大大小
组成

元空间溢出
产生大量的类对象,导致内存撑爆。OutOfMemoryError:MetaSpace
场景:框架(spring,mybatis创建代理对象,ioc创建bean时)动态生成类对象时,可能会出现,
常量池
常量池就是一张表,虚拟机指令根据这张表来找到具体要执行的类名,方法名,参数类型,字面量等信息。
运行时常量池
常量池是.class文件的,当类被加载时,对应的常量池数据就会被加载到运行时常量池中去,并把引用符号转换为真实的地址。
StringTable 串池
在堆中的空间,hashtable结构,不能扩容
当新建的String对象在串池中没有时,把字符串放在串池中,下次若需要同样的数据则直接返回,避免重复创建。
public static void main(String[] args) {
// 直接赋值的对象放到stringtable串池中,而通过new关键字创建的对象是放在堆中的,二者位置不同地址自然不同。
String s1 = "a";
String s2 = "b";
String s3 = "ab";
// 此时串池中有三个字符串,a,b,ab
String s4 = s1+s2; // 变量拼接,1.8中:new StringBuilder().append("a").append("b").toString();
String s5 = "a" + "b";// 常量拼接,编译期优化,两个常量确定为ab,ab在串池中存在,且就是s3
String s6 = s4.intern();// 主动将串池中没有的对象放到串池中,若在放入之前串池中已经有了,则不会放入。返回该对象。
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println("--------------");
// cd 目前在堆
String x2 = new String("c") + new String("d");
// 创建一个cd并放到常量池
String x1 = "cd";
// 常量池已经有cd,仍然在堆中
x2.intern();
System.out.println(x1 == x2);
System.out.println("-----------intern--------");
// y和z都在串池
String y1 = "y";
String z = "z";
// y2 为变量拼接,会创建新对象yz,且在堆中
String y2 = y1 + z;
String y3 = y1 + new String("z");// y1在串池,和newString拼接,新创建的对象在堆中
String y4 = y1 + "z"; // 变量和常量拼接仍然在堆中创建新对象
String yz = "yz";
String yz2 = "y" + "z";
System.out.println(y2 == y3);
System.out.println(y2 == y4);
System.out.println(y2 == yz);
System.out.println("------------yz-----------");
System.out.println(y3== yz);
System.out.println(y4 == yz);
System.out.println(yz == yz2);
// 结论:字符串拼接中带有变量或者new String,都会创建一个新的对象在堆中,只有常量之间的拼接是会放到串池,并从串池中取。
// 因为字符串变量拼接在1.8会被编译期优化成new StringBuilder().append().append().toString(),所以本质上等于new String()
// 而new关键字创建的对象都是在堆中。
}
-
字符串常量会放入串池,常量之间的拼接也会放入串池,以供下次使用
-
new String()创建的字符串对象都会被放到堆,不会放入串池,可以调用intern()方法放入串池(如果串池已存在则不放入)
-
含有变量的字符串拼接都会被编译器优化成StringBuilder的append.toString()(1.8中),本质上等同new String(),故还是放在堆中。
-
== 比较两个字符串(引用类型)是比较引用地址,所以字符串比较需要用equals()方法比较值
问题
-
jdk1.6的串池在常量池中,为什么1.8放到了堆中?
常量池的对象需要回收是走full gc,需要老年代空间达到阈值,触发时间晚,不利于频繁创建的串池回收
-
大量字符串拼接为什么推荐StringBuilder?
虽然串池提供了一定的字符串复用的功能,减少重复创建,但是只是对编译器确定的字符串才能够直接放到串池中,而运行期确定(如带有变量的字符串拼接,仍然是直接在堆中新建对象,且不会放到串池,会产生很多冗余对象,如String a = "ab",String b = "11", a +=b,直接可new StringBuilder().append("a").append("b").toString();避免创建了b
证明:
-
设置堆内存大小 -Xmx10m -XX:-UseGCOverheadLimit
-
循环创建字符串对象
调优
-
-XX: StringTableSize=20000
增加桶的数量,桶数量越多,可存放的值越稀散,越少,越集中。
-
考虑是否将字符串对象入池
入池后可利用重复对象,减少内存资源占用
6. 直接内存
定义:Direct Memory 系统中的内存
-
常见于NIO操作时,用于数据缓冲区
-
分配回收成本高,但读写性能高
-
不受JVM内存回收管理

直接内存是一片在java堆内存和系统内存中共享的区域,可以实现NIO零拷贝,避免读取写入数据时需要把数据再次从系统内存拷贝到java堆内存中去。
直接内存溢出
OutOfMemory:Direct buffer Memory
直接内存释放
可在任务管理器查看进程内存占用情况。
java Unsafe类,通过反射获取Unsafe对象,调用freeMemory()
package com.kaguya.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
// bytebuffer使用直接内存,快于普通io
public class Test3 {
private static final String FROM = "D:\\AA拾玖私密\\视频\\录屏\\Desktop\\v.mp4";
private static final String TO = "D:\\AA拾玖私密\\视频\\录屏\\Desktop\\aaaaa.mp4";
private static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io();
driectIo();
}
public static void driectIo() {
long start = System.nanoTime();
try(FileChannel is = new FileInputStream(FROM).getChannel();
FileChannel os = new FileOutputStream(TO).getChannel()
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = is.read(bb);
if(len == -1) {
break;
}
bb.flip();
os.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("diret io耗时:"+ ((end - start) / 1000_000.0) + "ms");
}
public static void io () {
long start = System.nanoTime();
try(FileInputStream is = new FileInputStream(FROM);
FileOutputStream os = new FileOutputStream(TO)
) {
byte[] bf = new byte[_1Mb];
while (true) {
int len = is.read(bf);
if(len == -1) {
break;
}
os.write(bf,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("普通io耗时:"+ ((end - start) / 1000_000.0) + "ms");
}
}
回收原理
DirectByteBuffer底层就是调用unsafe类中的各个方法来创建和回收内存资源,在创建时Cleaner建立了回调来freeMemory,Cleaner是一个虚引用对象,当bytebuffer被垃圾回收时,触发它的回收。
-
使用Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
-
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHanlder线程通过Cleaner的clean方法调用freeMemory来释放直接内存
禁用显式GC
vm启动项加上-XX;disableExplicitGC 禁用显式的GC,使System.gc()无效,此时回收直接内存就需要我们手动调用Unsafe类的freeMemory();