前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM基础

JVM基础

原创
作者头像
橘子又加强了么
发布2023-09-25 09:35:45
2350
发布2023-09-25 09:35:45
举报

JVM

参考链接:https://www.cnblogs.com/yrxing/p/14464799.html

https://github.com/YaoChuanbiao/Java-Summarize

https://juejin.cn/post/7143112298628907044

JVM(Java Virtual Machine)是Java虚拟机的缩写,它是Java程序运行的环境和平台。

JVM是Java语言的核心组成部分,它负责解释和执行Java字节码(Bytecode)指令,将Java源代码编译的字节码转换为机器可以执行的指令。JVM提供了一种与具体硬件平台无关的执行环境,使得Java程序具有跨平台的特性,可以在不同的操作系统和硬件架构上运行。

JVM运行
JVM运行
image
image

JVM主要包含以下几个组件:

  1. 类加载器(Class Loader):负责将字节码文件加载到内存中,并转换为JVM能够理解的数据结构(类对象)。类加载器还负责类的初始化、链接和安全性检查等工作。
  2. 运行时数据区(Runtime Data Area):包括方法区、堆、栈、程序计数器和本地方法栈等。这些数据区域用于存储程序执行过程中的数据和临时变量。
  3. 执行引擎(Execution Engine):负责解释和执行字节码指令。执行引擎可以采用解释执行或即时编译(Just-In-Time Compilation)的方式来执行字节码。
  4. 垃圾收集器(Garbage Collector):自动管理内存,回收不再使用的对象,释放内存空间。垃圾收集器通过标记-清除、复制、标记-整理等算法来实现垃圾回收。
  5. 即时编译器(Just-In-Time Compiler):将热点代码(Hot Spot)转换为本地机器码,以提高代码的执行速度。即时编译器属于JVM的可选组件,可以根据需要进行启用或禁用。

JVM的设计目标是提供一种可移植、高性能和安全的执行环境,使得开发人员能够编写一次代码,多平台运行。通过使用JVM,Java程序可以在不同的操作系统(如Windows、Linux、macOS等)和硬件架构(如x86、ARM等)上运行,无需针对每个平台编写不同的代码。这是Java语言的一个重要特性,也是其广泛应用于各种领域的原因之一。

类加载器

类加载器的分类

类加载器用于Java类的加载。

image
image

分为启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。

1、启动类加载器(Bootstrap ClassLoader)(引导类加载器),加载java核心类库(<JAVA_HOME>/jre/lib/rt.jar),无法被java程序直接引用,是用C++编写的,用来加载其他的类加载器(类加载器本质就是类),是所有加载器的父类

2、拓展类加载器(Extension ClassLoader),用来加载java的拓展库<JAVA_HOME>/jre/lib/ext

3、系统类加载器(System ClassLoader)(应用程序类加载器),用来加载类路径下的Java类

4、用户自定义类加载器,继承java.lang.ClassLoader类的方式实现。

官方文档中将类加载器分为引导类加载器和自定义类加载器

类装载方式

隐式装载:由加载器加载

显式装载

自定义加载,比如使用反射Class.forName(类路径),类加载器ClassLoader.getSystemClassLoader().loadClass("test.A");

使用当前进程上下文的使用的类装载Thread.currentThread().getContextClassLoader().loadClass("test.A")

======类加载是动态的====,它不会一次性加载所有类然后运行,而是保证程序运行的基础类(核心类库一部分的类)完全加载到JVM中就运行,这是为了节省内存开销。==

类加载器的性质:

全盘负责、双亲委派机制、缓存机制、可见性。

全盘负责:当一个 Class 类被某个类加载器所加载时,该 Class 所依赖引用的所有 Class 都会由这个加载器负责载入,除非显式的使用另一个 ClassLoader。(当然只是这个加载器负责,并不一定就是由这个加载器加载,这是由于双亲委托机制的作用)

