JVM

知识点汇总

JVM是Java运行基础,面试时一定会遇到JVM的有关问题,内容相对集中,但对只是深度要求较高.

image-20210502145623024

其中内存模型,类加载机制,GC是重点方面.性能调优部分更偏向应用,重点突出实践能力.编译器优化和执行模式部分偏向于理论基础,重点掌握知识点.

需了解

内存模型各部分作用,保存哪些数据.

类加载双亲委派加载机制,常用加载器分别加载哪种类型的类.

GC分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景.

性能调优常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法.

执行模式解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2

编译器针对的场景,C2针对的是server模式,优化更激进.新技术方面Java10的graal编译器

编译器优化javac的编译过程,ast抽象语法树,编译器优化和运行器优化.

知识点详解:

1、JVM内存模型:

线程独占:栈,本地方法栈,程序计数器线程共享:堆,方法区

2、栈:

又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法 出口等信息.调用方法时执行入栈,方法返回式执行出栈.

3、本地方法栈

与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈.

4、程序计数器

保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行

Native方法时,程序计数器为空.

5、堆

JVM内存管理最大的一块,对被线程共享,目的是存放对象的实例,几乎所欲的对象实例都会放在这里,当堆 没有可用空间时,会抛出OOM异常.根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行垃圾的回收管理

6、方法区:

又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据.1.7的永 久代和1.8的元空间都是方法区的一种实现

7、JVM 内存可见性

image-20210502145709395

JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主 内存操作.由于指令重排序,读写的顺序会被打乱,因此JMM需要提供原子性,可见性,有序性保证.

image-20210502145721104

类加载与卸载

加载过程

image-20210505154140789

其中验证,准备,解析合称链接

加载通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象.

验证确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全.

准备进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null).不包含final修饰的静态变量,因为final变量在编译时分配.

解析将常量池中的符号引用替换为直接引用的过程.直接引用为直接指向目标的指针或者相对偏移量等.

初始化主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用时才 会初始化.

触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候, 或者某个子类初始化的时候.

Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸.

1、加载机制-双亲委派模式]

image-20210502145802279

双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器.父类 加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载.

优点:

  1. 避免类的重复加载

  2. 避免Java的核心API被篡改

2、分代回收

分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间.

image-20210502145900296

年轻代->标记-复制

老年代->标记-清除

3、回收算法

a、G1算法

1.9后默认的垃圾回收算法,特点保持高回收率的同时减少停顿.采用每次只清理一部分,而不是清理全部的 增量式清理,以保证停顿时间不会过长

其取消了年轻代与老年代的物理划分,但仍属于分代收集器,算法将堆分为若干个逻辑区域(region),一部分 用作年轻代,一部分用作老年代,还有用来存储巨型对象的分区.

同CMS相同,会遍历所有对象,标记引用情况,清除对象后会对区域进行复制移动,以整合碎片空间.

年轻代回收:

并行复制采用复制算法,并行收集,会StopTheWorld.

老年代回收:

会对年轻代一并回收

初始标记完成堆root对象的标记,会StopTheWorld. 并发标记 GC线程和应用线程并发执行.

最终标记完成三色标记周期,会StopTheWorld.

复制/清楚会优先对可回收空间加大的区域进行回收

b、ZGC算法

前面提供的高效垃圾回收算法,针对大堆内存设计,可以处理TB级别的堆,可以做到10ms以下的回收停顿 时间.

image-20210502145939360

  • 着色指针
  • 读屏障
  • 并发处理

  • 基于region

  • 内存压缩(整理)

roots标记:标记root对象,会StopTheWorld.

并发标记:利用读屏障与应用线程一起运行标记,可能会发生StopTheWorld. 清除会清理标记为不可用的对象.

roots重定位:是对存活的对象进行移动,以腾出大块内存空间,减少碎片产生.重定位最开始会StopTheWorld,却决于重定位集与对象总活动集的比例.

并发重定位与并发标记类似.

介绍下 Java 内存区域(运⾏时数据区)

