java虚拟机系列-------JVM结构

it2022-07-02  110

JVM结构

JVM的内部体系结构分为三部分  (1)类装载器(ClassLoader)子系统   作用: 用来装载.class文件  (2)执行引擎     作用:执行字节码,或者执行本地方法  (3)运行时数据区     方法区,堆,java栈,PC寄存器,本地方法栈 (4)Native Interface本地接口     作用是融合不同的编程语言为Java所用      1.1 jvm类加载器介绍: 1.1.1 JVM将整个类加载过程划分为了三个步骤:

(1)装载   装载过程负责找到二进制字节码并加载至JVM中。JVM通过类名、类所在的包名通过ClassLoader来完成类的加载,同样,也采用以上三个元素来标识一个被加载了的类:类名+包名+ClassLoader实例ID。 (2)链接   链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。在完成了校验后,JVM初始化类中的静态变量,并将其值赋为默认值。最后一步为对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备应的权限(例如public、private域权限等),会造成NoSuchMethodError、NoSuchFieldError等错误信息。 (3)初始化   初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化。   在四种情况下初始化过程会被触发执行:   1.调用了new;   2.反射调用了类中的方法;   3.子类调用了初始化;   4.JVM启动过程中指定的初始化类。    1.1.2 JVM两种类装载器包括:启动类装载器和用户自定义类装载器:   启动类装载器是JVM实现的一部分,用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类。

