据说看完这篇 JVM 要一小时(四)

简介: 大家好,我是干货旋,今天又给大家怼干货了,我不就放纵了几天么,有些难搞的读者还以为我把号卖了,呐,你说我不出干货,我就问你这篇文章敢不敢三连。

什么是记忆集,什么是卡表?记忆集和卡表有什么关系?

为了解决跨代引用问题,提出了记忆集这个概念,记忆集是一个在新生代中使用的数据结构,它相当于是记录了一些指针的集合,指向了老年代中哪些对象存在跨代引用。

记忆集的实现有不同的粒度

  • 字长精度:每个记录精确到一个字长,机器字长就是处理器的寻址位数,比如常见的 32 位或者 64 位处理器,这个精度决定了机器访问物理内存地址的指针长度,字中包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,区域内含有跨代指针。

其中卡精度是使用了卡表作为记忆集的实现,关于记忆集和卡表的关系,大家可以想象成是 HashMap 和 Map 的关系。

什么是卡页?

卡表其实就是一个字节数组

CARD_TABLE[this address >> 9] = 0;

字节数组 CARD_TABLE 的每一个元素都对应着内存区域中一块特定大小的内存块,这个内存块就是卡页,一般来说,卡页都是 2 的 N 次幂字节数,通过上面的代码我们可以知道,卡页一般是 2 的 9 次幂,这也是 HotSpot 中使用的卡页,即 512 字节。

一个卡页的内存通常包含不止一个对象,只要卡页中有一个对象的字段存在跨代指针,那就将对应卡表的数组元素的值设置为 1,称之为这个元素变了,没有标示则为 0 。在垃圾收集时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,然后把他们加入 GC Roots 进行扫描。

所以,卡页和卡表主要用来解决跨代引用问题的。

什么是写屏障?写屏障带来的问题?

如果有其他分代区域中对象引用了本区域的对象,那么其对应的卡表元素就会变脏,这个引用说的就是对象赋值,也就是说卡表元素会变脏发生在对象赋值的时候,那么如何在对象赋值的时候更新维护卡表呢?

在 HotSpot 虚拟机中使用的是写屏障(Write Barrier) 来维护卡表状态的,这个写屏障和我们内存屏障完全不同,希望读者不要搞混了。

这个写屏障其实就是一个 Aop 切面,在引用对象进行赋值时会产生一个环形通知(Around),环形通知就是切面前后分别产生一个通知,因为这个又是写屏障,所以在赋值前的部分写屏障叫做写前屏障,在赋值后的则叫做写后屏障。

写屏障会带来两个问题

无条件写屏障带来的性能开销

每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作。显然,这会增加一些额外的开销。但是,扫描整个老年代相比较,这个开销就低得多了。

不过,在高并发环境下,写屏障又带来了伪共享(false sharing)问题。

高并发下伪共享带来的性能开销

在高并发情况下,频繁的写屏障很容易发生伪共享(false sharing),从而带来性能开销。

假设 CPU 缓存行大小为 64 字节,由于一个卡表项占 1 个字节,这意味着,64 个卡表项将共享同一个缓存行。

HotSpot 每个卡页为 512 字节,那么一个缓存行将对应 64 个卡页一共 64*512 = 32K B。

如果不同线程对对象引用的更新操作,恰好位于同一个 32 KB 区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能。

一个简单的解决方案,就是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表项未被标记过才将其标记为脏的。

这就是 JDK  7 中引入的解决方法,引入了一个新的 JVM 参数 -XX:+UseCondCardMark,在执行写屏障之前,先简单的做一下判断。如果卡页已被标识过,则不再进行标识。

简单理解如下:

if (CARD_TABLE [this address >> 9] != 0)
  CARD_TABLE [this address >> 9] = 0;

与原来的实现相比,只是简单的增加了一个判断操作。

