实例代码运行JVM内存处理全流程
提问:当一个实例在运行时我们的JVM究竟发生了什么?
先写个代码,然后运行
public class JVMObject {
public final static String MAN_TYPE = "man"; // 常量
public static String WOMAN_TYPE = "woman"; // 静态变量
public static void main(String[] args)throws Exception {
Teacher t1 = new Teacher();
}
}
代码运行时,JVM共做了5件事 :
1、JVM申请内存
就是向我们的操作系统申请内存,根据我们设置好的参数 (-Xms30m -Xmx30m -XX:MaxMetaspaceSize=30m )获得元空间、虚拟机栈等需要的内存。
2、初始化运行时数据区
由于方法区和堆属于线程共享所以会先初始化出来
3、类加载
该过程会将类信息、一些静态常量、静态变量等信息添加到方法区
4、执行方法
在我们的代码里面执行到main方法,虚拟机栈属于线程私有所以方法执行时初始化内存,然后压入栈帧,
5、创建对象
执行Teacher t1 = new Teacher();
首先现在堆中创建一个对象
然后在栈中添加一个引用指向堆中的Teacher
堆的垃圾回收(只简单概述,后面会细谈)
对上面代码进行加工
public class JVMObject {
public final static String MAN_TYPE = "man"; // 常量
public static String WOMAN_TYPE = "woman"; // 静态变量
public static void main(String[] args)throws Exception {
Teacher t1 = new Teacher();
t1.setName("Mark");
tT1.setSexType(MAN_TYPE);
t1.setAge(36);
for(int i =0 ;i<15 ;i++){
System.gc();//主动触发GC 垃圾回收 15次--- t1存活
}
Teacher t2 = new Teacher();
t2.setName("King");
t2.setSexType(MAN_TYPE);
t2.setAge(18);
Thread.sleep(Integer.MAX_VALUE);//线程休眠
}
}
注意:这是演示代码,千万不要代码中使用 System.gc();java有很好的自动垃圾回收机制,不需要我们手动去触发。而且垃圾回收器执行时会占用很多的资源,会影响到项目正常的运行。
堆分为4个区域Eden、From、To、Tenured
一般情况下大多数新创建的对象都会在Eden中,Tenured是存放比较难回收的对象,From和To的存在是为了充分利用空间和高效垃圾回收设计的,不细说后面会细细描述。
首先执行上面代码,之后堆中对象的分布是这样的
如果执行这段代码
public class JVMObject {
public final static String MAN_TYPE = "man"; // 常量
public static String WOMAN_TYPE = "woman"; // 静态变量
public static void main(String[] args)throws Exception {
Teacher t1 = new Teacher();
t1.setName("Mark");
tT1.setSexType(MAN_TYPE);
t1.setAge(36);
Teacher t2 = new Teacher();
t2.setName("King");
t2.setSexType(MAN_TYPE);
t2.setAge(18);
Thread.sleep(Integer.MAX_VALUE);//线程休眠
}
}
之后堆中对象的分布是这样的
我们发现两段代码的不同之处只是for循环15次的垃圾回收
也就是说,在我们创建对象最开始是在Eden区,经历了15次垃圾回收失败之后(t1属于强引用,回收不了)存入位置换到了Tenured区。
其实在进入到Ternured区之前,t1一直在From和To区通过复制清除算法来回切换。直到第15次回收失败才到Ternured区。
至于为什么是15次,原因是这样的:每个对象有个对象头,对象头中有一块区域存放的是age,这个区域大小是4位,2的四次方=16,在刚进入Eden区时是1,所以只能进行15次。
栈优化技术——栈帧之间数据的共享
在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的 JVM 在实现中会进行一些优化,使得两个栈帧出现一部分重叠。(主要体现在方 法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接公用一部分数据,无需进行额外的参数复制传递了。
代码示例:
public class JVMStack {
public int work(int x) throws Exception{
int z =(x+5)*10;//局部变量表有, 32位
Thread.sleep(Integer.MAX_VALUE);
return z;
}
public static void main(String[] args)throws Exception {
JVMStack jvmStack = new JVMStack();
jvmStack.work(10);//10 放入main栈帧 10 ->操作数栈
}
}
通过代码我们看到 jvmStack.work(10)方法传入常量10,此时这个10在main栈帧的操作数栈上。而随后又将压入新的栈帧用来执行work方法,而此时work栈帧第一件事,肯定是将main方法传入过来的常量10,通过work的操作数栈存入到局部变量表中。
事实上,在JVM中work局部变量表存放的常量10,和main方法操作数栈的常量10共用一块物理内存。这就意味着,jvm直接将main栈帧操作栈上的常量10,用作work栈帧局部变量表存放常量10,共用物理内存,简化操作,极大节省了内存和提高了效率。
对于上面的代码执行使用 JHSDB 工具查看栈空间下面红线标红的重叠部分就此存放常量10的物理内存。
声明:本文为学习享学课堂三期JVM章节课后所做的学习总结,文中借鉴King老师上课笔记和课堂PPT。