01-JVM底层原理

mac2024-11-03  12

1、JDK体系结构

JVM属于JRE的一部分

JVM屏蔽了底层系统的差异

JVM分为两个版本,Client VM和Server VM,但JDK8以后基本没有Client VM了,通过命令java -version查看 JRE是JDK的一部分

JRE包含了java程序运行时所需要的底层的类库,大部分是用C和C++语言去写的

JDK除了包含JRE以外,还包含了编译Java代码所需要的编译器、监控JVM的一些监控工具等等

2、JVM整体架构及Java程序执行流程

从上图中可以看出:JVM的作用就是屏蔽底层硬件、指令层面的细节,让java程序实现跨平台运行

跨平台分为两种

编译器层面跨平台:C/C++。底层系统层面的调用,不同的系统要用不同的语句 软件层面跨平台:Java。屏蔽底层硬件、指令层面的细节。类似适配器

2.1、流程说明

Java文件通过javac命令编译成.class文件然后通过java命令把字节码文件加载到JVM上去运行、JVM会将字节码文件转义成系统可识别的机器码(把字节码文件通过JVM的某个类装载子系统加载到运行时数据区中去执行)

JVM会屏蔽系统底层硬件与指令的差异,生成对应系统的机器码,操作系统最底层真正执行的代码是机器码01010101。注意:不同的操作系统,指令码不一样,所以会有对应版本的JDK中JVM的实现

3、JVM内存模型

3.1、JVM组成部分

JVM主要由三大部分组成:类装载子系统、运行时数据区、执行引擎

3.2、运行时数据区(JVM内存模型)

底层操作系统是数据流、指令流、控制流,所以将运行时数据区分为两大类:数据、指令 运行时数据区分为五部分:程序计数器、本地方法栈、虚拟机栈、堆、方法区 线程安全问题 JDK1.8之前和JDK1.8后的版本运行时数据区的变化 元空间是直接的物理内存,可以自动扩容。但是如果无线扩容下去就会挤压其他内存空间,所以需要定义MaxMetaSpaceSize(最大元空间大小)

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。   除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性: -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 -verbose参数是为了获取类型加载和卸载的信息

调优:

Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。 Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。 Xss 是指设定每个线程的堆栈大小 以上三个参数的设置都是默认以Byte为单位的,也可以在数字后面添加[k/K]或者[m/M]来表示KB或者MB。而且,超过机器本身的内存大小也是不可以的,否则就等着机器变慢而不是程序变慢了。 -Xms 为jvm启动时分配的内存,比如-Xms200m,表示分配200M -Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存 -Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

3.2.1、程序计数器

指向当前线程正在执行的字节码指令地址(行号)。线程私有,无GC(线程独享,每个线程都有自己的程序计数器)。

3.2.2、虚拟机栈(线程栈)

也叫线程栈。 虚拟机栈—>栈—>数据结构—>存储数据 存储 当前线程 运行方法时 所需要的数据、指令、返回地址。线程私有,无GC 特点:FILO,先进后出。 内部包含栈帧,每个线程执行每个方法的时候都会在栈中申请一个栈帧。一个方法一个栈帧 线程是用来执行方法的,至于怎么执行,取决于虚拟机栈

3.2.2.1、栈帧详解

栈帧:一个方法对应一块栈帧内存区域。

栈帧中包含:局部变量表、操作数栈、动态链接、方法出口等等

局部变量表:存放程序运行时局部变量的表。是一块内存区域

操作数栈:程序在运行时,对数据进行临时操作的内存区域。数据结构也是栈。是一块内存区域

方法出口:方法被调用的位置(方法调用方调用方法结束后,方法调用方程序计数器的值)。是一块内存区域

动态链接:JVM在执行程序时,会将静态符号解析成符号所对应的直接引用。是一块内存区域

A a= new A(); int b = a.fun(); 前言: 类加载进方法区内存时,会有相应的类元信息(包含指令码); 在new对象时(堆内存),会在对象头里面存放一个对象所属类的类元信息地址(这样就可以知道这个对象是属于哪个类,该指针指向方法区内存中对应类的类元信息--类型指针) 动态链接: 方法在JVM内存中都是以静态符号的形式存在于常量池 存放着静态符号对应的JVM指令码在内存中入口的位置(首地址)(程序在运行到a.fun()时才知道这个位置的) 怎么知道的?因为在new对象的时候,会在对象的对象头中存放一个对象所属类的类型指针,当执行到对象的某个方法时,就会根据这个类型指针动态的找到这个方法所对应的类元信息的指令码,存放到动态链接中。(前提:在程序运行过程中),在程序为运行到此处时,动态链接中保存的是静态符号 JVM执行到含有动态链接的指令码(调用方法时)-->通过静态符号(在常量池中)(两部分组成:类和成员:类.成员)-->JVM执行到静态符号时,会解析静态符号,将静态符号转换成其对应的直接引用(对应的指令码)。这些指令码实际也是静态的,但是一旦运行,就会将这些指令码装载到方法区内存区域,此时该指令码就会有一个入口的内存地址,类型指针会通过静态符号找到该静态符号所拥有的JVM内存地址指令码内存地址指针,然后把内存地址指针存到动态链接中

3.2.2.2、虚拟机栈中一般执行流程

整个过程程序计数器会参与