虽然开启 -XX:+UseCondCardMark 之后多了一些判断开销,但是却可以避免在高并发情况下可能发生的并发写卡表问题。通过减少并发写操作,进而避免出现伪共享问题(false sharing)。

什么是三色标记法?三色标记法会造成哪些问题?

根据可达性算法的分析可知,如果要找出存活对象,需要从 GC Roots 开始遍历,然后搜索每个对象是否可达,如果对象可达则为存活对象,在 GC Roots 的搜索过程中,按照对象和其引用是否被访问过这个条件会分成下面三种颜色:

  • 白色:白色表示 GC Roots 的遍历过程中没有被访问过的对象,出现白色显然在可达性分析刚刚开始的阶段,这个时候所有对象都是白色的,如果在分析结束的阶段,仍然是白色的对象,那么代表不可达,可以进行回收。
  • 灰色:灰色表示对象已经被访问过,但是这个对象的引用还没有访问完毕。
  • 黑色:黑色表示此对象已经被访问过了,而且这个对象的引用也已经被访问了。

注:如果标记结束后对象仍为白色,意味着已经“找不到”该对象在哪了,不可能会再被重新引用。

现代的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。

三色标记法会造成两种问题,这两种问题所出现的环境都是由于用户环境和收集器并行工作造成的 。当用户线程正在修改引用关系,此时收集器在回收引用关系,此时就会造成把原本已经消亡的对象标记为存活,如果出现这种状况的话,问题不大,下次再让收集器重新收集一波就完了,但是还有一种情况是把存活的对象标记为死亡,这种状况就会造成不可预知的后果。

针对上面这两种对象消失问题,业界有两种处理方式,一种是增量更新(Incremental Update) ,一种是原是快照(Snapshot At The Beginning, SATB)

请你介绍一波垃圾收集器

垃圾收集器是面试的常考,也是必考点,只要涉及到 JVM 的相关问题,都会围绕着垃圾收集器来做一波展开,所以,有必要了解一下这些垃圾收集器。

垃圾收集器有很多,不同商家、不同版本的 JVM 所提供的垃圾收集器可能会有很大差别,我们主要介绍 HotSpot 虚拟机中的垃圾收集器。

垃圾收集器是垃圾回收算法的具体实现,我们上面提到过,垃圾回收算法有标记-清除算法、标记-整理、标记-复制,所以对应的垃圾收集器也有不同的实现方式。

我们知道,HotSpot 虚拟机中的垃圾收集都是分代回收的,所以根据不同的分代,可以把垃圾收集器分为

新生代收集器:Serial、ParNew、Parallel Scavenge;

老年代收集器:Serial Old、Parallel Old、CMS;

整堆收集器:G1;

Serial 收集器

Serial 收集器是一种新生代的垃圾收集器,它是一个单线程工作的收集器,使用复制算法来进行回收,单线程工作不是说这个垃圾收集器只有一个,而是说这个收集器在工作时,必须暂停其他所有工作线程,这种暴力的暂停方式就是 Stop The World,Serial 就好像是寡头垄断一样,只要它一发话,其他所有的小弟(线程)都得给它让路。Serial 收集器的示意图如下:

微信图片_20220417152543.jpg

SefePoint 全局安全点:它就是代码中的一段特殊的位置,在所有用户线程到达 SafePoint 之后,用户线程挂起,GC 线程会进行清理工作。

虽然 Serial 有 STW 这种显而易见的缺点,不过,从其他角度来看,Serial 还是很讨喜的,它还有着优于其他收集器的地方,那就是简单而高效,对于内存资源首先的环境,它是所有收集器中额外内存消耗最小的,对于单核处理器或者处理器核心较少的环境来说,Serial 收集器由于没有线程交互开销,所以 Serial 专心做垃圾回收效率比较高。

ParNew 收集器

ParNew 是 Serial 的多线程版本,除了同时使用多条线程外,其他参数和机制(STW、回收策略、对象分配规则)都和 Serial 完全一致,ParNew 收集器的示意图如下:

