JavaEE核心【JVM 的作用、类加载器、JVM内存模型、GC回收机制】

1、JVM的作用

        JVM是Java Virtual Machine的缩写。我们安装的JDK中包含了JRE,在JRE中,包含了java的虚拟机和核心类库,如果想要运行java程序,则需要上述的JRE环境。

        java是一门高级程序语言,直接运行在硬件上并不现实,所以要在运行之前,需要对其进行一些转换。

转换过程:通过编译器将java程序转换成虚拟机能识别的指令序列,也叫做java字节码。java虚拟机会将字节码文件(.class文件)加载到JVM中,由JVM进行解释和执行。

        JVM运行在操作系统之上,与硬件没有直接的交互。

JavaEE核心【JVM 的作用、类加载器、JVM内存模型、GC回收机制】_第1张图片

 2、类加载器

        类加载器(ClassLoad),负责加载class文件,经过类加载器加载并初始化之后,会得到Class,实例化对象的时候会参考Class。如Person.class经过ClassLoad加载初始化后,会得到Person Class,之后每次实例化对象的时候,都会参考Person Class。

        类加载器的分类

        启动类加载器(Bootstrap):主要负责加载jre中的最为基础、最为重要的类。如$JAVA_HOME/jre/lib/rt.jar等。它由C++代码实现,没有对应的java对象,因此在java中,尝试获取此类时,只能使用null来指代。

        扩展类加载器(Extension):由Java代码实现,用于加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类。

        应用程序类加载器(AppClassLoad):由Java代码实现, 它负责加载应用程序路径下的类。比如Person.java,Car.java等程序员自定义的类。

        用户自定义加载器。

        其中Bootstrap是Extension的父类加载器,Extension是AppClassLoad的父类加载器。

        双亲委派机制

        每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器(在此加载器中如果还有父类加载器,依旧向上请求)。在父类加载器没有找到所请求的类的情况下,该类加载器才会向下尝试去加载。在AppClassLoad中仍然没有找到所请求的类,会抛出ClassNotFound异常。当子类和父类同时有相同的类时,由双亲委派机制决定,会优先使用父类的。

        好处:

        通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次

         防止java核心api中定义类型不会被用户恶意替换和篡改,从而引发错误。

3、JVM内存模型

JavaEE核心【JVM 的作用、类加载器、JVM内存模型、GC回收机制】_第2张图片

        (1)Execution Engine:执行引擎(Execution Engine)负责解释命令,提交操作系统执行。

                当JVM需要调用系统的硬件时,如CPU,硬盘等,需要向操作系统发送命令,但命令操作系统无法理解,这时就需要Execution Engine负责将命令解释并提交给操作系统。

        (2)Native Method Stack:本地方法栈( Native Method Stack)在每个操作系统内部,都定义了很多本地方法库。这些本地方法库中,定义了很多调用本地操作系统的方法,也称之为本地方法接口。

                当需要执行Native方法时,需要将Native方法压入Native Method Stack,然后向操作系统发送指令,交给执行引擎(Execution Engine) 解释命令,之后调用本地方法接口(Native Interface),调用本地方法接口时又会使用到本地方法库。

        (3)Program Counter Register:程序计数器,也叫PC寄存器,是一个小指针,提示下一个需要执行栈内的哪一个方法。

        (4)Method Area:方法区(Method Area),方法区是被所有线程共享,所有定义的方法的信息都保存在该区域,此区属于共享区间。

                静态变量 + 常量 + 类信息(构造方法/接口定义) + 运行时常量池存在方法区中。

        (5)Stark:栈,栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就结束,生命周期和线程一致,是线程私有的。

                8种基本类型的变量(byte,short,int,double,float,long,char,bool)+ 对象的引用变量 + 实例方法都是在栈内存中分配。

                在栈区域规定了两种异常状态:如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

                栈帧:一个线程的每个方法在调用时都会在栈上划分一块区域,用于存储方法所需要的变量等信息,这块区域称之为栈帧(stack frame)。栈由多个栈帧构成,好比一部电影由多个帧的画面构成。

                栈运行原理:栈中的数据都是以栈帧(Stack Frame)为载体存在。在栈中,方法的调用顺序遵循“先进后出”/“后进先出原则。

        (6)堆heap

        堆的逻辑设计:堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆内存的大小是可以调节的(通过 -Xmx 和 -Xms 控制),一般为物理内存的1 / 64 ,最大为物理内存的1 / 4。在逻辑上可以划分为三部分,新生区(Young Generation Space)、养老区(Tenure generation space)、永久区(Permanent Space),永久区在JDK1.8后为元空间。

