JVM: 内存模型
JVM四大模块:运行时数据区(内存模型?)、类加载器子系统、执行引擎、GC(垃圾回收器)。
图中箭头表示存在引用关系
虚拟机栈指向方法区 -- 动态链接
虚拟机栈指向堆区 -- 局部变量 e.g. Test obj = new Test();
方法区指向堆区 -- 静态引用类型的属性
堆区指向方法区 – 类型指针
//对象在内存中的存储结构:
对象(在堆区)的header中,有类型指针Klass pointer指向该对象的instanceKlass实例(类的元信息,在方法区)。
实验: 证明对象中类型指针指向InstanceKlass
HSDB attach Test程序的进程->点击main线程->找到Test对象的内存地址->inspector输入地址查看->显示压缩指针确实指向InstanceKlass
在JVM中,并不存在JVM内存模型的实体。
/openjdk/hotspot/src/share/vm/memory/metaspace.hpp
class Metaspace : public CHeapObj<mtClass> { … }
JVM中所有的内存模块都是CHeapObj、ValueObj和AllStatic这三个类之一的子类。这三个类的所有子类被统称为JVM内存模型。
/openjdk/hotspot/src/share/vm/adlc/arena.hpp
// All classes in the virtual machine must be subclassed // by one of the following allocation classes: // For objects allocated in the C-heap (managed by: free & malloc). // - CHeapObj // For embedded objects. // - ValueObj // For classes used as name spaces. // - AllStatic // class CHeapObj {…}; class ValueObj {…}; class AllStatic {…};
名称概念
class文件: 硬盘上的.class文件
class content: 类加载器文件将.class文件加载进内存后,存储字节码数据的那块内存区域。
/openjdk/hotspot/src/share/vm/classfile/classfile.hpp
instanceKlassHandle ClassFileParser::parseClassFile(…) { … ClassFileStream* cfs = stream(); // class content就是这里的stream字节流 … }
class对象: 反射获取到的class对象。(在JVM中,真正获取到的是instanceMirrorKlass的实例)
e.g.
Class clazz = Test.class;
对象: java代码中new获取到的对象。E.g.
Test obj = new Test();
程序计数器:JVM中模拟的程序计数器是字节码的索引。OS中真正的程序计数器是EIP(for 32位机)或RIP(for 64位机)。
查看程序计数器:jclasslib查看字节码,打开某个方法,各指令前的数字序号。
对操作系统来说JVM相当于一个大的内存池。
内存池:在OS heap里划分出一块小的堆。内存池又划分为很多小块的memory chunk。
e.g. CHeapObj、ValueObj和Allstatic类相当于memory chunk,一般对象相当于memory cell。
方法区
*类加载器子系统将解析得到的instanceKlass和instanceMirrorKlass实例分别存储在方法区和堆区
方法区是(理论)规范,永久代、元空间是具体实现。
永久代: openjdk 1.8以前的方法区的实现,在堆区。Jdk8以后用元空间代替。
元空间:openjdk1.8及以后方法区的实现,在直接内存(OS内存)。
JVM为什么用元空间取代了永久代?
1) 便于写GC算法,因为元空间将方法区和堆区分开放置。永久代中,类的元信息、字符串全部都放在堆区,而堆区是放置对象的,GC算法需要区分当前需要标记的是对象、字符串还是元信息,比较难写。
2) 避免永久代OOM。类的元信息后面可以有动态生成(e.g. cglib:反射/动态代理等底层有用到的一种自动生成技术),使用出错时有可能出现无限创建。
3) 硬件的发展。以前32位机时内存最大只有4G,分给内核层和应用层各2G。为了限制程序能使用的内存大小,永久代将方法区和堆区放在一起管理。如今内存增大,有些应用需要的内存较大,都放在堆区管理的做法已经不太合理。
4) 避免字符串OOM。Jdk6时字符串存储在永久代中,jdk7及以后字符串存储在堆区。
JVM不做任何调优的情况下,元空间最大/最小是多少?
java -XX:+PrintFlagsFinal -version | grep Metaspace
e.g.
最小: MetaspaceSize (bytes)
最大: MaxMetaspacesSize (bytes)
元空间如何调优?
*调节堆区用-Xms和-Xmx,一般将堆区的最大和最小调成一样大
-XX:MetaspaceSize; -XX:MaxMetaspaceSize
- 元空间的调优同理,将最大和最小调成一样大。(原因:防止内存抖动)
* 内存抖动: e.g. 线程池中设置了最小线程数min和最大线程数max,如果任务池里的任务较多,线程池就会自动扩容线程到max个。过了一段时间后任务执行完,max个线程会占内存消耗系统资源,线程池会自动销毁线程。内存忽大忽小,使用时需要判断是否需要调节,给程序的稳定性和性能带来不必要的开销。
- 调成多大:一般选择物理内存的1/32。
- 程序运行时查看元空间实际占用多少内存:使用工具visualVM(有GUI,适用中小型公司)或arthas(可在服务器使用)。
本地方法栈
本地方法栈:Java调用C/C++的动态链接库,运行里面的函数时所要用到的栈。i.e. JNI
随着socket的发展(稳定、性能高、兼容性强),本地方法栈已经逐渐不被使用。
虚拟机栈
一个JVM中有几个虚拟机栈?
- 每个线程一个。
一个虚拟机栈中有几个栈帧?
- 方法调用次数 个。
栈帧:虚拟机栈中一种更小的单位。存放每个方法的实参和局部变量等信息,便于更清晰地处理方法的运行。
怎么查看虚拟机栈大小?
命令:java –XX:+PrintFlagsFinal –version | grep StackSize
查看ThreadStackSize的值。
e.g. 虚拟机栈默认大小是1024K
**实验:一个栈帧占多少字节?
-> 将栈大小调成160k (用-Xss命令调节ThreadStackSize) //疑问:为什么是160k?100k不行吗?
-> 把栈搞成OOM,看创建了多少栈帧(栈深度),计算得到一个栈帧的字节数(160*1024/帧深度) //尚未做该实验,不知道结果
栈帧包含5个区域:局部变量表、操作数栈、动态链接、返回地址、附加信息。
*附加信息:建议存放调试信息。参考《java虚拟机规范》
动态链接:方法对应的JVM对象在元空间中的内存地址
返回地址:保存现场
局部变量表:存储局部变量的表。Jclasslib -> Methods/方法名/Code/LovalVariableTable可查看。E.g.
操作数栈:存储操作数的栈。可对其进行push/pop等栈操作。Jclasslib -> Methods/方法名/Code可查看指令中对操作数栈所做的bipush等操作。E.g.
类的方法 在方法区的存储
一个类解析完以后生成的klass对象存储在方法区,klass对象的方法对象集合中存储该类的方法对象。
每个方法对象存放class文件中解析出的对应方法信息e.g. 局部变量表大小,操作数栈大小,access flag,方法体字节码etc。
e.g. 案例代码
public class Test { public static Test t=new Test(); public static void main(String[] args) { Test demo=new Test(); System.out.println(demo.add()); } public int add() { int a = 10; int b = 20; return a+b; } }
IDEA run过程包括:
1) 调用javac命令将.java文件编译成.class文件
2) 调用java命令运行.class文件 //JVM此时开始运行
在JVM实现中,方法存放在InstanceKlass的虚表vtable里。一个vtable相当于一个类中各方法对象的集合e.g. list<MethodObject*>。
/home/lily/Documents/openjdk/hotspot/src/share/vm/oops/instanceKlass.hpp
int _vtable_len; // length of Java vtable (in words)
main方法字节码:*解释参考《字节码手册》
对应java代码中的”Test demo=new Test();”语句:
0 new #2 <com/experiment/aaa/jvm/Test>
-> 在堆区生成了一个不完全对象(InstanceOopDesc,未执行构造方法的对象)
-> 将不完全对象的指针(指向堆区)压入操作数栈
3 dup
//duplicate 用处:将对象指针作为this进行传参
-> 复制栈顶元素(不完全对象)
-> 压入操作数栈 //此时操作数栈中有2个不完全对象指针
4 invokespecial #3 <com/experiment/aaa/jvm/Test.<init>>
-> 执行invokespecial指令,完成运行方法的环境构建
/*在构建环境的过程中完成了this指针赋值:
->pop取出栈顶元素(this指针)
->在init方法局部变量表index0给this指针赋值 */
*非静态方法的第一个参数(index0位置)一定是this指针
-> 执行构造方法
//这句(执行构造方法)执行完,栈中指针指向的变为完全对象
7 astore 1
-> pop栈顶元素
-> 将完全对象的地址赋值给局部变量表index1的位置
查看main方法局部变量表证明index1确实指向new得到的对象(demo):
JVM运行main方法,内部是怎么做的?
* 线程保存有2个指针属性:局部表开始指针、操作数栈当前指针
1) 创建运行main方法需要的栈帧
2) 将main方法的操作数栈当前指针赋值给线程的操作数栈当前指针
3) 将main方法的局部表开始指针赋值给线程的局部表开始指针
JVM运行被调用的方法,内部是怎么做的?
e.g. 在main方法中调用add方法
1) 创建运行callee方法(add)需要的栈帧
2) 在callee方法(add)的栈帧中保存caller方法(main)字节码的下一行程序计数器(15的下一行==18)
3) 线程的局部表开始指针(指向caller (main)的局部变量表)保存至callee方法(add)的栈帧
4) 线程的操作数栈当前指针(指向caller (main)的操作数栈)保存至callee方法(add)的栈帧
5) 将callee方法(add)的局部表开始指针赋值给线程的局部表开始指针
6) 将callee方法(add)的操作数栈当前指针赋值给线程的操作数栈当前指针
堆
堆的最小大小:物理内存的1/64
堆的最大大小:物理内存的1/4
新生代和老年代大小比例为1:2,Eden区、From区和To区大小比例为8:1:1。
如何调优? -Xms和-Xmx,最小和最大调成一样大。
什么对象会进入老年代?
1) 15次GC仍然存活的对象
//因为hotspot实现中动态年龄占4bit(0~15),次数不可能调到>15
2) 大对象
//*大对象:对象大小超过eden区的一半
//大对象的计算标准不是固定的,因为eden区的大小是在运行期动态调整的。
3) 空间担保
//针对eden区
//GC后eden区还剩下的对象,如果from区或to区都不能存下,就会进入老年代
4) 动态年龄判断
// 针对eden区和from区
//GC后,Eden区+from区都有剩下对象,(eden区剩下的在from区放不下)如果to区不能存下,就会进入老年代