微信图片_20220417152547.jpg

虽然 ParNew 使用了多条线程进行垃圾回收,但是在单线程环境下它绝对不会比 Serial 收集效率更高,因为多线程存在线程交互的开销,但是随着可用 CPU 核数的增加,ParNew 的处理效率会比 Serial 更高效。

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的,而且它也能够并行收集,这么看来,表面上 Parallel Scavenge 与 ParNew 非常相似,那么它们之间有什么区别呢?

Parallel Scavenge 的关注点主要在达到一个可控制的吞吐量上面。吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比。也就是

微信图片_20220417152553.jpg

这里给大家举一个吞吐量的例子,如果执行用户代码的时间 + 运行垃圾收集的时间总共耗费了 100 分钟,其中垃圾收集耗费掉了 1 分钟,那么吞吐量就是 99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量,良好的响应速度可以提升用户体验,而高吞吐量可以最高效率利用处理器资源。

Serial Old 收集器

前面介绍了一下 Serial,我们知道它是一个新生代的垃圾收集,使用了标记-复制算法。而这个 Serial Old 收集器却是 Serial 的老年版本,它同样也是一个单线程收集器,使用的是标记-整理算法,Serial Old 收集器有两种用途:一种是在 JDK 5 和之前的版本与 Parallel Scavenge 收集器搭配使用,另外一种用法就是作为 CMS 收集器的备选,CMS 垃圾收集器我们下面说,Serial Old 的收集流程如下

微信图片_20220417152555.jpg

Parallel Old 收集器

前面我们介绍了 Parallel Scavenge 收集器,现在来介绍一下 Parallel Old 收集器,它是 Parallel Scavenge 的老年版本,支持多线程并发收集,基于标记 - 整理算法实现,JDK 6 之后出现,吞吐量优先可以考虑 Parallel Scavenge + Parallel Old 的搭配

微信图片_20220417152558.jpg

CMS 收集器

CMS收集器的主要目标是获取最短的回收停顿时间,它的全称是 Concurrent Mark Sweep,从这个名字就可以知道,这个收集器是基于标记 - 清除算法实现的,而且支持并发收集,它的运行过程要比上面我们提到的收集器复杂一些,它的工作流程如下:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

对于上面这四个步骤,初始标记和并发标记都需要 Stop The World,初始标记只是标记一下和 GC Roots 直接关联到的对象,速度较快;并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程。这个过程时间比较长但是不需要停顿用户线程,也就是说与垃圾收集线程一起并发运行。并发标记的过程中,可能会有错标或者漏标的情况,此时就需要在重新标记一下,最后是并发清除阶段,清理掉标记阶段中判断已经死亡的对象。

CMS 的收集过程如下

微信图片_20220417152603.jpg

CMS 是一款非常优秀的垃圾收集器,但是没有任何收集器能够做到完美的程度,CMS 也是一样,CMS 至少有三个缺点:

  • CMS 对处理器资源非常敏感,在并发阶段,虽然不会造成用户线程停顿,但是却会因为占用一部分线程而导致应用程序变慢,降低总吞吐量。
  • CMS 无法处理浮动垃圾,有可能出现Concurrent Mode Failure失败进而导致另一次完全 Stop The WorldFull GC 产生。

什么是浮动垃圾呢?由于并发标记和并发清理阶段,用户线程仍在继续运行,所以程序自然而然就会伴随着新的垃圾不断出现,而且这一部分垃圾出现在标记结束之后,CMS 无法处理这些垃圾,所以只能等到下一次垃圾回收时在进行清理。这一部分垃圾就被称为浮动垃圾。

  • CMS 最后一个缺点是并发-清除的通病,也就是会有大量的空间碎片出现,这将会给分配大对象带来困难。

Garbage First 收集器