Java 虚拟机在执⾏ Java 程序的过程中会把它管理的内存划分成若⼲个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下⾯会介绍到。

JDK 1.8之前:

image-20210504205028244

JDK 1.8 :

image-20210504205038579

线程私有的:
  • 程序计数器虚拟机栈本地⽅法栈
线程共享的:
  • ⽅法区
  • 直接内存(⾮运⾏时数据区的⼀部分)

程序计数器

程序计数器是⼀块较⼩的内存空间,可以看作是当前线程所执⾏的字节码的⾏号指示器。字节码解释器

⼯作时通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳转、异常处理、 线程恢复等功能都需要依赖这个计数器来完。

另外,为了线程切换后能恢复到正确的执⾏位置,每条线程都需要有⼀个独⽴的程序计数器,各线程之 间计数器互不影响,独⽴存储,我们称这类内存区域为“线程私有”的内存。

从上⾯的介绍中我们知道程序计数器主要有两个作⽤:
  1. 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、 选择、循环、异常处理。

  2. 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能 够知道该线程上次运⾏到哪⼉了。

注意:程序计数器是唯⼀⼀个不会出现 OutOfMemoryError 的内存区域,它的⽣命周期随着线程的创建⽽创建,随着线程的结束⽽死亡。

Java 虚拟机栈

与程序计数器⼀样,Java虚拟机栈也是线程私有的,它的⽣命周期和线程相同,描述的是 Java ⽅法执

⾏的内存模型,每次⽅法调⽤的数据都是通过栈传递的。

Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java虚拟机栈是由⼀个个栈帧组成,⽽每个栈帧中都拥有:局部变量表、操作数栈、动态链接、⽅法出⼝信息。)

局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、

long、double)、对象引⽤(reference类型,它不同于对象本身,可能是⼀个指向对象起始地址的引

⽤指针,也可能是指向⼀个代表对象的句柄或其他与此对象相关的位置)。

Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
  • StackOverFlowError: 若Java虚拟机栈的内存⼤⼩不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最⼤深度的时候,就抛出StackOverFlowError异常。
  • OutOfMemoryError: 若 Java 虚拟机栈的内存⼤⼩允许动态扩展,且当线程请求栈时内存⽤完了,⽆法再动态扩展了,此时抛出OutOfMemoryError异常。

Java 虚拟机栈也是线程私有的,每个线程都有各⾃的Java虚拟机栈,⽽且随着线程的创建⽽创建,随着线程的死亡⽽死亡。

扩展:那么⽅法/函数如何调⽤?

Java 栈可⽤类⽐数据结构中栈,Java 栈中保存的主要内容是栈帧,每⼀次函数调⽤都会有⼀个对应的栈帧被压⼊Java栈,每⼀个函数调⽤结束后,都会有⼀个栈帧被弹出。

Java⽅法有两种返回⽅式:

  1. return 语句。

  2. 抛出异常。

不管哪种返回⽅式都会导致栈帧被弹出。

本地⽅法栈

和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。

本地⽅法被执⾏的时候,在本地⽅法栈也会创建⼀个栈帧,⽤于存放该本地⽅法的局部变量表、操作数 栈、动态链接、出⼝信息。

⽅法执⾏完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和OutOfMemoryError 两种异常。

Java 虚拟机所管理的内存中最⼤的⼀块,Java 堆是所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以Java堆还可以细分为:新⽣代和⽼年代:再 细致⼀点有:Eden空间、From Survivor、To Survivor空间等。进⼀步划分的⽬的是更好地回收内存, 或者更快地分配内存。

img