JavaEE核心【JVM 的作用、类加载器、JVM内存模型、GC回收机制】_第3张图片

        堆的物理设计:在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。 

JavaEE核心【JVM 的作用、类加载器、JVM内存模型、GC回收机制】_第4张图片

                 新生代分为eden区、from和to区,他们是两块大小相等并且可以互换角色的空间(当进行轻量级垃圾回收后仍然存在对象的区称为from区)。绝大多数情况下,新new出来的对象首先分配在eden区,在新生代回收后,如果对象还存活(被引用),则进入from或to区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄(默认15)后,则进入老年代,否则进入to区。在新生代进行垃圾回收使用的是轻量级垃圾回收(Minor GC),在老年代中进行的是重量级的垃圾回收(Major GC),当在老年代中无法进行垃圾回收,会触发OOM(OutOfMemory)。

        永久区perm:永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会轻易被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

        Jdk1.6及之前: 有永久代, 字符串常量池1.6在方法区

        Jdk1.7:            有永久代,但已经逐步“去永久代”,字符串常量池1.7在堆

        Jdk1.8及之后: 无永久代,用元空间替代

4、GC

        JVM中的Garbage Collection,简称GC,它会不定时去堆内存中清理不可达(没有被引用)对象。

        GC分类:                

                JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)

          新生代GC(minor GC):只针对新生代区域的GC。

              老年代GC(major GC or Full GC):针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC。

             Minor GC触发机制:当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden区满,Survivor满不会引发GC。

               Full GC触发机制:当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代,当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。

        GC工作特点:理论上GC过程中会频繁收集Young区,很少收集Old区,基本不动Perm区(元空间/方法区)。

        标记不可达对象的引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到循环指向的存在。主流的Java虚拟机里面都没有选用引用计数算法来管理内存。

        标记不可达对象的可达性分析(GC Roots算法):根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

        垃圾回收的三种方式: 

                清除:把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。清除这种回收方式的原理及其简单,但是有两个缺点。一是会造成内存碎片。由于 Java 虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。另一个则是分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。

                压缩:把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。

                复制:把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。复制必交换,谁空谁为to。

                总结:回收死亡对象的内存共有三种方式,分别会造成内存碎片的清除性能开销较大的压缩以及堆使用效率较低的复制。当然,现代的垃圾回收器往往会综合上述几种回收方式,综合它们优点的同时规避它们的缺点。

        垃圾回收四大算法:

                标记复制(Mark-Copying)算法:

                当我们调用 new 指令时,它会在 Eden 区中划出一块作为存储对象的内存。当 Eden 区的空间耗尽了, Java 虚拟机便会触发一次 Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到 Survivor 区。

                新生代共有两个 Survivor 区,我们分别用 from 和 to 来指代。其中 to 指向的 Survivior 区是空的。当发生 Minor GC 时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中,然后交换 from 和 to 指针,以保证下一次 Minor GC 时,to 指向的 Survivor 区还是空的。

                Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。另外,如果单个 Survivor 区已经被占用了 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。

                万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。

                因此Minor GC使用的则是标记-复制算法。将 Survivor 区中的老存活对象晋升到老年代,然后将剩下的存活对象和 Eden 区的存活对象复制到另一个 Survivor 区中。理想情况下,Eden 区中的对象基本都死亡了,那么需要复制的数据将非常少,因此采用这种标记 - 复制算法的效果极好。

                标记清除(Mark-Sweep)算法:                

                老年代一般是由标记清除或者是标记清除与标记整理的混合实现。标记清除算法一般应用于老年代,因为老年代的对象生命周期比较长。该算法先对所有可访问的对象,做个标记再遍历堆,把未被标记的对象回收(标记活的)。                

                缺点:回收时,应用需要挂起,也就是stop the world,导致用户体验极差。由于需要遍历全堆对象,效率比较低(递归与全堆对象遍历)。造成内存碎片化。

                标记压缩(Mark--Compact)算法:

                标记清除算法和标记压缩算法非常相同,但是标记压缩算法在标记清除算法之上解决内存碎片化,也消除了复制算法当中,内存减半的高额代价。但效率低,压缩阶段,由于移动了可用对象,需要去更新引用。

                标记清除压缩(Mark-Sweep-Compact)算法:

                标记清除压缩(Mark-Sweep-Compact)算法是标记清除算法和标记压缩算法的结合算法。其原理和标记清除算法一致,只不过会在多次GC后,进行一次Compact操作!

你可能感兴趣的:(JavaEE核心,java-ee,java,开发语言)