JVM相关知识总结

JVM概念
  • JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
  • JDK中包含JVM和底层屏蔽操作系统差异的组件,不同系统的中JVM是相同的,但是底层屏蔽操作系统差异的组件是不相同的,所以要下在不同版本的JDK。
类的生命周期
  • 声明周期:加载–>连接–>初始化–>使用–>卸载

  • 加载:在硬盘中查找类的二进制文件(class文件),并加载进JVM中。

  • 连接:确定类与类之间的关系,例如:Student stu = new Student(address)中Student和address类之间的关联关系,并且连接中包含三个阶段,分别为验证、准备、解析:

    验证:验证class文件的正确性,或者是否存在修改;
    准备:为静态变量分配内存,并且赋初始默认值,在准备阶段只有类,没有对象;
    解析:在解析阶段,JVM会使用符号引用,类似“indi.dsl.entry.Studnet”的形式代替“Student”,最后将“indi.dsl.entry.Studnet”映射成为实际的内存地址来代替“Student”。

  • 初始化:将各个静态变量赋予真正的值,例如:默认值int = 0;变为 int = 10;

  • 使用:对象的初始化、对象的垃圾回收、对象的销毁;

  • 卸载:被自定义加载器加载的对象清除出内存区域。

JVM结束生命周期的时机
  • 正常结束
  • 异常结束/错误
  • System.exit():操作系统推出
  • 操作系统异常
针对于JVM运行时内存区域第一种划分方式—JVM内存模型(Java Memory Model,简称JMM)
  • 不同线程之间数据交互时经历步骤,如图所示:
    在这里插入图片描述
  • 特别注意:

    1.各个线程只能访问自己私有的工作内存,不能够访问其他的工作内存和主内存;
    2.各个线程可以通过主内存间接的访问其他线程的工作内存。

  • JVM要求以上图片中的8个动作必须是原子性的,但是对于64位的数据类型(long或者double)会产生非原子性问题,即读取该数据类型中的数据一半的数据,造成数据读取异常,有一下两种方式进行避免:

1.商用JVM已经充分考虑了此问题,正常使用无需特殊操作;
2.使用Volatile关键字解决JVM只读取半个数据的问题,例如:volatile long num;的方式去定义变量。

Volatile关键字
  • 概念:JVM提供的一个轻量级的同步机制

  • 作用:

    1.放置JVM对long/double等64位的非原子性数据的误操作(只读取半个数据)-------不保证原子性
    2.如果某个变量定义时添加volatile关键字,可以使该变量对所有线程立即可见,如果某一个添加voltile关键字的变量修改了工作内存中某个变量副本中的值,那么该变量的值会立刻同步到其他线程中的该变量的值。---------保证可见性
    3.禁止指令的“重排序”优化。-------禁止指令重排

  • 原子性和线程安全性

    1…volatile关键字是不能保证原子性,并且不是线程安全的。
    2.如果要原子性和线程安全的,可以使用原子包java.util.cocurrent.aotmic中的类,能够保证原子类的核心为该类中提供compareAndSet()方法,该方法提供了cas算法(无锁算法)

重排序
  • 概念:排序的对象就是原子性操作,目的是为提高执行效率,是一种JVM对代码的一种优化方式。
  • 对于单线程的重排序,是不会影响代码的执行结果。
JVM运行时内存区域第二种划分方式
线程私用区
  • 划分图解:
    在这里插入图片描述
    在这里插入图片描述

  • 程序计数器

  • 概念:行号指示器,用于指向当前线程所执行字节码指令的地址,简单理解为class文件的行号。

    • 注意:

      1.一般情况下程序计数器是class文件的行号,但是如果程序中正在执行的方法是native方法(系统方法),则程序计数器的值为undefined。
      2.程序计数器是唯一一个不能产生“内存溢出的”(Out Of Memory–OOM)的区域。

  • 虚拟机栈

    • 概念:描述JDK中或者自行编写的方法执行的内存模型。

    • 栈帧:每个方法在虚拟机栈中为一个栈帧,栈帧中包含:局部变量表、操作数据栈、动态链接、方法出口。
      在这里插入图片描述

    • 局部变量表:其中存放着方法中的局部变量,可以是简单类型也可以是复杂类型(对象),例如:int num = 10;

    • 操作数据栈:其中存放在局部变量表中的所有的值,例如:int num = 10;中的10就会存放在操作数据栈中。

    • 动态链接:符号引用(例如:indi.dsl.entry)会在实际代码执行时(多态),转化为不同地址;因此,转换行为会在每一次运行时都会发生一次。类似于如下代码:

      Person per=null;
      if(per instanceof Worker){
         Person per = new Worker();
      }else{
        Person per = new Student();
      }
      
    • 方法出口:本次方法执行完成后,指向下一步所要进行的信息。

  • 本地方法栈

    • 原理与结构与虚拟机栈一致,不同点:虚拟机栈中存放着JDK或者自己 编写的方法,而本地方法栈调用的是操作系统底层的方法。
