1. 工具以及配置
要可视化代码在内存中的位置,以及虚拟机是如何划分运行时数据区的,我们需要使用到HSDB工具,该工具位于JDK\lib目录下。
我们需要的工具如下:
- JDK\lib目录下的sa-jdi.jar
- JDK\jre\bin目录下的sawindbg.dll动态库 (注意!!需要把这个动态库copy到JDK\lib)
工具准备好了,那就行动吧。
-
Step1: 进入到JDK\lib,我的目录是:F:\Java\JDK\lib
-
Step2:打开命令行控制台
-
Step3:运行指令: java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
如果出现以下界面说明配置完成了。
2.测试代码以及开始分析
public class ObjectInJvm {
public final static String MAN_TYPE = "man"; //常量 存方法区
public static String WOMAN_TYPE = "woman"; //静态变量 存方法区
public static void main(String[] args) throws Exception {
Student T1 = new Student(); // Student对象 存堆区 T1放在栈帧的局部变量表中
T1.setName("DY");
T1.setSexType(WOMAN_TYPE);
T1.setAge(26);
for (int i = 0; i < 15; i++) {
System.gc(); // 经历15次GC,进入老年代
}
Student T2 = new Student(); // Student对象 存堆区 T2放在栈帧的局部变量表中
T2.setName("JackOu");
T2.setSexType(MAN_TYPE);
T2.setAge(29);
Thread.sleep(Integer.MAX_VALUE); //本地方法栈
}
}
class Student {
String name;
String sexType;
int age;
......
//get,set方法
}
2.1 介绍工具
- 使用javac和java工具将代码编译后运行起来如下:
- 使用指令:jps 查看进程号,我们demo进程号是:12328
- 打开HSDB,点击file-> attach to hotspot process-> 输入进程号,即可看到demo的线程信息如图
- 选中main线程,点击第三个按钮(show java stack trace),显示main线程栈帧信息如下
- 选中main线程,点击第二个按钮(show the stack memory),显示当前线程栈区的内存地址如下,下一小节会基于此图分析程序
3. 程序运行时内存变化分析
3.1 JVM运行java代码处理过程
-
JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间。
-
JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。
-
完成上一个步骤后, JVM 首先会执行构造器,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,静态变量和常量放入方法区。
-
执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 Student对象,对象引用 student 就存放在栈中。执行过程参照上一篇文章《JVM内存管理》中的“运行时数据区执行过程”小节。
3.2 运行时数据区分析
- 从第二节中运行代码可以在HSDB中看到栈区的内存分布,如下图是每块内存的介绍:
- 在HSDB中选择Heap Parameters可以看到运行时数据区堆区的分配情况
注意:准备放大招了,前方大招预警!!!!
因此我们把以上从HSDB中看到的内存地址和各个区放在同一张图中,就可以很清晰得看清楚代码在运行时,对象在各个区中的运动轨迹。如下图
运行时数据区结合HSDB分析.png//开始认真分析!!!
//=============第1步=================
//new Student()在堆区创建一个对象,并且在栈区的局部变量表中引用这个新对象的地址
Student T1 = new Student();
// 在堆区内存空间中找到对应位置并赋值
T1.setName("DY");
T1.setSexType(WOMAN_TYPE);
T1.setAge(26);
//=============第2步=================
// 因为T1是强引用,系统无法回收T1所指的对象,因此T1被挪到了老年代,从地址也可以看到
for (int i = 0; i < 15; i++) {
System.gc();
}
//=============第3步=================
//new Student()在堆区创建一个对象,并且在栈区的局部变量表中引用这个新对象的地址
//由于是才创建的,从地址可以看到T2所指向的对象被分配在Eden区
Student T2 = new Student();
T2.setName("JackOu");
T2.setSexType(MAN_TYPE);
T2.setAge(29);
4. 总结对JVM堆和栈的认识
-
功能角度
以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
-
线程独享还是共享
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
-
空间大小
栈的内存要远远小于堆内存
谈到内存空间大小,在开发中经常遇到OOM,要么是内存泄漏导致堆溢出,要么是递归调用没有选好结束条件导致栈溢出等等,那么下面我们再来看看OOM包括些什么。
4.1 堆溢出
堆内存溢出:创建对象时申请内存空间,超出最大堆内存空间。
解决方案:如果不是内存泄漏,就是说内存中的对象却是都是必须存活的,那么久应该检查JVM的堆参数设置,与机器的内存对比,看是否还有可以调整的空间,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。Android开发中常见的是因为内存泄漏和Bitmap管理不当造成堆溢出。
4.2 栈溢出
栈溢出:因为每个方法的执行都需要打包成栈帧,一般的方法调用是很难出现的,如果出现了可能会是无限递归。另一种可能是大量创建线程,JVM不断申请栈内存,导致机器没有足够的内存报OOM,因此一般在开发中,最好用线程池来管理线程。
4.3 方法区溢出
- 运行时常量池溢出
- 方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们配置。
5. 虚拟机优化技术
5.1 对内存的优化
在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的JVM在实现中会进行一些优化,使得两个栈帧出现一部分重叠。(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接公用一部分数据,无需进行额外的参数复制传递了。
JVM栈空间优化.pngpublic class JvmStackOpt {
public int work(int x) throws Exception{
int z =(x+5)*10;
Thread.sleep(Integer.MAX_VALUE);
return z;
}
public static void main(String[] args)throws Exception {
JvmStackOpt jvmStack = new JvmStackOpt();
jvmStack.work(10); // 变量10的内存空间被共享了
}
}
JVM栈空间优化取证.png
5.2 编译优化技术——方法内联
public static void main(String[] args) {
// boolean i1 = max(1,2);
//调用max方法: 虚拟机栈 --入栈(max 栈帧)
boolean i1 = 1>2;
}
public static boolean max(int a,int b){//方法的执行入栈帧。
return a>b;
}
网友评论