JVM与操作系统的关系

image-20220813203305453

以HelloWord.java程序为例,简单介绍一下java程序的执行过程

  • HelloWord.java通过javac编译成字节码文件HelloWorld.class
  • 通过类加载器ClassLoader将字节码文件加载到运行时数据区(这个会在后面介绍)
  • 通过执行引擎执行并与操作系统交互

image-20220813203540635

关于Java SE体系结构

  • JVM只是一个翻译工具
  • JRE提供了基础类库
  • JDK提供了工具

image-20220813204206894

JVM

运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • Java堆
  • 方法区(运行时常量池)

线程私有的:虚拟机栈、本地方法栈、程序计数器

  • 线程共享的:堆、方法区

image-20220813204329860

程序计数器和栈

  • 程序计数器是JVM内存区域中 唯一不会OOM的内存区

  • 局部变量表只能存储8大数据类型(

    byte[1]、short[2]、int[4]、long[8]、float[4]、double[8]、char[2]、boolean[1])+引用

  • 一个线程有多个方法

  • 一个方法一个栈帧

  • 操作数栈:存放方法的执行和操作。

  • 下面通过一个简单的1+2=3的计算过程解释局部变量表与操作数栈之间的关系

    1. 操作数栈将局部变量表中的1、2拷贝入操作数栈
    2. 操作数栈弹出1、2并计算得3压入操作数栈
    3. 之后又压入局部变量表

    录制_2022_08_14_12_11_14_245

  • 关于动态链接的理解:

    口述不清,咱们以实例来理解

    下图中Man与Woman为Person类的子类都有wc的实现方法

    引用首先指向Man,执行wc方法

    当引用指向新的对象调用wc方法,但是这俩wc并不一样,系统却能调用正确的方法,这就是动态链接的作用。

    image-20220813211001482

  • 本地方法栈

    • 本地方法栈保存的是native方法的信息
    • 当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机中创建栈帧,JVM只是简单地调用动态链接库并直接调用native方法。

方法区和堆

首先解决一个问题,为什么堆和方法区都是线程共享的,但却分成两份?

​ 1. 堆中存放的是数组和对象,需要频繁回收。

​ 2. 方法区中存放的有类信息、常量、静态变量、即时编译器编译后的代码

深入辨析堆和栈

  • 功能
    • 以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
    • 而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
  • 线程独享还是共享
    • 栈内存归属于单个线程,每个线程都有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
    • 堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
  • 存储空间
    • 栈的内存要远远小于堆内存,栈的深度是有限制的,可能发生StackOverFlowError问题。

堆的内存模型

  • 堆的内存空间分为Eden(新生代)、From区和To区(也可划分到新生代)、Tenured(老年代)
  • 对象被分配到的是连续的内存空间,也就是说上述的几个区域都是连续的内存空间。

这里先浅谈一下堆的内存模型,后面在讲解对象的分配存储时将深入。

虚拟机优化技术

  • 编译优化技术

    • 方法内联

      比如有的场景,可以用一个表达式代替一个Boolean判断方法。

  • 栈帧之间的数据共享

    image-20220814103626937

对象的创建

对象的创建过程(一般对象)

image-20220814103830376

分配内存

内存划分引发的并发编程问题(指针碰撞) 解决方案:

  • CAS加锁

  • 本地线程分配缓冲TLAB(Thread Local Allocation Buffer):

    给每个线程分配一块区域(一般在Eden区)

    image-20220814104057751

内存空间初始化

先初始化默认值(不需要赋值就可以使用,当然程序可以越早使用对象那么效率越高)

设置

设置对象头信息

image-20220814105348748

对象的初始化

  • 执行构造方法

关于对象的访问定位

image-20220814105534396

  • 使用句柄
    • 引用reference不再存对象地址,而是存对象所对应的指针(句柄),句柄池再映射一次。
    • 句柄池具有指针定位的开销,开销比较小。
  • 直接指针
    • 直接指向真实的地址(Hotspot以及一些主流的虚拟机就使用这种)

垃圾回收机制(GC)

GC是不会主动触发的,内存不足时触发。

引用计数法

引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

可达性分析算法

可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在Java中,可作为GCRoot的对象包括:

  1. 方法区:类静态属性的对象;

  2. 方法区:常量的对象;

  3. 虚拟机栈(本地变量表)中的对象;

  4. 本地方法栈JNI(Native方法)中的对象。

使用可达性分析算法,对象一定会被GC吗?

答案是否定的,那就要涉及到finalize这个Object万类之主中的方法

finalize与try catch finally

  • finalize

    • finalize()只能执行一次,且优先级较低

    • finalize()不一定能救活对象

    • 所以使用try finally救活对象更好。

  • try catch finally

    • 不管有没有出现异常,finally块中的代码一定会执行
    • 当try和catch中有return时,finally仍然会执行
    • finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值只在finally执行之前确定的
    • finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中包含的返回值。