1、将某种类型的的常量X压入操作数栈 2、将某种类型值存储到局部变量表n(其中n代表位置,局部变量表0放的是this,局部变量表会开辟一块内存空间来存储某个变量)3、如果需要用到存储到局部变量表n中的值参与运算,则从局部变量表n中装载某种类型值到操作数栈 4、执行某种类型的运算操作 5、运算完成后将结果放入操作数栈,然后再存储到局部变量表n中 6、如果需要用到结果返回,则需从局部变量表n中装载某种类型值到操作数栈,然后再返回到方法调用方的栈帧中的操作数栈中,再存储到方法调用方栈中局部变量表里

举例:

1、类A

public class A { public static final int CONTANT = 666; public int fun(){ int a = 2; int b = 5; int c = a*b; return c; } public static void main(String[] args){ A aa= new A(); int bb = aa.fun(); System.out.println(bb); } }

2、反编译字节码文件A.class中fun方法:

生成指令码命令:javap -c 字节码文件 > test.txt

生成更多指令码命令:javap -v 字节码文件 > test.txt 动态链接可以使用此命令查看反编译文件理解

命令:javap -c A.class > a.txt 或 javap -v A.class > a.txt

public int fun(); Code: 0: iconst_2 //将int类型的的常量2压入操作数栈 1: istore_1 //将int类型值存储到局部变量表1 2: iconst_5 //将int类型的的常量5压入操作数栈 3: istore_2 //将int类型值存储到局部变量表2 4: iload_1 //从局部变量表1中装载int类型值到操作数栈 5: iload_2 //从局部变量表2中装载int类型值到操作数栈 6: imul //执行int类型的乘法 7: istore_3 //将int类型值存储到局部变量表3 8: iload_3 //从局部变量表3中装载int类型值到操作数栈 9: ireturn //从方法中返回int类型的数据

3、图例说明:

3.2.2.3、问题

1、递归调用方法,栈里面有多少个栈帧? 多个。会栈溢出 2、每个栈帧的大小是不是一样的?为什么?

3.2.3、堆

存储类实例,一个JVM实例只有一个堆内存,线程共享,要GC 可通过:jvisualvm命令启动JVM工具查看内存使用情况

3.2.3.1、堆内存详解

堆分为年轻代和老年代; 年轻代又分为Eden区和Suvivor区; Survivor又分为FromSpace和ToSpace 年轻代占用1/3的堆空间; 老年代占用2/3的堆空间 Eden区:Survicor区 = 4:1 Eden区:s0区:s1区 = 8:1:1 Minor GC:新生代GC,指发生在新生代的垃圾收集动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂,效率高 Major GC/Full GC:老年代GC,指发生在老年代的GC 新生代:新创建的对象都是用新生代分配内存,新new出来的对象首先放在Eden区,Eden空间不足时,触发Minor GC,这时会把存活的对象转移进Survivor区。 老年代:老年代用于存放经过多次Minor GC之后依然存活的对象 JVM分别对新生代和老年代采用不同的垃圾回收机制 GC触发条件:Eden区满了触发Minor GC,这时会把Eden区存活的对象复制到Survivor区,当对象在Survivor区熬过一定次数的Minor GC之后(通过分代年龄判断,默认15(年轻代),每次Minor GC,对象的对象头的分代年龄就会加1),就会晋升到老年代(当然并不是所有的对象都是这样晋升的到老年代的),当老年代满了,就会报OutofMemory异常。 如果Eden区触发minor gc,通过GC复制回收算法进入Survivor区,如果Surver区装不下某个大对象,就会触发担保机制,从而进入老年代。 新生代的GC(Minor GC): 新生代通常存活时间较短基于Copying算法进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代 老年代的GC(Major GC/Full GC): 老年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗 GC算法 复制算法:扫描出存活的对象,并复制到一块新的完全未使用的空间中 可达性分析算法:这个算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径陈伟引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明对象是不可用的,可进行回收。 GC Roots根节点:类加载器、Thread、`虚拟机栈的本地变量表`static成员、常量引用、本地方法栈的变量等等 Created with Raphaël 2.2.0 new 类名(); 根据new的参数在常量池(元空间)中定位一符号引用 若未找到引用,则执行类的加载、验证、初始化 虚拟机为对象分配内存(堆) 将分配的内存初始化为零值 调用对象的init方法

堆内存分配原则:

1、优先分配Eden区 2、大对象直接分配到老年代 3、长期存活的对象分配到老年代 4、空间分配担保 检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小 5、动态对象年龄对象 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代

3.2.3.2、问题

1、为什么要分代? 2、为什么Eden:s0:s1=8:1:1?

3.2.4、本地方法栈

存储当前线程运行本地方法所需要的数据、指令、返回地址。线程私有,无GC

每一个线程都可能调用本地方法

native修饰的方法,底层不是Java实现,而是C/C++实现

非Java语言实现的方法在执行的过程中,也有内部的变量需要分配内存存放,所以就是用到本地方法栈内存区域

用于支持native方法的执行,存储了每个native方法调用的状态。

3.2.5、方法区

存放了要加载的类信息(字段方法的的字节码、部分方法的构造器,也称类元信息)常量、运行时常量池(JDK1.7+有变化)、静态变量、JIT(即时编译的信息,JDK1.7以前)。线程共享,无GC,非堆区(non-heap)

方法区是一种定义、概念,而所谓的永久代或元空间是其一种实现机制。

在JDK1.8之前方法区代表的是永久代(PermanetGeneration),JDK1.8及以后用元空间(Meta Space)代替。元空间是直接内存

JDK1.6及之前:有永久代,常量池在方法区 JDK1.7:有永久代,但已逐步去“永久代”,常量池在堆 JDK1.8之后:无永久代,常量池在元空间
最新回复(0)