缓存机制:当一个 Class 类加载完毕后,会放入缓存,在其他类需要引用这个类时就会从缓存中直接使用,这也是为什么我们在修改了文件后需要重启服务器才能使修改生效。

双亲委托机制:当一个类加载器收到了类加载的请求时,它首先会将这个请求委派给父类,父类不能执行再自己尝试执行,父类如果存在父类,也会委派给父类,这样传到了启动类加载器加载,当启动类加载器不能读取到类时才会传给子类加载器,然后子类加载器再尝试加载。

好处:1、防止自定义的类篡改核心类库中的代码 2、防止同一个类被重复加载

可见性:子类加载器可以访问父类加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没办法利用类加载器去实现容器的逻辑。后续有更新,

JVM内存结构(运行时数据区)

image
image

程序计数器

概述:较小的内存空间,是当前线程执行的字节码的行号指示器

作用:通过改变计数器的值来指定下一条需要执行的字节码指令,来恢复中断前程序运行的位置

特点:线程私有化,每个线程都有独立的程序计数器、无内存溢出、

JVM唯一 一个没有规定任何 OOM 的区域。也不存在GC

Java虚拟机栈

方法执行流程:进行压栈操作,开始方法的执行,如果此方法中调用了其他方法,那么会将调用的这个方法对应的栈帧压入栈,等到这个方法执行完之后,如果方法包含返回值,将这个返回值返回给上一个方法,然后这个被调用的栈帧出栈,随后继续执行上一个栈帧

概述:每个方法从调用直到执行的过程,对应着一个栈帧在虚拟机栈的入栈和出栈的过程

作用:每个方法执行都创建一个“栈帧”来存储局部变量表、操作数栈、动态链接、方法出口等信息

特点:线程私有化、生命周期与线程执行结束相同

image
image

参考链接:https://www.cnblogs.com/mengxinJ/p/14251272.html#_label1_0_2

创建时间:JVM启动时创建该区域

占用空间:Java虚拟机管理内存最大的一块区域

作用:用于存放对象实例及数组(所有new的对象)

特点:垃圾收集器作用该区域,回收不使用的对象的内存空间、各个线程共享的内存区域、该区域的大小可通过参数设置

方法区

作用:用于存储类信息、常量、静态变量、即时编译器编译后的代码缓存,是各个线程共享的内存区域

特点:线程共享

ps:方法区的实现在 1.8 之前是永久代,使用的是 JVM 的内存,在1.8开始实现变成元空间,使用的是本地内存。之所以这样改变,是因为原来的方法区很容易发生 OOM

之所以容易OOM是因为方法区的类信息被回收的条件非常苛刻,必须满足以下三点:

1、该类的所有对象都被回收;

2、加载该类的类加载器被回收;

3、该类对应的 Class 对象没有在任何地方被引用(无法在任何地方通过反射访问该类的方法)。

关于第三点的 Class 对象,在一个类被加载时,会在堆中创建一个用于用于访问这个类的类信息 Class 对象。而在成为元空间后,使用的是本地内存,所以方法区发生 OOM 的情况会极大改善

1)运行时常量池

image
image

当 Class 文件被类加载器加载到 JVM 中时,存储的位置就是在方法区,而在 Class 文件信息中包括着 class 文件的常量池,当 JVM 开始执行时,就会将文件常量池中的数据加载到 方法区内部的运行时常量池,变成运行时状态,并将符号引用转成直接引用。

2)字符串常量池

在 JDK 1.7 开始,字符串常量池就由方法区移入了堆中,字符串常量池是专门存放字符串常量的,至于为什么移入堆中,这是因为字符串的创建和对象一样频繁,销毁也就变得尤其频繁,而方法区的 GC 是伴随着 full gc 的, 因为 full gc 会造成 STW,在 full gc 期间其他程序都会停止,所以都会避免 full gc,而字符串常量池放在方法区中就减少了 字符串被回收的频率,提高了 OOM 的概率。

