3256 字
16 分钟
JVM笔记
2024-10-19

JVM:java虚拟机,是java实现跨平台的基石。

执行java程序的过程:源代码编译成class字节码,程序执行时由jvm解释翻译成对应平台的机器指令并运行。

jvm内存区域的划分

jvm内存区域可分为5个部分,分别是虚拟机栈,堆,方法区,本地方法栈和程序计数器。
其中方法区和堆是所有线程共享的区域,生命周期是随着虚拟机的创建而创建,随着虚拟机的结束而销毁。 虚拟机栈,本地方法栈,程序计数器在线程之间相互隔离,每个线程都有一块自己的区域,生命周期是随着线程的启动而创建,随着线程的结束而销毁。

程序计数器#
  • 用于记录代码运行到什么位置,在字节码解析工作进行的时候会改变这个值来指定下一条即将执行的指令。
虚拟机栈#
  • 当每个方法被执行的时候,JVM会同步创建一个栈帧,栈帧中包含当前方法的信息,如局部变量表,操作数栈,动态链接和方法出口等。
    本地方法栈:与虚拟机栈差不多,是一个为本地方法(Native Method)服务的栈。
#
  • 是jvm中占内存最大的一块区域,在虚拟机启动的时候被创建,这块内存的唯一目的就是存放和管理对象,当java堆被填满且无法进行垃圾回收的时候就会抛出OutOfMemoryError异常,堆是垃圾收集器管理的主要区域。
方法区#
  • 与堆一样,是线程共享的区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译的代码等。在JDK 7 之后,字符串常量从方法区移动到了堆中。
直接内存#
  • 又称堆外内存,是一块不受jvm管控的内存区域,这个区域的内存需要手动申请和释放,不会受到堆内存容量的限制,但仍然会受限于计算机的实际内存。在JDK1.4 引入了NIO类,是一种基于通道与缓冲区的I/O方式,可以直接使用Native函数库来分配堆外内存,之后通过一个存储在Java中的DirectByteBuffer 对象作为对这块内存的引用来进行操作,这样可以在某些场景中显著地提高性能。
总结#
  • (线程独有)程序计数器:存储当前程序的执行位置。
  • (线程独有)虚拟机栈:通过栈帧来维持方法调用顺序,帮助控制程序的有序运行。
  • (线程独有)本地方法栈:作用通虚拟机栈,只用于本地方法(Native Method)。
  • 堆:所有的对象都在这里保存。
  • 方法区:保存类信息、即时编译器的代码缓存、运行时常量池。

垃圾回收机制#

可达性分析算法#

用于判断对象是否存活,此算法使用了类似于树结构的搜索机制,如果某个对象无法到达任何GC Roots,则说明这个对象是不可能再被使用的。

每个对象都有机会成为树的根节点(GC Roots),被选定为根节点的条件如下:

  • 位于虚拟机栈的栈帧中的本地变量表中所引用到的对象(方法中的局部变量)同样也包括本地方法栈中JNI引用的对象。
  • 类的静态成员变量引用的对象。
  • 方法区中,常量池里面引用的对象。
  • 被加了锁的对象
  • 虚拟机内部需要使用到的对象。

    一旦已经存在的根节点不满足存在的条件时,那么根节点与对象之间的连接将被断开。此时虽然对象1仍存在对其他对象的引用,但是由于其没有任何根节点引用,所以此对象即可被判定为不再使用。比如某个方法中的局部变量引用,在方法执行完成返回之后:
最终判定#

在经历了可达性分析算法后可以判定哪些对象可以被回收,但是并不代表对象一定会被回收,可以在最终判定阶段对将被回收的对象进行挽留。

protected void finalize() throws Throwable { }

这个方法是最终判定方法,如果子类重写了这个方法,那么子类对象在被判定为可回收的时候,会进行二次确认,即执行finalize() 方法,如果在这个方法中,对象重新建立了与GC Roots的引用,那么该对象将不会被回收。
finalize() 方法并不是在主线程中调用的,jvm会自动创建一个优先级较低的Finalizer线程来进行处理,且同一个对象的finalize() 方法只能被调用一次,如果第二次被判定为可回收的时候,该对象一定会被回收。

垃圾回收算法#
分代收集机制#

堆内存的划分:新生代、老年代、永久代(JDK8之前);元空间(JDK8)之后 JVM将堆内存划分为新生代、老年代和永久代(永久代是HotSpot虚拟机特有的概念,JDK8之前的方法区就是采用永久代来实现,在JDK8之后,方法区由元空间实现,且使用的是本地内存)

所有新创建的对象一开始都会进入新生代的Eden区,如果是大对象则会直接进入老年代,在对新生代区域进行垃圾回收时,首先会对新生代的对象进行扫描,并且会对不再使用的对象进行回收。

在一次垃圾回收之后,Eden区没有被回收的对象会进入Survivor区域的From区,最后From区和To区会进行一次交换。

在下一次垃圾回收的时候,由于From区已经存在对象,在GC之后Eden区存活的对象会直接复制到From区,随后所有To区中的对象会进行一次年龄判定(每经历一轮GC,年龄+1),如果对象的年龄大于15,则会直接进入老年代,否则移动到From区,最后再交换一次To区和From区。

