JVM系列——栈与堆、方法区
栈
Java Virtual Machine Stacks (Java虚拟机栈) 线程运行时需要的内存空间,多个线程多个栈
栈的结构
栈中含有多个栈帧[Frame](链式调用时),栈帧就是每个方法运行时需要的内存 每个方法执行时都需要占用内存(参数、局部变量、返回值都需要分配内存) 当每个方法需要执行时,栈帧就会被压入栈内,直到方法执行完毕,栈帧就会出栈 ==注意!:每个线程只能有一个活动栈帧,对应正在执行的方法==
先进后出
入栈
出栈
栈的演示
package stack;
public class StackTest {
public static void do1(int a) {
do2(a, 2);
}
public static int do2(int a, int b) {
return a * b;
}
public static void main(String[] args) {
do1(2);
}
}
将代码打断点进入调试查看
如何在IDEA中查看具体调试信息
栈结构查看
到这里我们看到整个栈结构是:main方法在最下面,然后是do1,最后是do2,接下来执行出栈
看到do2方法先消失了,说明最后执行的do2方法最先被压出栈中,若你查看旁边的变量窗口,可以看到变量的占用内存的情况
我们还可以看到在方法 :号后有一个数字
这个就是对应的程序的代码行(程序计数器作用!!!),根据这个地址信息就可以锁定执行顺序
栈的特性
- 栈不需要垃圾回收机制!
- 栈的内存并不是越大越好!在默认中栈的默认大小多为1024KB(windows系统依赖于虚拟空间大小),栈的内存越大线程数越少,运行效率反而变低
- 若方法内的局部变量没有脱离该方法的作用域则线程安全,否则线程不安全
栈溢出问题(StackOverflowError)
又称栈内存溢出
溢出的原因
1.栈帧过多导致,当方法进行递归调用时,没有设置正确的终止条件导致溢出),也可能时程序的循环依赖导致 2. 栈帧过大导致的内存溢出,我们可以设想这种场景,在进行递归调用时,我们的方法使用了可变长参数,导致参数变量多到将栈帧撑到超出了栈内存,但实际来说几乎见不到
IDEA自定义栈内存大小
选择编辑配置
选择修改选项-->添加VM选项
输入
-Xss128k就可以改为128k的大小了
线程运行诊断(linux)
在linux环境下,我们可以使用ps查看线程占用,top可以查看进程占用情况
ps H -eo pid,tid,%cpu | grep 进程id
通过使用jstack命令对进程进行诊断,列出线程情况,我们将线程id转换为16进制进行锁定即可知道具体线程导致的问题(nid)
jstack 进程id
建议使用jconsole或jvisualvm
本地方法栈
Native Method Stacks 当本地方法接口进行调用时,本地方法栈为其提供内存空间
堆
Heap 我们通过使用new关键字创建的对象就会使用到堆内存
堆的特点
- 线程共享,需要考虑线程安全问题
- 堆中有垃圾共享机制
堆溢出问题(OutOfMemoryError)
当我们的程序不断产生新的变量,但是这些变量一直都在被使用的情况下就会产生,因为此时垃圾回收机制不生效,
IDEA自定义堆大小
和栈的方式一样,只要设置为:-Xmx堆大小即可
堆内存诊断
1. jps
用于查看当前系统中有哪些Java进程
jps
2. jmap
用于查看堆的占用情况(某个时刻)
jmap -heap 进程号
3. jconsole
用于连续监测,含有GUI,是多功能监测工具!
jconsole
选择你要监视的进程
4.jvisualvm
堆转储dump:监测完整的堆具体信息
选择检查中的查找即可查看类信息
方法区(jdk1.8后移至本地内存)
逻辑上属于堆的一部分,不强制位置,是一种规范
方法区的内存结构方法区保存的信息包括:
- 类型信息:包括了JVM加载类型(类class、接口interface、枚举enum、注解annotation)的完整有效名称(包名+类名)、其直接父类的完整有效名称、类型的修饰符、其直接继承的接口列表。
- 域(成员变量)信息:类型的所有成员变量的相关信息以及成员变量的声明顺序。
- 方法信息:包括了类型的成员方法的名称、返回类型、参数列表、修饰符、字节码、操作数栈、局部变量表、异常表等。
- 静态变量:non-final的静态类变量和全局常量。区别在于全局常量在编译器给指定值,静态类变量在加载时准备阶段赋初值,初始化阶段再给指定值。
- JIT代码缓存:即时编译产生的代码缓存,将热点代码编译成与本地平台相关的机器码,并保存到内存。
- 运行时常量池:各种字面量和对类型、域和方法的符号引用。
同样方法区也会有内存溢出,而且同堆一样抛出OutOfMemoryError的异常(证明了逻辑定义)
由于StringTable在程序中会大量使用,若放置在方法区中时,StringTable的回收效率低下则会导致永久代的内存不足,所以从1.7起StringTable移动到堆中
常量池
编译后查看二进制字节码时使用到的一张表 虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池
常量池是*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
StringTable
是hashtable的结构,不能扩容 当我们程序运行时,常量池中的信息会进入运行时常量池,常量池中的符号并未变为Java字符串对象,直到程序执行到相对应的语句时才会进入StringTable中
特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder (jdk1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回(jdk1.8)
当字符串变量进行拼接时
采用new StringBuilder.append("str1").append("str2")....toString()相当于直接new String("str")
(str = str1+str2....)
(原因:运行期间才能确定结果)
当字符串常量进行拼接时
直接合并出结果,并加入StringTable中(原因:编译期间就直接确定了结果)
StringTable的垃圾回收
设置VM Options : -Xmx8m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc -Xmx8m : 设置堆为8mb -XX:+PrintStringTableStatistics:设置打印StringTable统计信息 -Xlog:gc* :设置打印垃圾回收具体信息
//设置VM Options : -Xmx8m -XX:+PrintStringTableStatistics -Xlog:gc* -verbose:gc
//-Xmx8m : 设置堆为8mb
//-XX:+PrintStringTableStatistics:设置打印StringTable统计信息
//-Xlog:gc* :设置打印垃圾回收具体信息
public class StringTableGC {
public static void main(String[] args) {
int i = 0;
for (int j = 0; j < 100000; j++) {
//加入StringTable中
String.valueOf(j).intern();
i++;
}
System.out.println(i);
}
}
循环100次
循环10w次
这里可以看到触发了大量的垃圾回收
看完发现共有4次
StringTable性能调优
- 主要是调整hashTable中的桶个数(至少1009个)
-Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1010
- 采用字符串入池操作大量减少重复字符串
String.intern()