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区不能存下,就会进入老年代

posted @ 2020-08-31 13:35  丹尼尔奥利瓦  阅读(481)  评论(0编辑  收藏  举报