Garbage First 又被称为 G1 收集器,它的出现意味着垃圾收集器走过了一个里程碑,为什么说它是里程碑呢?因为 G1 这个收集器是一种面向局部的垃圾收集器,HotSpot 团队开发这个垃圾收集器为了让它替换掉 CMS 收集器,所以到后来,JDK 9 发布后,G1 取代了 Parallel Scavenge + Parallel Old 组合,成为服务端默认的垃圾收集器,而 CMS 则不再推荐使用。

之前的垃圾收集器存在回收区域的局限性,因为之前这些垃圾收集器的目标范围要么是整个新生代、要么是整个老年代,要么是整个 Java 堆(Full GC),而 G1 跳出了这个框架,它可以面向堆内存的任何部分来组成回收集(Collection Set,CSet),衡量垃圾收集的不再是哪个分代,这就是 G1 的 Mixed GC 模式。

G1 是基于 Region 来进行回收的,Region 就是堆内存中任意的布局,每一块 Region 都可以根据需要扮演 Eden 空间、Survivor 空间或者老年代空间,收集器能够对不同的 Region 角色采用不同的策略来进行处理。Region 中还有一块特殊的区域,这块区域就是 Humongous 区域,它是专门用来存储大对象的,G1 认为只要大小超过了 Region 容量一半的对象即可判定为大对象。如果超过了 Region 容量的大对象,将会存储在连续的 Humongous Region 中,G1 大多数行为都会把 Humongous Region 作为老年代来看待。

G1 保留了新生代(Eden Suvivor)和老年代的概念,但是新生代和老年代不再是固定的了。它们都是一系列区域的动态集合。

G1 收集器的运作过程可以分为以下四步:

  • 初始标记:这个步骤也仅仅是标记一下 GC Roots 能够直接关联到的对象;并修改 TAMS 指针的值(每一个 Region 都有两个 RAMS 指针),使得下一阶段用户并发运行时,能够在可用的 Region 中分配对象,这个阶段需要暂停用户线程,但是时间很短。这个停顿是借用 Minor GC 的时候完成的,所以可以忽略不计。
  • 并发标记:从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆中的对象图,找出要回收的对象。当对象图扫描完成后,重新处理 SATB 记录下的在并发时有引用的对象;
  • 最终标记:对用户线程做一个短暂的暂停,用于处理并发阶段结束后遗留下来的少量 SATB 记录(一种原始快照,用来记录并发标记中某些对象)
  • 筛选回收:负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择多个 Region 构成回收集,然后把决定要回收的那一部分 Region 存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作设计对象的移动,所以必须要暂停用户线程,由多条收集器线程并行收集

从上面这几个步骤可以看出,除了并发标记外,其余三个阶段都需要暂停用户线程,所以,这个 G1 收集器并非追求低延迟,官方给出的设计目标是在延迟可控的情况下尽可能的提高吞吐量,担任全功能收集器的重任。

下面是 G1 回收的示意图

微信图片_20220417152609.jpg

G1 收集器同样也有缺点和问题:

  • 第一个问题就是 Region 中存在跨代引用的问题,我们之前知道可以用记忆集来解决跨代引用问题,不过 Region 中的跨代引用要复杂很多;
  • 第二个问题就是如何保证收集线程与用户线程互不干扰的运行?CMS 使用的是增量更新算法,G1 使用的是原始快照(SATB),G1 为 Region 分配了两块 TAMS 指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须在这两个指针位置以上。如果内存回收速度赶不上内存分配速度,G1 收集器也要冻结用户线程执行,导致 Full GC 而产生长时间的 STW。
  • 第三个问题是无法建立可预测的停顿模型。

JVM 常用命令介绍

