1. JVM的内存结构
JVM的内存结构主要包括五个部分:程序计数器、虚拟机栈、本地方法栈、堆和方法区,其中程序计数器、虚拟机栈、本地方法栈是线程独有的,而堆和方法区是线程共享的。
1.1 程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。是为了线程切换后能恢复到正确的执行位置。如果线程正在执行的是一个Java方法,这个计数器指示的是正在执行的虚拟机字节码指令的地址;如果正在执行一个native方法,计数器则为空(Undefined)。
1.2 虚拟机栈(JVM Stacks)
每个栈由多个栈帧组成,当执行一个方法时会创建一个栈帧,方法的调用到结束对应了栈帧的入栈到出栈,每个栈中同一时间只有一个活动栈帧:即同一时间线程中只有一个方法在执行。
栈由多个栈帧组成,栈帧中存放了局部变量表、操作数栈、动态链接和方法出口等信息。
1.2.1 栈内存溢出
当出现递归程度很深的程序时,栈一直进行入栈操作,内存可能会耗尽,此时会出现StackOverFlowError的错误。
public void stackError(int i){
i++;
stackError(i);
}
public static void main(String[] args) {
new StackError().stackError(1);
}

可以使用-Xss256K来设置栈帧的大小
1.3 本地方法栈(Native Method Stacks)
本地方法:不由java代码编写的方法,用native关键字修饰,不给出实际的方法实现体,大多由操作系统提供。
本地方法栈和虚拟机栈的功能类似,不同点是本地方法栈为本地方法服务。
2.1 堆(Heap)
主要存放对象实例。
2.1.1 特点
①:线程共享;
②:存在垃圾回收。
2.1.2 堆内存溢出
堆内存不够用时出现OutOfMemoryError:Java heap space错误:
public static void main(String[] args) {
List<String> list = new ArrayList();
while(true){
list.add("heapError");
}
}

2.1.3 堆内存的诊断
①:使用jps命令查看哪个java进程出现错误;
②:使用jmap -heap pid查看堆内存的占用情况,类似快照;
③:也可以使用jconsole,它是图形界面的,可以连续监测,类似视频;
2.2 方法区(Method Area)
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
1.7前使用永久代(PermGen)实现,在1.8后使用元空间(Metaspace)实现。
使用-XX:MaxMetaspaceSize=8m设置最大元空间大小。
2.2.1 元空间溢出
OutOfMemory:metaspace
2.2.2 常量池
存放字面量和符号引用。
2.2.3 运行时常量池
当类的字节码被加载时会将此类的常量池信息放入运行时常量池,出了保存符号地址,还保存了翻译出来的真正的内存地址。
2.2.4 串池String Teble
是一个HashTable,即桶的个数是它的大小,它主要存放字符串常量。
在java里字符串的构造主要有两种:
①:String str = new String("123");
②:String str = "123";
第一种方式创建的对象被存放在堆内存中,每次调用都会产生一个新的对象;
第二种会先从串池中找是否有和"123"相同的字符串,如果有则将串池中的字符串引用赋给str,若没有则将"123"加入串池中,然后将加入到串池中的"123"的引用赋给str。
我们来验证一下:
String str1 = "123";
String str2 = new String("123");
System.out.println(str1 == str2);
运行以上代码:

拼接字符串的本质
先来看下面这段代码:
String str1 = "q";
String str2 = "w";
String str3 = "qw";
String str4 = str1 + str2;
System.out.println(str3 == str4);
.我先运行

为什么是flase?我们用javap -v StringTable.class命令看看二进制字节码中的JVM指令:


可见字符串拼接由StringConcatFactory中的makeConcatWithConstant实现,我找到了makeConcatWithConstant方法,里面有STRATEGY枚举,这是实现拼接的六种策略:
private static enum Strategy {
BC_SB,
BC_SB_SIZED,
BC_SB_SIZED_EXACT,
MH_SB_SIZED,
MH_SB_SIZED_EXACT,
MH_INLINE_SIZED_EXACT;
private Strategy() {
}
}
switch(STRATEGY) {
case BC_SB:
return StringConcatFactory.BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, StringConcatFactory.Mode.DEFAULT);
case BC_SB_SIZED:
return StringConcatFactory.BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, StringConcatFactory.Mode.SIZED);
case BC_SB_SIZED_EXACT:
return StringConcatFactory.BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, StringConcatFactory.Mode.SIZED_EXACT);
case MH_SB_SIZED:
return StringConcatFactory.MethodHandleStringBuilderStrategy.generate(mt, recipe, StringConcatFactory.Mode.SIZED);
case MH_SB_SIZED_EXACT:
return StringConcatFactory.MethodHandleStringBuilderStrategy.generate(mt, recipe, StringConcatFactory.Mode.SIZED_EXACT);
case MH_INLINE_SIZED_EXACT:
return StringConcatFactory.MethodHandleInlineCopyStrategy.generate(mt, recipe);
default:
throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented");
}
可以发现前五种都是和StringBuilder有关,而我们采用的是默认方式,所以应该是第一种StringConcatFactory.Mode.DEFAULT。在jdk8中用"+"连接的字符串变量是纯用StringBuilder实现的,通过使用append方法追加字符串,toString方法创建新的字符串,所以用"+"连接的字符串本质上还是用new创建了新的String对象,所以它连接后的字符串属于堆内存。
再看一个代码:
String str1 = "q" + "w";
String str2 = "qw";
System.out.println(str1 == str2);


对于常量字符串的拼接,javac在编译期就进行了优化处理,拿到的字符串是"qw",所以没有makeConcatWithConstant方法的调用。
串池的位置?
jdk1.7是在永久代的常量池中,在jdk1.8以后是在堆中。
字符串的intern操作
调用intern的字符串,若串池中没有该字符串则将该字符串加入串池(调用者本身不加入串池),并将串池中的对象返回。环境是jdk11
String str1 = new String("qw");
String str2 = "qw";
System.out.println(str1 == str2);
str1.intern();
System.out.println(str1 == str2);

将intern后的变量赋给str1:
String str1 = new String("qw");
String str2 = "qw";
System.out.println(str1 == str2);
str1 = str1.intern();
System.out.println(str1 == str2);

串池的性能调优
①:-XX:StringTableSize=1009:改变串池中桶的个数(让每个桶中装的字符串个数适当);
②:重复的字符串进行入池可以节省大量内存。
本文详细介绍了JVM的内存结构,包括程序计数器、虚拟机栈、本地方法栈、堆和方法区。栈内存溢出可能由于递归深度过大导致StackOverFlowError,而堆内存溢出则表现为OutOfMemoryError。方法区存储类信息、常量、静态变量等,1.8后由元空间替代永久代。字符串常量池在1.8后位于堆中, intern操作可优化内存使用。
481

被折叠的 条评论
为什么被折叠?



