JVM浅谈

做为java开发者,平时工作中打交道最多的就是JVM了,JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

市面上常见的主流JVM虚拟机有HotSpot VM、J9 VM、JRockit VM(JDK8中已与HotSpot合并)、Zing VM等,本文内容主要基于HotSpot VM。

JAVA类加载过程

类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。其中准备、验证、解析三个部分统称为连接。

加载

查找并加载类的二进制数据按照虚拟机所需的格式存储在方法区之中。

  • 通过一个类的全限定名来获取其定义的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个代表这个类的 java.lang.Class对象,作为对方法区中这些数据的访问入口。

验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响。如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

正式为类变量(static成员变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

  • 内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  • 初始值通常情况下是数据类型的默认值(如0、0L、false等),不是被在Java代码中被显式地赋予的值。如public static int a = 1 在准备阶段过后的值为0而不是1。赋值为1的动作将在初始化阶段才会执行。
  • 如果类字段为常量类字段,那么在准备阶段变量值会被初始化为指定的常量值。如public static final int a = 1 在准备阶段过后的值就是1。

解析

解析阶段是把常量池内的符号引用替换成直接引用的过程。包括对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行解析。

  • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。
  • 直接引用(Direct References):直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,有了直接引用,那么它一定已经存在于内存中了。

初始化

初始化阶段,才真正开始执行类中定义的java程序代码(字节码),为类的静态变量赋予正确的初始值。
对类变量进行初始值设定的方式:

  • 声明类变量时指定初始值
  • 使用静态代码块为类变量指定初始值

静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

public class Test{
 static{
     a = 0;
    System.out.println(a); // IDE将提示 Illegal forward reference
 }
 static int a = 1;
} 

初始化步骤:

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

JVM内存结构

20201119222821.jpg
  • 程序计数器: 一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
  • jvm栈:即线程栈(每个线程都有自己的栈),栈帧为加载的每个方法(入栈操作即方法入栈)或for循环等;栈用来保存基本数据类型的值,方法内对象的引用(指针),方法参数引用,常量池引用等。
  • jvm本地方法栈: 针对Native方法的栈。(HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一)
  • jvm元数据空间:存储class的元数据信息,使用的是Direct Memory,即本地内存,默认情况下,大小只受可用的本地内存限制。
  • jvm 堆:
    • 新生代:1个Eden区和2个Survivor区(分别叫From和To)。
      • Eden: 保持着新创建的对象
      • From:上一次Young GC后的To,保存着每次Young GC后存活的对象,每次GC后对象的年龄会加1,到一定阈值会回直接放到老年代,没有到达阈值的放到To Survivor中。
      • To: To Survivor被填满后会将对象一次性移动到老年代中,然后To和From Survivor区相互交换。
    • 老年代: 存放的都是一些生命周期较长的对象。

JVM垃圾回收算法

所有的垃圾收集算法都面临同一个问题,那就是找出应用程序不可到达的内存块,将其释放,这里面得不可到达主要是指应用程序已经没有内存块的引用了,而在JAVA中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在所有线程栈的对象的引用)引用或者对象被另一个可到达的对象引用。

  • 复制算法
    此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
  • 标记清除
    此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
  • 标记整理
    此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

jvm中gc算法 年轻代:复制算法; 老年代:标记清除,标记整理。

java对象内存申请:

Java堆是被所有线程共享的一块内存区域,主要用于存放对象实例,在堆上为对象分配内存就是把一块大小确定的内存从堆内存中划分出来,将对象放进去。

  • 指针碰撞法:已分配的内存和空闲内存分别在不同的一侧,通过一个指针指向分界点,当需要分配内存时,把指针往空闲的一端移动与对象大小相等的距离即可,对于堆空间是连续的来说可以直接偏移指针即可,一般来说采用标记整理算法的堆空间是连续的。
  • 空闲列表法:对于堆空间已分配的内存和空闲内存相互交错不连续时需要由jvm来维护一个“空闲列表”用来记录那些区域内存是空闲的。

对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性,该种方式在并发较高时对CPU时间片占用较多一般不会使用;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。

JVM常用启动参数及含义

-server:指定jvm以server模式运行;Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。
-Xms:初始堆大小
-Xmx:最大堆大小
-Xmn:设置年轻代大小,不能超过-Xmx
-Xss:设置线程栈大小,默认为1024KB,对于高并发应用且线程中存放的局部变量信息不多可降低该值以提高有更多内存来开辟新的线程。
-XX:NewRatio: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio: 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxMetaspaceSize: 设置最大元数据空间。
-XX:MaxMetaspaceSize: 设置初始元数据空间。
-XX:+UseConcMarkSweepGC :老年代使用CMS收集器
-XX:CMSInitiatingOccupancyFraction:CMS垃圾收集器,当老年代内存使用达到指定配置比例时,触发CMS垃圾回收
-XX:+UseCMSInitiatingOccupancyOnly: 指定HotSpot-VM总是使用-XX:CMSInitiatingOccupancyFraction的值作为old的空间使用率限制来启动CMS垃圾回收。如果没有使用-XX:+UseCMSInitiatingOccupancyOnly,那么HotSpot-VM只是利用这个值来启动第一次CMS垃圾回收,后面都是使用HotSpot-VM自动计算出来的值。

JVM进程正常退出的条件

线程类型

  • 用户线程:普通线程又可以称为用户线程
  • 后台线程:执行setDaemon(true)的线程,在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。

当不存在用户线程的时候,JVM进程就会退出。

可能出现FullGC的情况

  • 老年代被写满
  • 元数据空间超过设置 -XX:MaxMetaspaceSize时
  • System.gc()被显示调用
  • 上一次GC之后Heap的各域分配策略动态变化

Minor、Young、Major GC的区别

73230d3cdceb46300d51f8aaf5f262d3eca.jpg
  • Minor GC发生在Eden区
  • Young GC发生在Eden、Survivor0、Survivor1区
  • Major GC发生在Old区

你可能感兴趣的:(JVM浅谈)