上图所示的 eden区、s0区、s1区都属于新⽣代,tentired 区属于⽼年代。⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden区i>Survivor 区后对象的初始年龄变为1),当它的年龄增加到⼀定程度(默认为15 岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 XX:MaxTenuringThreshold来设置

⽅法区

⽅法区与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把⽅法区描述为堆的⼀个逻辑部分,但 是它却有⼀个别名叫做 Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来。

⽅法区也被称为永久代。很多⼈都会分不清⽅法区和永久代的关系,为此我也查阅了⽂献。

⽅法区和永久代的关系

《Java虚拟机规范》只是规定了有⽅法区这么个概念和它的作⽤,并没有规定如何去实现它。那么,在不同的 JVM 上⽅法区的实现肯定是不同的了。 ⽅法区和永久代的关系很像Java中接⼝和类的关系,类实现了接⼝,⽽永久代就是HotSpot虚拟机对虚拟机规范中⽅法区的⼀种实现⽅式。 也就是说,永久代是HotSpot的概念,⽅法区是Java虚拟机规范中的定义,是⼀种规范,⽽永久代是⼀种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并没有永久带这⼀说法。

常⽤参数

JDK 1.8 之前永久代还没被彻底移除的时候通常通过下⾯这些参数来调节⽅法区⼤⼩

-XX:PermSize=N  //⽅法区(永久代)初始⼤⼩
-XX:MaxPermSize=N  //⽅法区(永久代)最⼤⼤⼩,超过这个值将会抛出 OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

相对⽽⾔,垃圾收集⾏为在这个区域是⽐较少出现的,但并⾮数据进⼊⽅法区后就“永久存在”了。

JDK 1.8 的时候,⽅法区(HotSpot的永久代)被彻底移除了(JDK1.7就已经开始了),取⽽代之是元空间,元空间使⽤的是直接内存。

下⾯是⼀些常⽤参数:

-XX:MetaspaceSize=N  //设置Metaspace的初始(和最⼩⼤⼩)
-XX:MaxMetaspaceSize=N  //设置Metaspace的最⼤⼤⼩

与永久代很⼤的不同就是,如果不指定⼤⼩的话,随着更多类的创建,虚拟机会耗尽所有可⽤的系统内 存。

为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?

整个永久代有⼀个 JVM 本身设置固定⼤⼩上线,⽆法进⾏调整,⽽元空间使⽤的是直接内存,受本机可⽤内存的限制,并且永远不会得到java.lang.OutOfMemoryError。你可以使⽤ MaxMetaspaceSize 标志设置最⼤元空间⼤⼩,默认值为 unlimited,这意味着它只受系统内存的限制。 -XX:MetaspaceSize 调整标志定义元空间的初始⼤⼩如果未指定此标志,则 Metaspace 将根据运⾏时的应⽤程序需求动态地重新调整⼤⼩。

当然这只是其中⼀个原因,还有很多底层的原因,这⾥就不提了。

运⾏时常量池

运⾏时常量池是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息外,还有常量池信息(⽤于存放编译期⽣成的各种字⾯量和符号引⽤)

既然运⾏时常量池时⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛 出 OutOfMemoryError 异常。

JDK1.7及之后版本的 JVM 已经将运⾏时常量池从⽅法区中移了出来,在 Java 堆(Heap)中开辟了⼀块区域存放运⾏时常量池。

image-20210504205233921

直接内存

直接内存并不是虚拟机运⾏时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部分内存 也被频繁地使⽤。⽽且也可能导致 OutOfMemoryError 异常出现。

JDK1.4 中新加⼊的 NIO(New Input/Output) 类,引⼊了⼀种基于通道(Channel) 与缓存区(Buffer) 的 I/O ⽅式,它可以直接使⽤ Native 函数库直接分配堆外内存,然后通过⼀个存储在Java 堆中的 DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样就能在⼀些场景中显著提⾼性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存⼤⼩以及处理器寻址空间的限制。

简述一下JVM的内存模型

1.JVM内存模型简介

JVM定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着JVM启动及销毁,另外一些区域的数据是线程性独立的,随着线程创建和销毁。jvm内存模型总体架构图如下:(摘自oracle官方网 站)

image-20210502150005913

JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大部分,线程私有区和共享区。下图是根据自己理解画的一个JVM内存 模型架构图:

image-20210502150016337

JVM内存分为线程私有区和线程共享区

线程私有区

1、程序计数器

当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线 程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA 方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。

2、虚拟机栈

线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法 调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈 的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出 OutofMemoryError。

使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:

image-20210502150043807

3、本地方法栈

与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同。

线程共享区

1、方法区

线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的代 码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃常量和 无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类的定义信息 不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收)