下面介绍一下 JVM 中常用的调优、故障处理等工具。

  1. jps :虚拟机进程工具,全称是 JVM Process Status Tool,它的功能和 Linux 中的 ps 类似,可以列出正在运行的虚拟机进程,并显示虚拟机执行主类 Main Class 所在的本地虚拟机唯一 ID,虽然功能比较单一,但是这个命令绝对是使用最高频的一个命令。
  2. jstat:虚拟机统计信息工具,用于监视虚拟机各种运行状态的信息的命令行工具,它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。
  3. jinfo:Java 配置信息工具,全称是 Configuration Info for Java,它的作用是可以实时调整虚拟机各项参数。
  4. jmap:Java 内存映像工具,全称是 Memory Map For Java,它用于生成转储快照,用来排查内存占用情况
  5. jhat:虚拟机堆转储快照分析工具,全称是 JVM Heap Analysis Tool,这个指令通常和 jmap 一起搭配使用,jhat 内置了一个 HTTP/Web 服务器,生成转储快照后可以在浏览器中查看。不过,一般还是 jmap 命令使用的频率比较高。
  6. jstack:Java 堆栈跟踪工具,全称是 Stack Trace for Java ,顾名思义,这个命令用来追踪堆栈的使用情况,用于虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条正在执行的方法堆栈的集合。

什么是双亲委派模型?

JVM 类加载默认使用的是双亲委派模型,那么什么是双亲委派模型呢?

这里我们需要先介绍一下三种类加载器:

  • 启动类加载器,Bootstrap Class Loader,这个类加载器是 C++ 实现的,它是 JVM 的一部分,这个类加载器负责加载存放在 <JAVA_HOME>\lib 目录,启动类加载器无法被 Java 程序直接引用。这也就是说,JDK 中的常用类的加载都是由启动类加载器来完成的。
  • 扩展类加载器,Extension Class Loader,这个类加载器是 Java 实现的,它负责加载 <JAVA_HOME>\lib\ext 目录。
  • 应用程序类加载器,Application Class Loader,这个类加载器是由 sum.misc.Launcher$AppClassLoader 来实现,它负责加载 ClassPath 上所有的类库,如果应用程序中没有定义自己的类加载器,默认使用就是这个类加载器。

所以,我们的 Java 应用程序都是由这三种类加载器来相互配合完成的,当然,用户也可以自己定义类加载器,即 User Class Loader,这几个类加载器的模型如下

微信图片_20220417152614.jpg

上面这几类类加载器构成了不同的层次结构,当我们需要加载一个类时,子类加载器并不会马上去加载,而是依次去请求父类加载器加载,一直往上请求到最高类加载器:启动类加载器。当启动类加载器加载不了的时候,依次往下让子类加载器进行加载。这就是双亲委派模型。

双亲委派模型的缺陷?

在双亲委派模型中,子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。这就导致了双亲委派模型并不能解决所有的类加载器问题。

Java 提供了很多外部接口,这些接口统称为 Service Provider Interface, SPI,允许第三方实现这些接口,而这些接口却是 Java 核心类提供的,由 Bootstrap Class Loader 加载,而一般的扩展接口是由 Application Class Loader 加载的,Bootstrap Class Loader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给 Application Class Loader,因为它是最顶层的类加载器。

双亲委派机制的三次破坏