线程共享区
  • 结构图:
    在这里插入图片描述

  • 堆(Heap):

    1.堆中存放为对象实例(数组、对象);
    2.堆是内存区域中最大的一块区域,在JVM启动时就已经创建完成;
    3.GC主要管理堆区域;
    4.堆本身是线程共享的,但是堆的内部可以划分出多个线程私有的缓冲区;
    5.堆允许物理空间不连续,只要逻辑连续即可;
    6.堆可以分为新生代、老生代两大区域,并且大小比为:新生代:老生代=1:2
    7.新生代中包含:eden、S0(From Survivor)、S1(To Survivor),并且大小比为:eden:s0:s1=8:1:1;
    8.新生代的使用率一般在90%,在新生代的使用中eden只能和一块S区域(S0\S1)一起使用,原因时底层采用“复制算法”,为了避免存储空间产生碎片。
    9.新生代存放生命周期较短的对象或者是小对象,老生代存放生命周期长的对象或者大的对象(集合、数组、字符串),新生代的对象声明周期可以通过虚拟机参数:-XX: PretenureSizeThreShold;
    10.从新生代到老生代的年龄机制:MinorGC是新生代中的垃圾回收器,如果Eden中的对象在一次回收中存活,就会被转移到S区域中;之后MinorGC再次回收时,已经在S区域中的对象如果依然存活,则年龄+1处理,直到年龄达到一定数值时,对象会被转移到老生代区。简言之,在新生代中的对象,在每一次MinorGC时,有三种可能出现:①从Eden区域–>S区、②已经在S区中的对象年龄+1、③转移到老生代中(可以从Eden区域中转入或者从S区域中转入)。
    11.老生代的对象声明周期可以使用虚拟机参数:-XX:MaxTenusingThreshold,老生代的垃圾回收器为MajorGC或者FullGC(也会回收全部堆空间)。

    • 新生代特点:

      1.大部分对象存在与新生代;
      2.新生代的对象回收频率高、效率高。

    • 老生代特点:

      1.空间大
      2.增长速度快
      3.频率低

  • 方法区:

    • 存放:类的元数据(描述类的信息)、常量池(常量)、静态成员变量(静态全局变量)、方法信息(方法数据、方法代码),其中常量池为编译期产生的字面量(例如:“abc”)、符号引用。
    • GC只会回收方法区域中的元数据和常量池
    • 方法区域中的数据如果太多,也会出现OutOfMemory(内存溢出)
  • 执行解析图:
    在这里插入图片描述

  • 特别注意:产生内存溢出的异常,除了虚拟机中的4个区域以外,还存在“直接内存”,直接内存为系统内存,即本身电脑的系统内存溢出。

类的使用方式–类的初始化
  • 类的初始化:JVM只会在“首次主动使用”一个类或者接口时,才会初始化类。
主动使用
  • 使用new时:即使用new来构造类时
  • 访问类或者接口的静态成员(属性、方法)时也会初始化类
  • 使用反射Class.forName(“xxx.xx”)方法执行时使用的类会被初始化
  • 初始化子类时,父类也会被初始化
被动使用
  • main()方法也是静态方法,在main方法被执行时,main方法所在的类也会被初始化
  • 如果成员变量既是static,又是final,即为常量,则成员变量被调用时,成员变量所在类不会被初始化
  • 如果成员变量既是static,又是final并且该成员变量的值为随机数,则成员变量被调用时,成员变量所在类会被初始化