2、堆

存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的Eden 区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则OutOfMemoryError。

image-20210502150054703

img |

堆和栈的区别

栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆是存 储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连 续,会有碎片。

1、功能不同

栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量, 还是类变量,它们指向的对象都存储在堆内存中。

2、共享性不同

栈内存是线程私有的。

堆内存是所有线程共有的。

3、异常错误不同

如果栈内存或者堆内存不足都会抛出异常。

栈空间不足:java.lang.StackOverFlowError。堆空间不足:java.lang.OutOfMemoryError。

4、空间大小

栈的空间大小远远小于堆的。

什么时候会触发FullGC

除直接调用System.gc外,触发Full GC执行的情况有如下四种。

1. 旧生代空间不足

旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:

java.lang.OutOfMemoryError: Java heap space

为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

2. Permanet Generation空间满

PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较 多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:

java.lang.OutOfMemoryError: PermGen space

为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

3. CMS GC时出现promotion failed和concurrent mode failure

对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。

promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。

应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可 通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间

这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的 现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。

例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。

当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。

类⽂件结构

介绍⼀下类⽂件结构吧!

根据 Java 虚拟机规范,类⽂件由单个 ClassFile 结构组成:

image-20210504211248212

Class⽂件字节码结构组织示意图

image-20210504211305827

下⾯会按照上图结构按顺序详细介绍⼀下 Class ⽂件结构涉及到的⼀些组件。

  1. 魔数: 确定这个⽂件是否为⼀个能被虚拟机接收的 Class ⽂件。

  2. Class ⽂件版本 :Class ⽂件的版本号,保证编译正常执⾏。

  3. 常量池 :常量池主要存放两⼤常量:字⾯量和符号引⽤。

  4. 访问标志 :标志⽤于识别⼀些类或者接⼝层次的访问信息,包括:这个 Class 是类还是接⼝, 是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。

  5. 当前类索引,⽗类索引 :类索引⽤于确定这个类的全限定名,⽗类索引⽤于确定这个类的⽗类的全限定名,由于 Java 语⾔的单继承,所以⽗类索引只有⼀个,除了 java.lang.Object 之外,所有的 java 类都有⽗类,因此除了java.lang.Object外,所有 Java 类的⽗类索引都不为 0。

  6. 接⼝索引集合 :接⼝索引集合⽤来描述这个类实现了那些接⼝,这些被实现的接⼝将按 implents (如果这个类本身是接⼝的话则是 extends ) 后的接⼝顺序从左到右排列在接⼝索引集合中。

  7. 字段表集合 :描述接⼝或类中声明的变量。字段包括类级变量以及实例变量,但不包括在⽅法内部声明的局部变量。

  8. ⽅法表集合 :类中的⽅法。

  9. 属性表集合 : 在 Class ⽂件,字段表,⽅法表中都可以携带⾃⼰的属性表集合。

Java内存结构

方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内 存区域。

  • Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存 区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在 这里分配内存。
  • 方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
  • JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执 行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出 栈的过程。
  • 本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一 半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

说⼀下堆内存中对象的分配的基本策略

堆空间的基本结构:

image-20210504205643332

上图所示的 eden区、s0区、s1区都属于新⽣代,tentired 区属于⽼年代。⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden区i>Survivor 区后对象的初始年龄变为1),当它的年龄增加到⼀定程度(默认为15 岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 XX:MaxTenuringThreshold来设置。

另外,⼤对象和⻓期存活的对象会直接进⼊⽼年代。

image-20210504205657107

描述一下JVM加载class文件的原理机制?

VM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确 保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据 读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载 完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括 验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步 骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先 初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和 用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是 根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器 无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个 类加载器的说明:

  • Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar)
  • Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
  • System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加 载器。

Java对象创建过程

  1. JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)
  2. 为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”
  3. 将除对象头外的对象内存空间初始化为0
  4. 对对象头进行必要设置