虽然双亲委派机制是 Java 强烈推荐给开发者们的类加载器的实现方式,但是并没有强制规定你必须就要这么实现,所以,它一样也存在被破坏的情况,实际上,历史上一共出现三次双亲委派机制被破坏的情况:

  • 双亲委派机制第一次被破坏发生在双亲委派机制出现之前,由于双亲委派机制 JDK 1.2 之后才引用的,但类加载的概念在 Java 刚出现的时候就有了,所以引用双亲委派机制之前,设计者们必须兼顾开发者们自定义的一些类加载器的代码,所以在 JDK 1.2 之后的 java.lang.ClassLoader 中添加了一个新的 findClass 方法,引导用户编写类加载器逻辑的时候重写这个 findClass 方法,而不是基于 loadClass编写。
  • 双亲委派机制第二次被破坏是由于它自己模型导致的,由于它只能向上(基础)加载,越基础的类越由上层加载器加载,所以如果基础类型又想要调用用户的代码,该怎么办?这也就是我们上面那个问题所说的 SPI 机制。那么 JDK 团队是如何做的呢?它们引用了一个 线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader 进行设置,如果创建时线程还未设置,它将会从父线程中继承,如果全局没有设置类加载器的话,这个 ClassLoader 就是默认的类加载器。这种行为虽然是一种犯规行为,但是 Java 代码中的 JNDI、JDBC 等都是使用这种方式来完成的。直到 JDK 6 ,引用了 java.util.ServiceLoader,使用 META-INF/services + 责任链的设计模式,才解决了 SPI 的这种加载机制。
  • 双亲委派机制第三次被破坏是由于用户对程序的动态需求使热加载、热部署的引入所致。由于时代的变化,我们希望 Java 能像鼠标键盘一样实现热部署,即时加载(load class),引入了 OSGI,OSGI 实现热部署的关键在于它自定义类加载器机制的实现,OSGI 中的每一个 Bundle 也就是模块都有一个自己的类加载器。当需要更换 Bundle 时,就直接把 Bundle 连同类加载器一起替换掉就能够实现热加载。在 OSGI 环境下,类加载器不再遵从双亲委派机制,而是使用了一种更复杂的加载机制。

常见的 JVM 调优参数有哪些?

  • -Xms256m:初始化堆大小为 256m;
  • -Xmx2g:最大内存为 2g;
  • -Xmn50m:新生代的大小50m;
  • -XX:+PrintGCDetails 打印 gc 详细信息;
  • -XX:+HeapDumpOnOutOfMemoryError  在发生OutOfMemoryError错误时,来 dump 出堆快照;
  • -XX:NewRatio=4    设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8 设置新生代 Eden 和 Survivor 比例为 8:2;
  • -XX:+UseSerialGC   新生代和老年代都用串行收集器 Serial + Serial Old
  • -XX:+UseParNewGC 指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelGC  新生代使用 Parallel Scavenge,老年代使用 Serial Old
  • -XX:+UseParallelOldGC:新生代 ParallelScavenge + 老年代 ParallelOld 组合;
  • -XX:+UseConcMarkSweepGC:新生代使用 ParNew,老年代使用 CMS;
  • -XX:NewSize:新生代最小值;
  • -XX:MaxNewSize:新生代最大值
  • -XX:MetaspaceSize 元空间初始化大小
  • -XX:MaxMetaspaceSize 元空间最大值

后记

这篇文章是 JVM 面试题的第二版,新增了很多内容,写的时间也比较长了,如果你觉得文章还不错的话,大家三连走起!另外,分享到朋友圈是对我莫大的支持,感谢!不骗你,看完真的需要一小时。

相关文章
|
存储 缓存 算法
据说看完这篇 JVM 要一小时(三)
大家好,我是干货旋,今天又给大家怼干货了,我不就放纵了几天么,有些难搞的读者还以为我把号卖了,呐,你说我不出干货,我就问你这篇文章敢不敢三连。
据说看完这篇 JVM 要一小时(三)
|
存储 算法 Java
据说看完这篇 JVM 要一小时(二)
大家好,我是干货旋,今天又给大家怼干货了,我不就放纵了几天么,有些难搞的读者还以为我把号卖了,呐,你说我不出干货,我就问你这篇文章敢不敢三连。
据说看完这篇 JVM 要一小时(二)
|
存储 安全 Java
据说看完这篇 JVM 要一小时(一)
大家好,我是干货旋,今天又给大家怼干货了,我不就放纵了几天么,有些难搞的读者还以为我把号卖了,呐,你说我不出干货,我就问你这篇文章敢不敢三连。
据说看完这篇 JVM 要一小时(一)
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
4天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
2天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
6 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
55 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
21天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
45 10
|
20天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。