本地方法栈与native(本地)方法

本地方法栈(也就是最上面图中的本地接口)是 JVM 与底层交互的接口,用于调用 native 方法。作用与 Java 虚拟栈差不多,只不过是为 native 方法服务的,是由非 Java 语言编写的

JVM垃圾回收机制

垃圾回收器是JVM(Java Virtual Machine)的重要组成部分,其主要作用是自动管理内存并回收不再使用的对象。以下是垃圾回收器的主要作用:

  1. 自动内存管理:垃圾回收器负责自动分配和释放内存空间,开发人员无需手动管理内存。它跟踪对象的使用情况并标记不再被引用的对象,将其标记为垃圾。
  2. 回收无用对象:垃圾回收器定期扫描堆内存,识别并回收不再被引用的对象。这些无用对象占用了内存空间,但不再对程序的执行产生任何影响,回收它们可以释放内存,提供更多的可用空间。
  3. 解决内存泄漏:内存泄漏指的是应用程序中的对象占用了内存空间,但由于不正确的引用导致无法被回收。垃圾回收器可以识别这些无法访问的对象,并回收它们,从而解决内存泄漏问题。
  4. 减少内存碎片:在程序运行过程中,频繁地创建和销毁对象会导致内存空间出现碎片化。垃圾回收器可以进行内存整理和压缩,将不连续的内存空间整理成连续的块,减少内存碎片,提高内存利用率。
  5. 提高应用性能:垃圾回收器可以在合适的时机执行垃圾回收操作,回收无用对象,释放内存空间。通过减少内存占用和内存碎片,可以提高应用程序的性能和响应速度。

总的来说,垃圾回收器的作用是帮助开发人员管理内存,自动回收不再使用的对象,解决内存泄漏问题,减少内存碎片,提高应用程序的性能和可靠性。

JVM有有哪些GC
  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

GC算法

1、引用计数法(很少使用)

  • 每个对象在创建的时候,就给这个对象绑定一个计数器。
  • 每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一;
  • 这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。

2、可达性算法

通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系链向下搜索,如果某个对象无法被搜索到,则说明该对象无引用执行,可回收。相反,则对象处于存活状态,不可回收。

3、标记-清除算法

为每个对象存储一个标记位,记录对象的生存状态。

  • 标记阶段:这个阶段内,为每个对象更新标记位,检查对象是否死亡。
  • 清除阶段:该阶段对死亡的对象进行清除,执行GC操作。

缺点:两次扫描严重浪费时间;会产生内存碎片,如果Java堆中包含大量对象,而且大部分是需要被回收的,这时必须记性大量标记及清除动作,导致标记和清除两个过程执行效率都随对象数量增长而降低

优点:不需要额外的空间

5、复制算法

复制算法主要发生在年轻代(幸存0区和幸存1区)

  • 当Eden区满的时候,会触发轻GC,每触发一次,活的对象就被转移到幸存区,死的对象就被GC清理掉,所以每次触发轻GC时,Eden区就会清空
  • 对象被转移到了幸存区,幸存区又分为From Space和To Space,这两块区域是动态交换的,谁是空的谁就是To Space,然后From Space就会把全部对象转移到To Space去;
  • 那如果两块区域都不为空呢?这就用到了复制算法,其中一个区域会将存活的对象转移到另一个区域去,然后将自己区域的内存空间清空,这样该区域为空,又成为了To Space
  • 所以每次触发轻GC后,Eden区清空,同时To区也清空了,所有的对象都在From区
  • 好处:没有内存碎片,适用于高吞吐量

坏处:浪费内存空间(浪费幸存区一半的空间);对象存活率较高的场景下,需要复制的东西太多,效率会下降。

最佳使用环境:对象存活率较低的时候,也就是年轻代。

4、标记-整理算法