image-20210504205327339

①类加载检查: 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那 必须先执⾏相应的类加载过程。

②分配内存: 在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。

内存分配的两种⽅式:(补充内容,需要掌握)

选择以上两种⽅式中的哪⼀种,取决于 Java 堆内存是否规整。⽽ Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

image-20210504205344299

内存分配并发问题(补充内容,需要掌握) 在创建对象的时候有⼀个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的 事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采⽤两种⽅式来保证线程安全: CAS+失败重试: CAS 是乐观锁的⼀种实现⽅式。所谓乐观锁就是,每次不加锁⽽是假设没有冲突⽽去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采⽤ CAS 配上失败重试的⽅式保证更新操作的原⼦性。 TLAB: 为每⼀个线程预先在Eden区分配⼀块⼉内存,JVM在给线程中的对象分配内存时,⾸先在TLAB分配,当对象⼤于TLAB中的剩余内存或TLAB的内存已⽤尽时,再采⽤上述的CAS进⾏内存分 配

③初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。

④设置对象头: 初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。

⑤执⾏ init ⽅法: 在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样 ⼀个真正可⽤的对象才算完全产⽣出来。

对象的访问定位有哪两种⽅式?

建⽴对象就是为了使⽤对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问⽅式有虚拟机实现⽽定,⽬前主流的访问⽅式有①使⽤句柄和②直接指针两种:

  1. 句柄: 如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;

image-20210504205536832

  1. 直接指针: 如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址。

image-20210504205552206

这两种对象访问⽅式各有优势。使⽤句柄来访问的最⼤好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,⽽ reference 本身不需要修改。使⽤直接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销。

类加载过程

知道类加载的过程吗?

类加载过程:加载y>连接y>初始化。连接过程⼜可分为三步:验证y>准备y>解析。

image-20210504211412126

那加载这⼀步做了什么?

类加载过程的第⼀步,主要完成下⾯3件事情:

  1. 通过全类名获取定义此类的⼆进制字节流

  2. 将字节流所代表的静态存储结构转换为⽅法区的运⾏时数据结构

  3. 在内存中⽣成⼀个代表该类的 Class 对象,作为⽅法区这些数据的访问⼊⼝

虚拟机规范多上⾯这3点并不具体,因此是⾮常灵活的。⽐如:"通过全类名获取定义此类的⼆进制字节 流" 并没有指明具体从哪⾥获取、怎样获取。⽐如:⽐较常⻅的就是从 ZIP 包中读取(⽇后出现的JAR、EAR、WAR格式的基础)、其他⽂件⽣成(典型应⽤就是JSP)等等。

⼀个⾮数组类的加载阶段(加载阶段获取类的⼆进制字节流的动作)是可控性最强的阶段,这⼀步我们可以去完成还可以⾃定义类加载器去控制字节流的获取⽅式(重写⼀个类加载器的 ⽅法)。数组类型不通过类加载器的loadClass()创建,它由 Java 虚拟机直接创建。

类加载器、双亲委派模型也是⾮常重要的知识点,这部分内容会在后⾯的问题中单独介绍到。

加载阶段和连接阶段的部分内容是交叉进⾏的,加载阶段尚未结束,连接阶段可能就已经开始了。

知道哪些类加载器?

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承⾃ java.lang.ClassLoader : 1.BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib ⽬录下的jar包和类或者或被 -Xbootclasspath 参数指定的路径中的所有类。 2.ExtensionClassLoader(扩展类加载器) :主要负责加载⽬录 ⽬录下的jar包和类,或被 系统变量所指定的路径下的jar包。 3.AppClassLoader(应⽤程序类加载器) :⾯向我们⽤户的加载器,负责加载当前应⽤classpath下的所有jar包和类。 双亲委派模型知道吗?能介绍⼀下吗? 双亲委派模型介绍 每⼀个类都有⼀个对应它的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤ 双亲委派模型 。即在类加载的时候,系统会⾸先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,⾸先会把该请求委派该⽗类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时,才由⾃⼰来处理。当⽗类加载器为null时,会使⽤启动类加载器 BootstrapClassLoader 作为⽗类加载器。