四种引用

  • 强引用:就像我们平时使用的“=”

  • 软引用(SoftReference):

    当OOM时被回收

    • 软引用测试:

    1654658511709

    第一次测试结果:

    1654658638965

    第二次测试结果:

    1654658686941

    • 软引用的实际使用:图片加载
  • 弱引用(WeakReference):

    只要GC就被回收

    1654659017602

    1654659061704

    • 实际应用:一些不是很重要的东西,比如缓存。可以用来防止内存泄漏
    • 实际过程中弱引用使用的更多,因为发生GC的频次肯定比OOM的频次高
  • 虚引用(PhantomReference):

    • 最弱的:随时会被回收,GC时会收到一个通知

      1654659537857

      1654659543594

    • 实际应用:监控垃圾回收器是否正常工作

对象分配策略

image-20220814111523597

  • 几乎所有的对象都在堆中分配

    • 所以不是所有的对象都是在堆中分配,未发生逃逸的对象内存其实是分配在栈上
    • 首先会判断是否在栈上分配
  • 栈上分配 -> 虚拟机栈

    • 栈中运行完了,不需要垃圾回收,栈是跟随线程存在。
  • 逃逸分析

    1654660876349

  • 堆中的优化技术

    • 本地线程缓冲(TLAB)
  • 如果是大对象(很长的字符串、数组)

    • 放入到老年代(Tenured)中
  • 长期存活的对象进入老年代

    • 首先进入From区,然后接下来的GC中在From和To区移动(复制回收算法,只要把复制的对象传递过去,就可以对内存区域直接处理,效率高)
    • age最大15,超过15将进入老年代,age也可以自定义
    • From区与To区等大
    • 大数据显示,大部分的新生对象中90%以上会在第一次GC被回收,剩下的才进入From区,所以可以让我们的空间利用率达到90%以上(并且空间比例Eden:From:To=8:1:1)。
  • 空间分配担保

    • From区与To区空间不够用,进入老年代(Tenured区)

    • 由JVM做担保,不用fullGC(GC在堆中分为minorGC->Eden、From、To和fullGC->Tenured),空间够用

    • 如果担保失败(空间不够用),那么首先需要fullGC,然后再存储对象到老年代

    • 如此处理,就不用再每次向老年代存储对象时都fullGC

分代收集理论

  1. 绝大多数对象都是“朝生夕死” ->新生代
  2. 对象熬过了很多次垃圾回收,越难回收 ->老年代
  3. fullGC也会回收包括方法区在内的空间,即使方法区中的内容很难回收(静态变量、常量…)

复制算法(即前面的复制回收算法)

image-20220814122152653

image-20220814122206391

  • 实现简单、运行高效

  • 内存复制、没有内存碎片

  • 利用率只有一半(预留一半进行复制)

  • Appel式回收

    即前面提到的,Eden区与From、To区分配比例大致为8:1:1(理由上面提及过,第一次回收时90%以上的都会被回收,只有不到10%才能进入From区)

    这样使空间利用率提高了,提升至90%。

标记清除算法

  • 即在回收前标记需要回收的空间
  • 执行效率不稳定(可能有90%需要回收、可能10%需要回收)
  • 内存碎片导致提前GC

标记-整理算法(Mark-Compact)

是标记清楚算法缺点的一种解决方案,但同样也存在明显缺陷。

  • 对象移动
  • 引用更新
  • 用户线程暂停
  • 没有内存碎片

image-20220814122427544

JVM中常见的垃圾回收器

  • 单线程垃圾回收器
  • 多线程并行垃圾回收器
  • 多线程并发垃圾回收器:支持垃圾回收线程与用户线程 同时工作。

image-20220814122835721

CMS

Concurrent Mark Seep并发标记清除

  • 减少了STW(Stop The World)的时间
  • 只针对老年代

image-20220814123049105

标记阶段

  • 初始标记(暂停所有用户线程):标记GCRoots直接相连的对象,所以速度很快。
  • 并发标记:标记GCRoots所有关联的对象。
  • 重新标记(暂停所有用户线程):将中间有变动的重新标记(并发收集是GC不干净的,所以需要重新标记),时间短。

清理阶段

  • 并发清理(用户和GC同时进行):时间长
  • 重置线程

缺点

  • CPU敏感:用户线程、GC线程都要跑,如果CPU核心数不足,那么对用户影响会很大
  • 浮动垃圾:并发清理时,用户线程还是会产生垃圾
  • 内存碎片

常量池与String

最后再来谈一个易混淆的知识点,String,同时介绍一个常量池。

静态常连池

所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。

运行时常量池

运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,咱们常说的常量池,就是指方法区中的运行时常量池。

String

String str = new String(“abc”);

image-20220814153313986

intern

在调用intern方法之后,会去常量池中查找是否有等于该字符串常量对象的引用,有就返回引用。