这个是标记-清除算法的一个改进版,又叫做标记-清除-压缩算法。不同的是在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存货的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。可以进一步优化,在内存碎片不太多的情况下,就继续标记清除,到达一定量的时候再压缩。

优点:

1、弥补了“标记-清除”算法,内存区域分散的缺点

2、弥补了“标记-复制”算法内存减半的代价

缺点:

1、效率不高,对于“标记-清除”而言多了整理工作。

内存效率:复制算法>标记清除算法>标记压缩算法

内存整齐度:复制算法 = 标记压缩算法>标记清除算法

内存利用率:标记压缩算法= 标记清除算法 >复制算法

分代收集

当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下。具体如下:

根据各个年代的特点采取最适当的收集算法

1、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。

2、老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。

GC分类

轻GC(Minor GC)针对年轻代进行垃圾回收,回收短命对象,保持年轻代的空间可用性。

重GC(Major GC)对年轻代和部分老年代进行垃圾回收,回收较大的对象和年龄较大的存活对象。

完全GC(Full GC)对整个堆进行垃圾回收,清除整个堆中的不再被引用的对象。完全GC的过程较长,可能导致较长的停顿时间。

Minor GC触发条件:

当Eden区满时,触发Minor GC。

Full GC触发条件

(1) System.gc()方法的调用

此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。

(2) 老年代空间不足

老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

(3) 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC

(4) 对象太大,年轻代容不下

什么情况对象直接在老年代分配

分配的对象大小大于eden space。适合所有收集器。

eden space剩余空间不足分配,且需要分配对象内存大小不小于eden space总空间的一半,直接分配到老年代,不触发Minor GC。适合-XX:+UseParallelGC、-XX:+UseParallelOldGC,即适合Parallel Scavenge。

大对象直接进入老年代,使用-XX:PretenureSizeThreshold参数控制,适合-XX:+UseSerialGC、-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC,即适合Serial和ParNew收集器。

GC分代年龄为什么最大为15?

因为Object Header采用4个bit位来保存年龄,4个bit位能表示的最大数就是15

内存溢出

内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存

内存溢出的原因

主要有两点:

分配的少了:比如虚拟机本身可使用的内存太少。

应用用的太多,并且用完没释放

举例如下:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小;

处理方法

1、先把内存镜像dump出来,有两种方法

设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息

使用jmap命令。"jmap -dump:format=b,file=heap.bin " 其中pid可以通过jps获取

2、得到内存信息文件后就使用工具去分析,也有两个工具

mat: eclipse memory analyzer, 基于eclipse RCP的内存分析工具

jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等

栈溢出

  1. 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
  2. 局部静态变量体积太大,局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。
  3. 指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。

解决办法

  1. 用栈把递归转换成非递归
  2. 使用static对象替代nonstatic局部对象
  3. 增大堆栈大小值

Java程序运行慢怎么解决

  1. 查看部署应用系统的系统资源使用情况,CPU,内存,IO这几个方面去看。找到对应的进程。
  2. 使用jstack,jmap等命令查看是JVM是在在什么类型的内存空间中做GC(内存回收),和查看GC日志查看是那段代码在占用内存。 首先,调节内存的参数设置,如果还是一样的问题,就要定位到相应的代码。
  3. 定位代码,修改代码(一般是代码的逻辑问题,或者代码获取的数据量过大。)

逃逸分析

逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。

逃逸分析可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配,由于该对象一定是局部的,所以栈上分配不会有问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JVM
    • 类加载器
      • 类加载器的分类
      • 类装载方式
      • 类加载器的性质:
    • JVM内存结构(运行时数据区)
      • JVM垃圾回收机制
        • GC算法
        • 分代收集
        • GC分类
        • 什么情况对象直接在老年代分配
        • GC分代年龄为什么最大为15?
      • 内存溢出
        • 内存溢出的原因
        • 栈溢出
        • Java程序运行慢怎么解决
        • 逃逸分析
    相关产品与服务
    对象存储
    对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档