image-20210504211445569

每个类加载都有⼀个⽗类加载器

双亲委派模型带来了什么好处呢? 双亲委派模型保证了Java程序的稳定运⾏,可以避免类的重复加载(JVM 区分不同类的⽅式不仅仅根据类名,相同的类⽂件被不同的类加载器加载产⽣的是两个不同的类),也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为java.lang.Object类的话,那么程序运⾏的时候,系统就会出现多个不同的Object类。

如果我们不想⽤双亲委派模型怎么办

为了避免双亲委托机制,我们可以⾃⼰定义⼀个类加载器,然后重载loadClass()即可。

如何⾃定义类加载器?

除了BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承⾃java.lang.ClassLoader 。如果我们要⾃定义⾃⼰的类加载器,很明显需要继承ClassLoader

类的生命周期

类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如 下图;

  • 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
  • 连接,连接又包含三块内容:验证、准备、初始化。 1)验证,文件格式、元数据、字节码、符号引用验证; 2)准备,为类的静态变量分配内存,并将其初始化为默认值; 3)解析,把类中的符号引用转换为直接引用
  • 初始化,为类的静态变量赋予正确的初始值使用,new出对象程序中使用
  • 卸载,执行垃圾回收

简述Java的对象结构

Java对象由三个部分组成:对象头、实例数据、对齐填充。

对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线 程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的) 对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)

如何判断对象可以被回收?

判断对象是否存活一般有两种方式:

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数 为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
  • 从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

image-20210504205804118

Java的四种引用,强弱软虚

强引用

强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方

式:

String str = new String("str");

软引用

软引用在程序内存不足时,会被回收,使用方式:

// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,

// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T

SoftReference<String> wrf = new SoftReference<String>(new String("str"));

可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的

对象。

弱引用

弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:

WeakReference<String> wrf = new WeakReference<String>(str);

可用场景: Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是,

一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。虚引用

虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意

哦,其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被

用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue ,使用

例子:

PhantomReference<String> prf = new PhantomReference<String>(new
String("str"), new ReferenceQueue<>());

可用场景: 对象销毁前的一些操作,比如说资源释放等。 Object.finalize() 虽然也可以做

这类动作,但是这个方式即不安全又低效

上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用

( SoftReference 等)。

特别注意,在程序设计中⼀般很少使⽤弱引⽤与虚引⽤,使⽤软引⽤的情况较多,这是因为软引⽤可以 加速JVM对垃圾内存的回收速度,可以维护系统的运⾏安全,防⽌内存溢出(OutOfMemory)等问题的产⽣。

如何判断⼀个常量是废弃常量?

运⾏时常量池主要回收的是废弃的常量。那么,我们如何判断⼀个常量是废弃常量呢?

假如在常量池中存在字符串 "abc",如果当前没有任何String对象引⽤该字符串常量的话,就说明常量"abc" 就是废弃常量,如果这时发⽣内存回收的话⽽且有必要的话,"abc" 就会被系统清理出常量池。

如何判断⼀个类是⽆⽤的类?

⽅法区主要回收的是⽆⽤的类,那么如何判断⼀个类是⽆⽤的类的呢?

判定⼀个常量是否是“废弃常量”⽐较简单,⽽要判定⼀个类是否是“⽆⽤的类”的条件则相对苛刻许多。 类需要同时满⾜下⾯3个条件才能算是 “⽆⽤的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地⽅被引⽤,⽆法在任何地⽅通过反射访问该类的⽅法。

虚拟机可以对满⾜上述3个条件的⽆⽤类进⾏回收,这⾥说的仅仅是“可以”,⽽并不是和对象⼀样不使⽤了就会必然被回收。

JVM的永久代中会发生垃圾回收么?

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小 对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)

垃圾收集算法

GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。

  • 标记 -清除算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