主要分为以下几类:   (1) Bootstrap ClassLoader   这是JVM的根ClassLoader,它是用C++实现的,JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOME中jre/lib/rt.jar(Sun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。   (2) Extension ClassLoader   JVM用此classloader来加载扩展功能的一些jar包   (3) System ClassLoader   JVM用此classloader来加载启动参数中指定的Classpath中的jar包以及目录,在Sun JDK中ClassLoader对应的类名为AppClassLoader。   (4) User-Defined ClassLoader   User-DefinedClassLoader是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录    1.1.3 ClassLoader抽象类提供了几个关键的方法: (1)loadClass   此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从parent ClassLoader中寻找,如仍然没找到,则从System ClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法 (2)findLoadedClass   此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native的方法。 (3) findClass   此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。 (4) findSystemClass   此方法负责从System ClassLoader中寻找类,如未找到,则继续从Bootstrap ClassLoader中寻找,如仍然为找到,则返回null。 (5)defineClass   此方法负责将二进制的字节码转换为Class对象 (6) resolveClass   此方法负责完成Class对象的链接,如已链接过,则会直接返回。

1.2 JVM执行引擎

1.2.1 JVM通过执行引擎来完成字节码的执行,在执行过程中JVM采用的是自己的一套指令系统 每个线程在创建后,都会产生一个程序计数器(pc)和栈(Stack),其中程序计数器中存放了下一条将要执行的指令,Stack中存放Stack Frame,栈帧,表示的为当前正在执行的方法,每个方法的执行都会产生Stack Frame,Stack Frame中存放了传递给方法的参数、方法内的局部变量以及操作数栈,操作数栈用于存放指令运算的中间结果,指令负责从操作数栈中弹出参与运算的操作数,指令执行完毕后再将计算结果压回到操作数栈,当方法执行完毕后则从Stack中弹出,继续其他方法的执行。

在执行方法时JVM提供了invokestatic、invokevirtual、invokeinterface和invokespecial四种指令来执行 (1)invokestatic: 调用类的static方法 (2)invokevirtual: 调用对象实例的方法 (3)invokeinterface:将属性定义为接口来进行调用 (4)invokespecial: JVM对于初始化对象(Java构造器的方法为:)以及调用对象实例中的私有方法时。

1.2.2 反射机制是Java的亮点之一,基于反射可动态调用某对象实例中对应的方法、访问查看对象的属性等(java反射)

1.3 JVM内存模型 1.3.1 程序计数器

程序计数器是一块较小的空间,可以看作是当前线程执行的字节码的行号指示器,字节码解释器在工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能。 由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。所以每条线程都有一个独立的程序计数器,使各个线程之间计数器互不影响。独立存储,这个称为”线程私有“的内存。 如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。

1.3.2 虚拟机栈 栈也是私有的,它的生命周期与线程相同。

栈是用来描述java方法执行的内存模型,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。每个方法在执行的同时会创建栈用于存储局部变量、操作数栈、动态链接、方法出口等信息;局部变量中存储了编译期可知的各种基本类型、对象的引用、和returnAddress类型。

对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

1.3.3 本地方法栈 本地方法栈(Native MethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。 1.3.4 方法区 方法区是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。 1.3.4.1 简单说方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、连接(验证、准备、解析)和初始化。而装载(后的结果就是由.class文件转变为方法区中的一段特定的数据结构。这个数据结构会存储如下信息:

1.3.4.1.1 类型信息

这个类型的全限定名 这个类型的直接超类的全限定名 这个类型是类类型还是接口类型 这个类型的访问修饰符 任何直接超接口的全限定名的有序列表

1.3.4.1.2 字段信息 字段名 字段类型 字段的修饰符

1.3.4.1.3 方法信息 方法名 方法返回类型 方法参数的数量和类型(按照顺序) 方法的修饰符

1.3.4.1.4 其他信息 除了常量以外的所有类(静态)变量 一个指向ClassLoader的指针 一个指向Class对象的指针 常量池(常量数据以及对其他类型的符号引用)

1.3.5.2 方法区主要有以下几个特点:

1、方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待

2、方法区的大小不必是固定的,JVM可根据应用需要动态调整。同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是JVM自己的堆)中自由分配。

3、方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集

可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小(实际为限制非堆内存的大小,其中包含方法区)。关于内存设置详见这里

1.3.5 JAVA堆 堆内存中最大的一块,被所有的线程共享,唯一的目的就是存放对象实例,堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。 还可以细分为新生代和老年代,新生代中还可以分为 Eden空间、from Surivor空间、To Surivor空间等。 从内存共享的角度可以划分出多个私有的分配缓冲区TLAB。 年轻代(Young Generation) 对象在被创建时,内存首先是在年轻代进行分配(注意,大对象可以直接在老年代分配)。当年轻代需要回收时会触发Minor GC(也称作Young GC)。 年轻代由Eden Space和两块相同大小的Survivor Space(又称S0和S1)构成,可通过-Xmn参数来调整新生代大小,也可由-XX:SurvivorRadio来调整Eden Space和Survivor Space大小。不同的GC方式会按不同的方式来按此值划分Eden Space和Survivor Space,有些GC方式还会根据运行状况来动态调整Eden、S0、S1的大小。 年轻代的Eden区内存是连续的,所以其分配会非常快;同样Eden区的回收也非常快(因为大部分情况下Eden区对象存活时间非常短,而Eden区采用的复制回收算法,此算法在存活对象比例很少的情况下非常高效,后面会详细介绍)。 如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java Heap Space异常。

老年代(Old Generation) 老年代用于存放在年轻代中经多次垃圾回收仍然存活的对象,可以理解为比较老一点的对象,例如缓存对象;新建的对象也有可能在老年代上直接分配内存,这主要有两种情况:一种为大对象,可以通过启动参数设置-XX:PretenureSizeThreshold=1024,表示超过多大时就不在年轻代分配,而是直接在老年代分配。此参数在年轻代采用Parallel Scavenge GC时无效,因为其会根据运行情况自己决定什么对象直接在老年代上分配内存;另一种为大的数组对象,且数组对象中无引用外部对象。 当老年代满了的时候就需要对老年代进行垃圾回收,老年代的垃圾回收称作Major GC(也称作Full GC)。 老年代所占用的内存大小为-Xmx对应的值减去-Xmn对应的值。

总结

名称特征作用配置参数异常程序计数器占用内存小,线程私有生命周期与线程相同大致为字节码行号指示器无无虚拟机栈线程私有,生命周期与线程相同,使用连续的内存空间Java方法执行的内存模型、存储局部变量表、操作栈、动态链接、方法出口等信息XssStackOverflowError OutOfMemoryErrorjava堆线程共享,生命周期与虚拟机相同可以使用不连续的内存地址保存对象实例,所有对象实例(包括数组)都要分配在堆上Xms、XmxOutOfMemoryError方法区线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据-XX:PermSize:16M-XX:MaxPermSize64MOutOfMemoryError运行时常量池方法区的一部分,具有动态性存放字面量及符号引用

但是在java8中对jv内存模型进行了修改,将其中的方法区移到non-heap中的Metaspace(元空间)

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


最新回复(0)