JDK8内存模型
作者 | 时间 |
---|---|
雨中星辰 | 2023-01-15 |
方法区
方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、方法区中的静态变量是内存引用的地址,实际对象仍在对中创建。
例如:
源码如下:
public class MathUtil {
public static String aaa = "sdf";
public static final double PI=3.14;
public int add() {
int a = 100;
int b = 200;
int c = a + b;
return c;
}
public static void main(String[] args) {
MathUtil mathUtil = new MathUtil();
mathUtil.add();
}
}
class字节码反编译成可读的指令:javap -v MathUtil
Classfile /Users/star/opensource/la/target/classes/MathUtil.class
Last modified 2023-1-5; size 722 bytes
MD5 checksum 6ede24c108f9167fb00261f045de898e
Compiled from "MathUtil.java"
public class MathUtil
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#36 // java/lang/Object."<init>":()V
#2 = Class #37 // MathUtil
#3 = Methodref #2.#36 // MathUtil."<init>":()V
#4 = Methodref #2.#38 // MathUtil.add:()I
#5 = String #39 // sdf
#6 = Fieldref #2.#40 // MathUtil.aaa:Ljava/lang/String;
#7 = Class #41 // java/lang/Object
#8 = Utf8 aaa
#9 = Utf8 Ljava/lang/String;
#10 = Utf8 PI
#11 = Utf8 D
#12 = Utf8 ConstantValue
#13 = Double 3.14d
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 LMathUtil;
#22 = Utf8 add
#23 = Utf8 ()I
#24 = Utf8 a
#25 = Utf8 I
#26 = Utf8 b
#27 = Utf8 c
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 mathUtil
#33 = Utf8 <clinit>
#34 = Utf8 SourceFile
#35 = Utf8 MathUtil.java
#36 = NameAndType #15:#16 // "<init>":()V
#37 = Utf8 MathUtil
#38 = NameAndType #22:#23 // add:()I
#39 = Utf8 sdf
#40 = NameAndType #8:#9 // aaa:Ljava/lang/String;
#41 = Utf8 java/lang/Object
{
public static java.lang.String aaa;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC
public static final double PI;
descriptor: D
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: double 3.14d
public MathUtil();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LMathUtil;
public int add();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: iload_1
8: iload_2
9: iadd
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 10: 0
line 11: 3
line 12: 7
line 13: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this LMathUtil;
3 10 1 a I
7 6 2 b I
11 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class MathUtil
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method add:()I
12: pop
13: return
LineNumberTable:
line 17: 0
line 18: 8
line 19: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
8 6 1 mathUtil LMathUtil;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #5 // String sdf
2: putstatic #6 // Field aaa:Ljava/lang/String;
5: return
LineNumberTable:
line 6: 0
}
SourceFile: "MathUtil.java"
虚拟机栈
image.png局部变量表:用于存放栈帧中的局部变量
操作数栈:将class转换为底层的操作数
例如:
public int add() {
int a = 100;
int b = 200;
int c = a + b;
return c;
}
转换后为:
0: bipush 100 # bipush 将一个8位带符号整数压入栈,这里为将100压入栈
2: istore_1 # istore_1 将int类型值存入局部变量1,通过局部变量表,可以找到局部变量1的变量名为a,则就是将100存入a
3: sipush 200 # 将16位带符号整数压入栈,则立为将200压入栈
6: istore_2 # 将int类型值存入局部变量2,这里为将200存入到变量b中
7: iload_1 # 从局部变量1中装载int类型值,这里为取出100
8: iload_2 # 从局部变量2中装载int类型值,这里为取出200
9: iadd # 执行int类型的加法,这里为100+200
10: istore_3 # 将int类型值存入局部变量3,这里为将300存入变量c中
11: iload_3 # 从局部变量3中装载int类型值,这里为从c中取出值300
12: ireturn # 从方法中返回int类型的数据,这里为将300返回
动态链接:在jvm文件被编译到字节码文件中,所有的变量和方法引用都作为符号链接保存在class文件的常量池中,比如描述一个方法调用了另外的其他方法,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是将这些符号引用转换为调用方法的直接引用。
例:
Constant pool:
#1 = Methodref #7.#36 // java/lang/Object."<init>":()V
#2 = Class #37 // MathUtil
#3 = Methodref #2.#36 // MathUtil."<init>":()V
#4 = Methodref #2.#38 // MathUtil.add:()I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V // 参数为字符串数组,返回值为void
flags: ACC_PUBLIC, ACC_STATIC // 方法是public static
Code:
stack=2, locals=2, args_size=1
0: new #2 // new 创建类实例,从常量池中找到#2为MathUtil
3: dup // dup 复制栈顶部一个字长内容
4: invokespecial #3 // 根据编译时类型来调用实例方法,这里为调用MathUtil."<init>":()V方法
7: astore_1 // astore_1 将引用类型或returnAddress类型值存入局部变量1
8: aload_1 // aload_1 从局部变量1中装载引用类型值
9: invokevirtual #4 // invokevirtual 调用对象的实例方法,这里为调用MathUtil.add:()I方法
12: pop // pop 弹出栈顶端一个字长的内容
13: return // return 从方法中返回,返回值为void
方法出口:记录该方法被其他方法调用时的位置,以便在该方法执行完毕后,跳回被调用的方法中,继续执行后面。
例:
image.png本地方法栈
每个线程栈在执行本地方法时,都会字啊栈中分配一块内存,用于执行本地方法。
程序计数器
记录程序运行到那个位置了,也就是行号。
因为程序在系统中,是以多线程方式运行到,程序计数器可以方便告诉CPU,线程在那里挂起到,再重新得到CPU后,继续执行。
堆
image.png堆分为年轻代和老年代,其中年轻代占1/3空间,老年代占2/3空间。
年轻代中有分为Eden区和两个survivor区,其中Eden区占年轻代8/10的空间,两个survivor分别占1/10的空间。
大多数情况下,对象被分配在年轻代的Eden区,大对象会被直接放到老年代,栈上分配的对象被放到栈上,当栈桢弹出后,对象及所在空间被清理。
大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可。
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小。
哪些情况对象会被放入老年代?
-
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
为了避免为大对象分配内存时的复制操作而降低效率。
-
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。 -
对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的
-
老年代空间分配担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,fullgc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM
什么情况下会触发full gc?
- 在程序中调用了
System.gc()
,可能会触发full gc。 - 老年代空间不足
- 方法区满了,这种情况比较简单,在启动脚本中配置合适方法区参数即可:
-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
,方法区通常在启动后变动比较小了,如果应该方法区导致full gc,通常现场为启动比较慢,因为在启动过程中会频繁触发full gc,导致程序卡顿。 - 当大对象进入老年代,大对象指需要大量连续内存空间的java对象,例如很长的数组,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
网友评论