这种垃圾收集算法会带来两个 明显的问题:

  1. 效率问题

  2. 空间问题(标记清除后会产⽣⼤量不连续的碎⽚)

image-20210504210319322

  • 复制算法,为了解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只 使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已 使用过的内存空间一次清理掉。

image-20210504210504346

  • 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清 理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

image-20210504210538223

  • 分代收集算法,当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不 同将内存分为⼏块。⼀般将java堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适 的垃圾收集算法。

    ⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制 成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分 配担保,所以我们必须选择标记清除”或“标记-整理”算法进⾏垃圾收集。。

HotSpot为什么要分为新⽣代和⽼年代?

主要是为了提升GC效率

常⻅的垃圾回收器有那些?

image-20210504210150802

如果说收集算法是内存回收的⽅法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进⾏⽐较,但并⾮要挑选出⼀个最好的收集器。因为知道现在为⽌还没有最好的 垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应⽤场景选择适合⾃⼰的垃圾 收集器。试想⼀下:如果有⼀种四海之内、任何场景下都适⽤的完美收集器存在,那么我们的HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。

  1. Serial收集器

Serial(串⾏)收集器收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( "Stop The World"),直到它收集结束。新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

img

虚拟机的设计者们当然知道Stop The World带来的不良⽤户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。

但是Serial收集器有没有优于其他垃圾收集器的地⽅呢?当然有,它简单⽽⾼效(与其他收集器的单线程相⽐)。Serial收集器由于没有线程交互的开销,⾃然可以获得很⾼的单线程收集效率。Serial收集器对于运⾏在Client模式下的虚拟机来说是个不错的选择。

  1. ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控制参数、收集算法、回收策略等等)和Serial收集器完全⼀样。

新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

image-20210504210925056

它是许多运⾏在Server模式下的虚拟机的⾸要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后⾯会介绍到)配合⼯作。

并⾏和并发概念补充:

  • 并⾏(Parallel) :指多条垃圾收集线程并⾏⼯作,但此时⽤户线程仍然处于等待状态。

  • 并发(Concurrent):指⽤户线程与垃圾收集线程同时执⾏(但不⼀定是并⾏,可能会交替执 ⾏),⽤户程序在继续运⾏,⽽垃圾收集器运⾏在另⼀个CPU上。

  1. Parallel Scavenge收集器 Parallel Scavenge 收集器类似于ParNew 收集器。 那么它有什么特别之处呢?
-XXé+UseParallelGC

使⽤Parallel收集器+   ⽼年代串⾏

-XXé+UseParallelOldGC

使⽤Parallel收集器+   ⽼年代并⾏

Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验)。所谓吞吐量就是CPU中⽤于运⾏⽤户代码的时间与CPU总消耗时 间的⽐值。 Parallel Scavenge收集器提供了很多参数供⽤户找到最合适的停顿时间或最⼤吞吐量,如果对于收集器运作不太了解的话,⼿⼯优化存在的话可以选择把内存管理优化交给虚拟机去完成也是⼀ 个不错的选择。

新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

image-20210504211006132

  1. Serial Old收集器

Serial收集器的⽼年代版本,它同样是⼀个单线程收集器。它主要有两⼤⽤途:⼀种⽤途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使⽤,另⼀种⽤途是作为CMS收集器的后备⽅案。

  1. Parallel Old收集器

Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

  1. CMS收集器

CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。它⽽⾮常符合在注重⽤户体验的应⽤上使⽤。

CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是⼀种 “标记-清除”算法实现的,它的运作过程相⽐于前⾯⼏种垃圾收集器来说更加复杂⼀些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;
  • 并发标记: 同时开启GC和⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以 GC线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐ 并发标记阶段时间短

  • 并发清除: 开启⽤户线程,同时GC线程开始对为标记的区域做清扫

image-20210504211031503

从它的名字就可以看出它是⼀款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下⾯三个 明显的缺点:

  • 对CPU资源敏感;

  • ⽆法处理浮动垃圾;

  • 它使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。
  1. G1收集器

G1 (Garbage-First)是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.

以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征.

