一、JVM内存结构图
二、运行时数据区内容
-
方法区(Method Area,线程共有)
方法区别名Non-Heap,此区域为各个线程共享的内存区域,用于存储 类信息、静态变量、常量和即时编译代码。方法区中有块区域称为运行时常量池(Runtime Constant Pool),用于存放编译器生成的符号引用和字面量(值本身,String值“ABC”,int值 3 等)。 Java的动态性特征使得不要求常量一定在编译时产生,运行期间尝试新的常量也会加入池中。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError。
运行时常量池中内容:
①字面量包括final修饰的常量、基本数据类型的值和字符串
②符号引用包括类和接口全限定类名、方法名和描述符 及 字段名和描述符
Tips:JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。实际上方法区又被称为永久代(PremGen),但这只限于Hotspot虚拟机中,因为方法区是JVM的规范,永久代是HotSpot对JVM规范的一种实现,其他类型的虚拟机(Oracle的JRockit、IBM的J9)没有永久代这个概念。 -
Java堆(Heap,线程共有)
Java堆为线程共享区域,主要用于存放对象实例,是GC机制主要管理区域,因此也叫GC堆。Java堆对其中对象进行了分代管理,分为新生代和老年代,具体分法将记录JVM笔记(三)——分代与GC算法中。如果堆中剩余内存不够分配,将抛出OutOfMemoryError异常。 -
虚拟机栈(JVM Stack,线程私有)
JVM虚拟机栈是线程私有的部分,与线程同存亡。虚拟机栈和普通栈一样都是FILO(先进后出)的,只能对栈顶元素进行操作。栈中存储元素为栈帧,每个方法在执行时都会创建一个栈帧。栈帧中包含 局部变量表、操作数栈、动态链接 和 方法返回地址 。当虚拟机栈不满足内存分配需求,会抛出StackOverFlowError和OutOfMemoryError异常。
- 局部变量表:用于存放方法参数和方法内部定义的局部变量,以变量槽(Slot)为单位。
- 操作数栈:类似汇编语言中进行数据操作使用的自定义栈,用于存储操作的内容。
// a = 1 + 2 虚拟机操作
iload_0 //将 1 压入操作数栈
iload_1 //将 2 压入操作数栈
iadd //从操作数栈中弹出 1、2,将算出的值 3 压入操作数栈
istore_2 //把 3 从操作数栈中弹出,保存到本地变量区
- 动态链接:静态链接指的是在类加载(解析)阶段将符号引用转为直接引用,即给出地址,而动态链接指的是在运行期间将符号引用转为直接引用(地址),类似于汇编语言中的间接寻址方式。
- 方法返回地址:指令执行完毕后返回的地址,也即方法调用字节码指令处下一条字节码指令地址,方法调用和返回类似汇编中CALL指令和RET指令。
-
程序计数器(Program Counter Register,线程私有)
各个线程有各自程序计数器,程序计数器主要用来指示当前指令执行到字节码指令的行号,类似汇编语言中指令前面的行号,通过行号可定位指令位置,从而实现分支、循环、跳转等功能。
此区域在JVM规范中没有规定会产生OutOfMemoryError的情况 -
本地方法栈(Native Method Stack,线程私有)
本地方法栈为线程私有的区域,功能类似为Java方法服务的虚拟机栈,但此栈是为native方法服务的。native方法为使用非Java语言而是C/C++等语言实现的方法,例如垃圾回收System.gc()即为native修饰的方法。
当本地方法栈不满足内存分配需求,会抛出StackOverFlowError和OutOfMemoryError异常。
三、直接内存
直接内存(Direct Memory)并不属于虚拟机运行时数据区,也不是Java虚拟机规范中定义的内存区域。但这部分也被频繁使用,也可能产生OutOfMemoryError异常。
直接内存的分配不受Java堆大小的现在,但是会受到本机总内存的限制,在方法区和堆动态扩展时可能会导致各个内存区域大小总和超过物理内存最大容量,从而产生OutOfMemoryError。
四、疑难
- Integer常量池(Integer为final修饰的,不可继承)
public static void main(){
Integer val1 = new Integer("100");
Integer val2 = new Integer("100");
Integer val3 = 100;
Integer val4 = 100;
Integer val5 = 128;
Integer val6 = 128;
System.out.println(val1 == val2);
System.out.println(val3 == val4);
System.out.println(val5 == val6);
}
//结果
//false
//true
//false
原因:当使用直接赋值的时, 实际调用的就是 Integer的valueOf()方法,将其装为一个Integer类,我们来看看valueOf源码。这里IntegerCache.cache是一个存储Integer类型的数组,
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer类静态代码块中通过IntegerCache类向常量池装入了-128 ~ 127(JLS规定至少要载入-128 ~ 127),当自动装箱的值在-128 ~ 127之间则直接使用其cache数组中的Integer对象,因此实际上val3和val4指向的是同一个对象,因此 == 输出为true就是这么来的。
val1和val2由于是自己new的对象,两个不同对象地址不同,因此 == 输出为 false 。
val5 和 val6虽然也是自动装箱,但是可以看到使用的是return new Integer(XX),因此实际上也是两个不同的对象,自然 == 输出为false。
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// 根据虚拟机设定的最大值参数载入常量池,至少为127
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
//这里Max表示至少为127
i = Math.max(i, 127);
// 载入最大值必须小于等于Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
- 字符串常量池(String为final修饰的,不可继承)
Integer的例子明白了这个也应该很好明白。
使用new创建的是对象,对象存放在堆中;直接赋值的方式查找的是常量池中有无当前赋值的字符串,如果有则将变量指向该字符串,否则在常量池中创建一个再将变量指向该串。
那么字符串 + 号连接怎么解释?
//字符串常量的 + 号操作在编译期就连接
String str1 = "abcd";
String str2 = "ab" + "cd";
System.out.println(str1 == str2));
//true
//字符串变量的 + 号操作在运行时连接
String str3 = "aba";
String str4 = "abab";
String str5 = str3 + "b";
System.out.print(str4 == str5);
//false
字符串变量 + 连接由于编译期无法确定,在运行期将连接后的新地址赋给 str5,所以 str5 和 str4 引用的内存地址不同,所以结果为 false。
另外,+ 号操作实际上等于new 了一个StringBuilder,初始值为str3中内容,然后append(b),最终str5指向的是堆中这个StringBulider对象字符串地址。
最后,不要在循环中进行 + 号操作,这会导致每次 + 号操作都生成StringBuilder对象,每次用完之后又丢弃,造成不必要的浪费。
四、笔记目录
JVM关键总结(一)——类加载机制
JVM关键总结(二)——JVM内存结构
JVM关键总结(三)——分代与垃圾回收算法
JVM关键总结(四)——垃圾回收器及调优命令与工具
参考资料:
https://www.cnblogs.com/newAndHui/p/11168791.html
https://blog.csdn.net/qq_41297896/article/details/89949632
https://blog.csdn.net/rongtaoup/article/details/89142396
https://www.jianshu.com/p/d5ff19152854