JMV的学习

pansoso2021-04-19  26

一.JVM学习 1.1JVM运行机制的最重要的三点:加载(类加载器,classloader) 、内存管理(包含GC)、执行。 如果再加上JDK所作的把java文件编译为二进制class文件的步骤,就组成了 Java代码的执行机制三部曲: 编译–>加载–>执行 2.1 Java编译机制 Java编译机制不属于JVM,但是JVM运行class文件,首先需要JDK把java源码编译成class文件。 JVM规范规定了class文件的格式,但并未规定java源码如何编译成class文件,以及如何执行class文件。 各厂商的JDK编译器各不相同,比如Sun JDK中的编译器是javac.exe。 Java 源码编译由以下三个过程组成: 分析和输入到符号表注解处理语义分析和生成class文件 2.1.1 分析和输入到符号表(Parse and Enter): Parse过程主要做词法分析和语法分析。 Enter过程是将符号输入到符号表。   2.1.2 注解处理(Annotation Processing): 根据注解产生一些新的代码,或进行一些特殊检查。 2.1.3 语义分析和生成class文件(Analyse and Generate): 语义分析包括声明检查、类型检查、语句到达检查、exception检查、变量赋值检查、解除语法、泛型转换等。 采用 后续遍历语法树生成class文件。 class文件包含了如下信息: 结构信息:包括格式版本号及各部分的数量与大小的信息。 元数据:主要是声明和常量的信息。 方法信息:主要是语句和表达式的信息。 2.2 类加载机制 类加载机制是指class文件加载到JVM,并形成class对象的机制。类的加载又分为三个步骤: 装载–>链接–>初始化 2.2.1 装载(Load) 装载过程负责找到二进制代码并加载到JVM中。 对于接口或非数组型的类,由ClassLoader直接加载; 对于数组型的类,数组类由JVM直接创建,而数组中的元素类型还是由ClassLoader加载。 2.2.2 链接(Link) 链接过程又分为: 校验,准备,和解析。 校验格式遵循JVM规范。 准备过程会初始化类中的静态变量,并将其值赋为默认值。(特别注意这一点:静态变量初始化是在Initialize阶段之前完成的) 最后对类中所有的属性、方法进行验证。 装载和链接过程完成后,二进制字节码就转换成了class对象。此时还未初始化。 2.2.3 初始化(Initialize) 初始化即执行类的静态初始化代码、静态属性初始化、实例初始块代码、构造方法等。(但静态初始化和实例/构造方法初始化不是同时进行的,一个是类加载完后进行,一个是实例化的时候进行) 初始化是在初次主动使用对象前执行,在以下4种情况下初始化过程会被触发执行: 1) 调用了new; 2) 反射执行newInstance()方法; 3) 子类调用了初始化; 4) JVM启动过程中指定的初始化类。 关于静态变量、静态初始化块、实例初始化块、构造方法的初始化顺序,见另一文 《Java类和实例初始化顺序》 JVM对类的加载,是通过ClassLoader及其子类来完成的,分为: Bootstrap ClassLoader, Extension ClassLoader, Application ClassLoader, Custom ClassLoader。 1) Bootstrap ClassLoader: Sun JDK采用C++实现此类,启动时会初始化此ClassLoader,并由它完成jre/lib/rt.jar包里所有class文件的加载。 如果用java代码去打印此类,只会显示null。 2) Extension ClassLoader: 用来加载扩展包。Sun JDK中,继承自java.lang.ClassLoader,全限定名为:sun.misc.Launcher.ExtClassLoader 3) Application ClassLoader: 用来加载classpath指定的jar包。Sun JDK中,继承自java.lang.ClassLoader,全限定名为:sun.misc.Launcher.ExtClassLoader (注意:ExtClassLoader和AppClassLoader是属于同一个package的,而不是加载关系中的父子关系) 4) Custom ClassLoader: 根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。 在代码中可以通过类名.class.getClassLoader()或类名.class.getClassLoader().getParent()或继续.getParent()来得到类名信息。 Sun JDK中各ClassLoader的继承关系如下图: 其实IBM的JDK中用到的ClassLoader基本都是Sun的这一套,并在此基础上再实现了一些ClassLoader类。 加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。 而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。 这就是所谓的双亲委托模式(英文名为parent delegation,其实是单亲,双亲这个词容易让人误会),这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。比如java.lang.String在系统启动的时候由Bootstrap ClassLoader加载了,所以用户就不能再试图自己写一个java.lang.String来代替原有的String类了。 命名空间: Java的命名空间其实说法并不统一。 一说Java的命名空间就是package,如同C#里的namespace。 一说Java的命名空间是由类装载器实例所装载的类组成。每个类装载器实例有自己的命名空间,命名空间由所有以此装载器实例为创始类装载器的类组成。 不管怎么说,不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。 隐式加载和显式加载 隐式加载:程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。 显式加载:通过class.forname(),class.loadClass()等方法,显式加载需要的类。 两个异常ClassNotFoundException和NoClassDefFoundError的区别: ClassNotFoundException: 当前ClassLoader加载类时未找到类文件。 NoClassDefFoundError: 主要原因是加载的类中引用到得其他类找不到。 2.3 类执行机制 前面说了,JVM规范并没有规定如何执行class文件。计算机不能理解高级语言(如Java,C),只能理解机器语言。Java编译器是把Java文件编译为二进制字节码。这是一种中间代码,计算机仍然不懂。这就需要JVM把它翻译成机器码。翻译有两种方式:解释和编译。那么在JVM执行类的时候,就有两种方式了: 解释执行和编译执行。 解释执行: 在运行程序的时候才翻译,且每次运行都需要翻译,所以效率低。 JVM采用了 invokestatic、invokevirtual、invokeinterface、invokespecial四个指令来执行不同的方法调用,基本上可以从名字看出来各自的用途,其中invokespecial是用来调用private方法和实例初始化方法。 编译执行: 将字节码编译成机器码执行。这里需要特别指出的是,Sun JVM支持在运行时编译(Just In Time, JIT编译)。Sun JVM在执行过程中对执行频率高的代码(Hotspot,热点)进行编译,对执行不频繁的代码则继续采用解释的方式,所以Sun JVM又称为Hotspot VM。 Sun JVM提供了两种编译模式: client compiler(-client)和server compiler(-server)。 client comiler: 又称为C1,轻量级,只做少量优化,占用内存少。主要优化有: 方法内联 去虚拟化 冗余削除 server compiler: 又称为C2,重量级,采用大量优化,占用内存多。逃逸分析是C2进行很多优化的基础。 主要优化有: 变量替换 栈上分配同步削除 逃逸分析(Escape Analysis):是指根据运行状况来判断方法中的便利是否会被外部读取。如会被读取,则此变量是逃逸的。比如在方法中,给外部全局变量赋值,或方法有返回值,或对象引用传递等。如果没有发生逃逸,则可以对代码进行上述优化 几种情况下: 32位windows机器始终都是client模式。 当机器配置CPU超过2核且内存超过2G,默认为server模式。 可以通过增加-client或-server来强制指定。 查看本机默认编译方式: 在命令行执行”java –version” 如: java version “1.7.0_01″ Java(TM) SE Runtime Environment (build 1.7.0_01-b08) Java HotSpot(TM) 64-Bit Server VM (build 21.1-b02, mixed mode) 最后一行显示了其JVM默认为server模式编译。mixed mode表示解释和编译混合执行,区别于单纯的解释执行和编译执行。 ================================================================== 3. JVM内存管理 3.1 内存空间划分 JVM在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,各区域有各自不同的用途和生存期。按规范,JVM所管理的内存包括以下几个运行时数据区域: 程序计数器(Program Counter Register): 程序计数器是一块比较小的内存空间,它是线程私有的内存,指向当前线程所执行的字节码的位置。 程序计数器是唯一一块不会抛出OutOfMemoryError的区域。  虚拟机栈(VM Stacks): 即栈,或称方法栈。也是线程私有的。基本操作为压栈和出栈,单位为栈帧,顺序为先进后出。 栈帧由三部分组成: 局部变量区、操作数栈、栈帧数据区。(动态链接,方法出口) 局部变量表存放了编译器可知的基本数据类型、对象引用和方法返回值等。 栈是运行时的单位。 StackOverFlowError: 当线程请求的栈深度大于虚拟机所允许的深度时,抛出StackOverFlowError。换句话说,当需要存储的数据超过了分配的栈空间时,就会抛这个错误信息。比如死循环、递归次数过多等。 OutOfMemoryError: 虚拟机栈动态扩展到无法申请到足够的内存时,就会抛OutOfMemoryError。但基本很少会碰到这个Error。 本地方法栈(Native Method Stacks): 本地方法栈是为虚拟机使用的本地方法服务的。而虚拟机栈则是为虚拟机执行Java方法服务的。两者很相似,HotSpot虚拟机是直接把两者合二为一的。 当然也是线程私有的。  堆(Heap): 一个JVM只有一个堆,所有线程共享。堆用来存放类实例和数组。这块区域是GC管理的主要区域。 堆是存储的单位。 当堆中没有足够内存来分配对象实例,且堆无法再扩展时,就会OOM。 方法区(Method Area): 又称为非堆(Non-Heap),也是所有线程共享的。 用于存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的常量和符号引用。 HotSpot虚拟机使用Perm代(永久代)来实现方法区,所以在一定概念和时间范围内可以相互通用。但以后HotSpot也将采用Native Memory来实现方法区。 3.2 对象访问 我们知道,一个对象的引用reference是分配在栈空间的局部变量表里的,指向对象的引用。但JVM规范并没有规定对象的访问方式。主流的访问方式有两种:使用句柄和直接访问。 使用句柄:Java堆中划分一块区域作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。如图: 直接访问: reference中直接存储堆中的对象地址。如图: 使用句柄的好处:当发生GC时,对象经常会发生移动,而reference中存储的句柄地址就不需要发生变化,变化的只是句柄中的对象指针。 直接访问的好处:速度! HotSpot采用直接访问的方式。 3.3 GC 3.3.1 垃圾回收算法: 引用计数(Reference Counting) 标记-清除(Mark-Sweep) 复制(Copying) 标记-整理(Mark-Compact) 增量收集(Incremental Collecting) 分代(Generational Collecting): 3.3.2 垃圾收集器: HotSpot JVM有如下几种垃圾收集器,这里不展开了: Serial收集器 ParNew收集器 Parallel Scavenge收集器 Serial Old收集器 Scavenge Old收集器 CMS收集器 G1收集器  3.3.3 分代回收: 不同的对象的生命周期是不一样的,因此对不同生命周期的对象,可以采取不同的回收方式,以提高回收效率。所以分代收集算法是以其他算法为基础的。 在HotSpot JVM中将分配到的内存堆(Heap)分为两个物理区域,一个是年轻代(Young Generation),一个是年老代(Tenured Generation),另外再加上持久代(Permanent Generation),就组成了分代回收GC中的三个内存区域。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。 Young(年轻代): 一般来说年轻代又分为一个Eden区和两个Survivor区。(也可分多个Survivor区) Survivor区无先后顺序,彼此对等。大部分对象在Eden区生成。 当Eden区满的时候,存活的对象就被复制到其中一个Survivor区中,我们权且称其为主Survivor区; 当此Survivor区的数据又满的时候,其中的对象就被复制到另一个Survivor区,原Survivor区被清空。那么现在,第二个Survivor区就成了主区,所以它同时又接收Eden区复制转移过来的对象。 当此Survivor区再次满的时候,复制转移了两次还存活的对象,就被复制到年老代中去了。(转移几次才复制到老年代中,可通过参数配置) Tenured(老年代):一般来说,年老代存放的都是存活期较长的对象。 Permanent(持久代): 用于存放静态数据,如类、方法等。注意,持久代仅仅是HotSpot虚拟机目前对方法区的实现方式,其他虚拟机是没有持久代的,所以持久代不等同于方法区。 分代垃圾回收分为两种:Scaverage GC和Full GC Scaverage GC: 当新产生对象,写入Eden区,而Eden区满了的时候,就会触发Scaverage GC。这时GC就会清理Eden区,清除非存活对象,把存活的对象写入Survivor区, 同时清理两个Survivor区。 Eden区内存分配不大,且大部分对象都是在Eden区产生的,所以Scaverage GC会比较频繁。 Full GC: Full GC会对整个堆进行整理,包括年轻代、年老代、持久代。Full GC会消耗很多内存,频繁的Full GC更会严重影响性能,所以要避免频繁的Full GC。会导致Full GC的可能原因有: Tenured Generation写满了。Perm Generation写满了。显式调用System.gc() 3.3.4 常见配置汇总 堆设置 -Xms:初始堆大小-Xmx:最大堆大小-XX:NewSize=n:设置年轻代大小-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5-XX:MaxPermSize=n:设置持久代大小 收集器设置 -XX:+UseSerialGC:设置串行收集器-XX:+UseParallelGC:设置并行收集器-XX:+UseParalledlOldGC:设置并行年老代收集器-XX:+UseConcMarkSweepGC:设置并发收集器 垃圾回收统计信息 -XX:+PrintGC-XX:+Printetails-XX:+PrintGCTimeStamps-Xloggc:filename 并行收集器设置 -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 并发收集器设置 -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

转载于:https://www.cnblogs.com/llaq/p/9451777.html

相关资源:JAVA上百实例源码以及开源项目