被视为JDK1.7中HotSpot虚拟机的⼀个重要进化特征。它具备⼀下特点:

  • 并⾏与并发:G1能充分利⽤CPU、多核环境下的硬件优势,使⽤多个CPU(CPU或者CPU核⼼)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执⾏的GC动作,G1收集器仍然可以通过并发的⽅式让java程序继续执⾏。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独⽴管理整个GC堆,但是还是保留了分代的概 念。
  • 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集 器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是G1相对于CMS的另⼀个⼤优势,降低停顿时间是G1 和 CMS 共同的关注点, 但G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为M 毫秒的时间⽚段内。

G1收集器的运作⼤致分为以下⼏个步骤:

  • 初始标记
  • 并发标记

  • 最终标记

  • 筛选回收

G1收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的Region(这也就是它的名字Garbage-First的由来)。这种使⽤Region划分内存空间以及有优先级的区域回收⽅式, 保证了GF收集器在有限时间内可以尽可能⾼的收集效率(把内存化整为零)。

调优命令有哪些?

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo

  • jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap,JVM Memory Map命令用于生成heap dump文件
  • jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
  • jstack,用于生成java虚拟机当前时刻的线程快照。

  • jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

调优工具

常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

  • jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
  • jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
  • GChisto,一款专业分析gc日志的工具

Minor GC与Full GC分别在什么时候发生?有什么不同

新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC

image-20210504205728463

⼤多数情况下,对象在新⽣代中 eden 区分配。当 eden 区没有⾜够空间进⾏分配时,虚拟机将发起⼀次Minor GC。

  • 新⽣代GC(Minor GC):指发⽣新⽣代的的垃圾收集动作,Minor GC⾮常频繁,回收速度⼀般也⽐较快。
  • Major GC的速度⼀般会⽐Minor GC的慢10倍以上。

你知道哪些JVM性能调优

  • 设定堆内存大小

-Xmx:堆内存最大限制。

  • 设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代

-XX:NewSize:新生代大小

-XX:NewRatio 新生代和老生代占比

-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比

  • 设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC

JVM内存分哪几个区,每个区的作用是什么?

Java虚拟机主要分为以下一个区:

  • 方法区:
  1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类 型的卸载

  2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代 码等数据。

  3. 该区域是被线程共享的。

  4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有 动态性,也就是说常量并不一定是编 译时确定,运行时生成的常量也会存在这个常量池中。

  5. 虚拟机栈:

  1. 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建 一个栈帧,用于存储局部变量表、操 作数栈、动态链接和方法出口等信息。

  2. 虚拟机栈是线程私有的,它的生命周期与线程相同。

  3. 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址) 和对象引用,这个对象引用有可能是指 向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定

  4. 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来 访问,而是压栈和出栈的方式

  5. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持 方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。

  6. 本地方法栈

本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。

Java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作 。

  • 程序计数器

内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指 令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区 域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。

简述Java垃圾回收机制?

在Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一 个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不 足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。

  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。

  3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。

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

你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理 过程中有哪些收获?

常见的原因

  • 内存加载的数据量太大:一次性从数据库取太多数据;
  • 集合类中有对对象的引用,使用后未清空,GC不能进行回收; 代码中存在循环产生过多的重复对象;
  • 启动参数堆内存值小。

JDK 1.8之后Perm Space有哪些变动? MetaSpace⼤⼩默认是

⽆限的么? 还是你们会通过什么⽅式来指定⼤⼩?

JDK 1.8后用元空间替代了 Perm Space;字符串常量存放到堆内存中。MetaSpace大小默认没有限制,一般根据系统内存的大小。JVM会动态改变此值。

-XX:MetaspaceSize:分配给类元数据空间(以字节计)的初始大小(Oracle逻辑存储上的初始高水位,the initial high-water-mark)。此值为估计值,MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。

-XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。

跟JVM内存相关的几个核心参数图解

image-20210502150556420

如何启动系统的时候设置JVM的启动参数

image-20210502150611950

results matching ""

    No results matching ""