• 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取出栈顶元素

                     ->在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和-Xms,最小和最大调成一样大。

     

    什么对象会进入老年代?

    1) 15次GC仍然存活的对象

      //因为hotspot实现中动态年龄占4bit(0~15),次数不可能调到>15

    2) 大对象

      //*大对象:对象大小超过eden区的一半

      //大对象的计算标准不是固定的,因为eden区的大小是在运行期动态调整的。

    3) 空间担保

      //针对eden区

      //GC后eden区还剩下的对象,如果from区或to区都不能存下,就会全部进入老年代

    4) 动态年龄判断

      // 针对eden区和from区

      //GC后,Eden区+from区都有剩下对象,如果to区不能存下,就会全部进入老年代

  • 相关阅读:
    移动web技能总结
    canvas绘图基础
    如何自定义滚动条?
    学习笔记-AngularJs(十)
    学习笔记-AngularJs(九)
    硬盘杀手!Windows版Redis疯狂占用C盘空间【转】
    64位win10系统无法安装.Net framework3.5的两种解决方法【转】
    分享一个电子书地址
    阿里、腾讯、百度、华为、京东、搜狗和滴滴最新面试题汇集【转】
    jQuery时间轴
  • 原文地址:https://www.cnblogs.com/RDaneelOlivaw/p/13586605.html
Copyright © 2020-2023  润新知