四种对象的引用方式
  • 四种对象的引用方式:强引用、软引用、弱引用、虚引用,并且从左至右越容易被回收。
  • 强引用(例如:Person per = new Person();)只有在对象的生命周期结束或者被置为null时(Person per =null),才会被GC回收,除了以上两种情况外,任何时候GC都不会回收强引用。
  • 软引用:利用Refence类的子类SoftRefence新建立的对象成为软引用,根据JVM内存情况,如果内存重组则GC不会随便回收软引用;如果JVM内存情况不足,GC则会主动回收软引用对象。
  • 弱引用:利用Refence类的子类WeakRefence新建立的对象成为软引用,只要GC执行,就会将弱引用对象回收。
  • 虚引用(幻影引用或者幽灵引用):使用java.lang.ref.PhontomRefence来创建虚引用对象,是否使用虚引用,和引用对象无关,无法通过虚引用来获取对象本身;虚引用不能够单独使用,回合引用队列一起使用。因为虚引用的执行机制是:GC–>如果有虚引用–>虚引用入队–>虚引用出队–>回收对象,所以可以在对象回收之前增加其他操作
双亲委派
  • 双亲委派概念:双亲是指JVM自带的类加载器(在JVM的内部所包含,C++)和用户自定义类的加载器(独立于JVM之外的类加载器),二者称为双亲,委派是指二者对类的加载过程。

  • JVM自带的加载器

    1.根加载器–BootStrap:加载jre\lib\rt.jar(包含编写的代码中大部分JDK中的API),也可以让根加载器指定加载某个jar(通过设置JVM参数:-Xbootclasspath=a.jar)
    2.扩展类加载器–Extension:jre\lib\ext/*.jar;也可以让根加载器指定加载某个jar(通过设置JVM参数:-Djava.ext.dirs=xxx.jar)
    3.应用加载器或者系统加载器–App/System:加载classpath下的类;也可以指定加载某个jar或者某个类(通过设置JVM参数:-Djava.class.path=xxx.jar/xxx类)

  • 用户自定义加载器

    都是抽象类java.lang.ClassLoader的子类

  • 类加载过程
    在这里插入图片描述

  • 双亲委派过程:当一个加载器加载类的时候,自己先不加载,而是逐层向上交由双亲去加载,如果双亲中存在某一个合适的加载器加载成功,会向下回馈成功信息,如果所有双亲都无法加载,则会报出类加载异常。

  • 小结:

    1.如果类是rt.jar中的,则该类会被bootstrap(根加载器)加载,如果是classpath中的类(自家编写的类),则该类会被AppClassLoader(系统加载器或者叫应用加载器)加载。
    2.定义类加载:最终加载类的加载器;
    3.初始化类加载器:直接面对加载任务的类,即第一次直接面对的类的加载器类。

-双亲委派机制优势:可以有效防止用户自定义类和rt.jar中的类重名,而造成的混乱。
如果自己编写的类与rt.jar中的类重名,控制台会报错如下图所示:
在这里插入图片描述

  • 报错原因:根据双亲委派,越上层的加载器越优先执行,最顶层的加载器是根加载器,根加载器会加载rt.jar中的类,因此rt.jar中的类中类的方法会被优先加载,即最终加载的不是自己编写的类中的同名方法,而在加载rt.jar中的类时没有自行编写的main()方法,因此会报错无法找到main()方法。
类的卸载
  • 系统自带(根加载器、扩展加载器、系统加载器),这些加载器的类是不会被卸载的。
  • 用户自定义的加载器的类,会被GC卸载的。
GC调优
  • 调优实际上是一种取舍,通常是以XX换取XX的策略。因此在调优之前必须明确调优的方向:低延迟?OR 高吞吐量?

  • 有两种方式需要考虑:

    1.在已知条件相同的情况下,牺牲低延迟 来换取高吞吐量,还是反之处理。
    2.随着软硬件的技术提升,可能二者都会提升。

  • 目前在已知现有的条件下,可以尝试调大新生代的空间,或者调大新生代到老生代的年龄阈值,从而降低短生命周期对象从新生代转移到老生代的概率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值