内存结构
程序计数器
- 由寄存器实现,用来记住下一条JVM指令的执行地址
- 线程私有
- 不存在内存溢出
虚拟机栈
特点
- 线程运行时需要的内存空间,一个线程对应一个栈
- 每个线程只能有一个活动栈帧,即正在执行的方法(栈顶)
问题:
1、垃圾回收是否涉及栈内存?不涉及
2、栈内存是否越大越好?
3、方法内的局部变量是否线程安全?
若方法内的局部变量没有逃离方法的作用范围(没有被return),就是线程安全的
栈内存溢出(StackOverflowError)
- 栈帧过多导致栈内存溢出(如循环递归)
- 栈帧过大导致栈内存溢出(较少出现)
- 设置虚拟机栈的大小:
-Xss Size
线程运行诊断
-
CPU占用过多诊断:
linux环境下:
使用top
命令监测哪个进程占用CPU过高
使用ps H -eo pid,tid,%cpu | grep 32655
进一步定位是哪个线程引起的cpu占用过高
使用jstack pid
定位到有问题的线程(十进制线程号要转成十六进制),进一步定位到问题代码行数 -
程序运行长时间无结果诊断(排查死锁):
使用jstack pid
本地方法栈
堆
特点
- 通过new关键字创建的对象都会使用堆内存
- 线程共享,堆中的对象需要考虑线程安全问题
- 有垃圾回收机制
堆内存溢出(OutOfMemoryError)
- 设置堆大小:如:
-Xmx8m
改成8M
堆内存诊断
-
jps工具
查看当前数系统中有哪些Java进程
-
jmap工具
查看该堆内存占用情况
-
jconsole工具
图形界面的多功能的检测工具,支持连续监测
-
案例:垃圾回收后,内存占用仍然很高
使用jvisualvm工具
方法区
特点
- HotSpot虚拟机:JDK1.8以前,方法区的实现叫永久代,是堆内存的一部分;JDK1.8以后,方法区的实现叫元空间,是本地内存(操作系统)的一部分,默认无上限(电脑内存决定)
方法区内存溢出(OutOfMemoryError)
- JDK1.8前设置永久代大小:
-XX:MaxPermSize=8m
- JDK1.8后设置元空间大小:
-XX:MaxMetaspaceSize=8m
运行时常量池
-
常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
反编译字节码:
-
运行时常量池:常量池是
*.class
文件中的,当该类被加载时,它的常量池信息会被放入运行时常量池,里面的符号地址会变为内存真实地址
StringTable
一道面试题:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
StringTable特性
- 常量池中的信息都会被加载到运行时常量池中,此时的"a"、“b”、"ab"都是常量池中的符号,还没有变为Java中的字符串对象
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s5);
}
}
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
-
JDK1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
-
JDK1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
-
StringTable的位置
从1.6到1.8对StringTable位置的改动:更容易触发对StringTable的垃圾回收
演示StringTable内存溢出:
-
在jdk8下设置堆最大内存并关闭UseGCOverheadLimit:
-Xmx10m -XX:-UseGCOverheadLimit
-
在jdk6下设置永久代最大内存:
-XX:MaxPermSize=10m
StringTable垃圾回收
- StringTable中的字符串在没有引用时也会被垃圾回收
- 演示StringTable垃圾回收:
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
StringTable性能调优
- 调整桶个数
-
StringTable底层也是哈希表,所以StringTable的性能调优主要也是改变底层桶的个数
-
桶的个数默认60013,自定义设置必须在规定区间
-
调整桶个数,测试耗费时间:
-Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
- 考虑是否将字符串对象入池
- 不入池
- 入池(调用intern方法)
直接内存
特点
- 直接内存,即操作系统的内存
- 常用于NIO操作时,作为数据缓冲区
- 分配回收成本较高,但读写性能高
public class Demo1_9 {
static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
为什么直接内存的效率更高?
传统javaIO:
使用allocateDirect:
- 不受JVM垃圾回收管理
也会有内存溢出的情况:
分配和回收的原理
- 测试代码:
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
/*
* -XX:+DisableExplicitGC 显式的
*/
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
-
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
-
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
思考上面的测试代码,问题:
当禁用显式的垃圾回收:-XX:+DisableExplicitGC
,则System.gc();
就会失效,所以即使设置byteBuffer = null;
,由于堆内存空间仍充裕,byteBuffer不会被马上回收,进而影响到直接内存的释放
解决:直接手动使用Unsafe 对象完成直接内存的分配回收