GC的分类#
  • Minor GC:次要垃圾回收,主要进行新生代区域的GC。 -触发条件:新生代Eden区容量已满。
  • Major GC:主要垃圾回收,主要对老年代进行GC。
  • Full GC:完全垃圾回收,对整个Java堆内存和方法区进行GC。
    • 触发条件1:每次晋升到老年代的对象平均大小大于老年代剩余的空间。
    • 触发条件2:Minor GC后存活的对象超过了老年代剩余的空间。
    • 触发条件3:永久代内存不足(JDK8之前,JDK8之后永久代使用的是本地内存)
    • 触发条件4:手动调用System.gc() 方法

GC流程图

空间分配担保#

在一次GC后,新生代的Eden区仍然存在大量的对象,但此时Eden区中存活的对象所需的容量已经超出Survivor区的容量。此时,就会把Survivor区无法容纳的对象直接存到老年代中,让老年代进行空间分配担保,这样新生代就能腾出更多的空间来容纳更多的对象。如果此时老年代判断到剩余的容量也无法装下新生代的数据,就会进行一次Full GC来尝试腾出空间,之后再次进行判断是否有空间存放新生代的数据,如果依然无法容纳,则会直接抛出OOM(OutOfMemoryError)错误。

垃圾回收算法#
标记-清除算法#

标记-清除算法是最古老的垃圾回收算法,通过标记出需要回收的对象,然后依次回收被标记的对象,或者标记出不需要回收的对象,回收掉没有被标记的对象来进行垃圾回收。

优点是非常易于理解,十分简单。

缺点是如果存在大量的对象需要回收,就有可能存在大量的标记并进行大规模垃圾回收,而且在一次垃圾回收之后,连续的内存空间内有可能出现大量空隙,使得内存空间碎片化,降低了连续内存空间的利用率。

标记-复制算法#

标记-复制算法将内存区域划分成两块同样大小的区域,每次只会使用一块内存区域,每当垃圾回收完毕之后就会将所有存活的对象复制到另一半空闲的内存区域中,并且清空之前被GC的内存区域,虽然多了一步复制所有对象的操作,但是解决了大规模GC后带来的内存空间碎片化的问题,提高了内存的利用率。

此算法适合用于新生代垃圾回收,因为新生代在GC后一般不会留下太多对象,根据这个特性可以防止出现复制对象占用大量时间的问题。

标记-整理算法#

老年代中的对象都是经过多次GC才会进入的,因此针对老年代的GC可能进行过之后依然会剩下大量对象,而标记-复制算法会在GC后完整地复制整个内存区域中的内容,并且会划分出一半区域用于复制对象,显然不符合老年代GC的需求。

标记-整理算法是标记所有待回收的对象后,先不进行GC操作,而是先把所有被标记需要GC的对象整齐地排列在一片连续的内存空间中,此时老年代中的对象就分为两堆,一堆是不需要进行垃圾回收的对象,一堆是需要进行垃圾回收的对象,此时只需要回收那堆被标记需要回收的对象即可,解决了内存碎片的问题。

此算法的优点是能保证内存空间的充分利用,并且实现的繁杂程度比标记-复制算法来说要低。 缺点是效率低,要修改对象在内存中的位置,此时程序必须要暂停才能进行修改,在某些极端情况下会导致整个程序发生停顿。

因此,将标记-清除算法和标记-整理算法混合起来使用会是一种不错的解决方案,在内存空间不那么凌乱的时候使用标记-清除算法,当内存凌乱到一定程度的时候使用标记-整理算法。

垃圾收集器的实现#

Serial收集器
Serial是最古老的一个垃圾收集器,在JDK1.3.1之前这个垃圾收集器是唯一选择,它是一个单线程垃圾收集器,在进行GC的时候会暂停所有的线程到GC工作结束。新生代垃圾收集算法是用的是标记复制算法,老年代垃圾收集算法采用的是标记整理算法。

由图可以看到,进行GC的时候所有的用户线程都必须要等待GC线程完成工作,此垃圾回收器最大的缺点就是这个。
Serial收集器的优点是设计简单,在用户桌面应用场景中,使用内存量一般不会很大,因此可以在较短的时间内完成垃圾收集,在GC时因为用户线程暂停而造成的卡顿影响就会小得多,只要GC不频繁发生,使用Serial收集器是可以接受的。
在终端中输入 java -version可以查看当前模式是否为客户端模式,如果是Client VM,那么在客户端模式下的新生代默认垃圾收集器至今依然是Serial收集器。

java version "1.8.0_421"
Java(TM) SE Runtime Environment (build 1.8.0_421-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.421-b09, mixed mode)

可以在jvm.cfg文件中切换JRE使用Server VM还是Client VM,文件的默认路径为:

JDK安装目录/jre/lib/jvm.cfg

在此文件中把参数改成如下所示,就是客户端模式了

-server IGNORE
-client KNOWN

ParNew收集器
这个收集器相当于Serial收集器的多线程版本,使用的垃圾回收算法是一样的,区别只是在于支持多线程垃圾收集:

目前某些JVM默认的服务端模式的新生代垃圾收集器使用的就是此收集器。

未完持续。。。。。。

JVM笔记
https://fuwari.vercel.app/posts/jvmnote/jvm笔记/
作者
白露未晞
发布于
2024-10-19
许可协议
CC BY-NC-SA 4.0