Skip to content

Latest commit

 

History

History
392 lines (209 loc) · 25.5 KB

JVM.md

File metadata and controls

392 lines (209 loc) · 25.5 KB

01.JVM中有哪几块内存区域?Java 8 之后对内存分代做了什么改进?

JDK1.8之前:

image-20200706212721634

JDK 1.8:

image-20200706212813342

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存(非运行时数据区的一部分)

1.程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条所需执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。另外,为了线程切换后恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,个线程之间计数器互不影响,独立存储,我们称这类内存区为“线程私有”的内存。

从以上介绍得知程序计数器主要有两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程指令的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了。

2.Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,生命周期与线程相同,随着线程创建而创建,死亡而死亡(线程周期:新建(New),就绪(Runnable),运行(Running),阻塞(Blocked),死亡(Dead)),描述的Java方法执行的内存模型,每次方法调用的数据都是通过栈传递。

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

局部变量表主要存放编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或者其他与此对象相关位置)。

java虚拟机栈会出现两种异常:StackOverFlowError和OutOfMemoryError:

  • StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候就抛出此异常。
  • OutOfMemoryError:若Java虚拟机栈的内存大小允许扩展,且当前请求占内存用完了,无法在动态扩展了,此时抛出OutOfMemoryError异常。

扩展:那么方法/函数如何调用?

Java栈可用类比数据结构中栈,Java栈中保存的主要内容是栈帧,每次函数调用都会有一个相应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。

Java方法有两种返回方式:

  1. return语句
  2. 抛出异常

不管是那种返回方式都会导致栈帧被弹出。

3.本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,二本地方法栈则为虚拟机使用到的Native方法服务,在HotSpot虚拟机中和Java虚拟机栈合二为一。

本地方法被执行的时候在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链、出口信息。

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

4.堆

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

Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本采用的是分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致店:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好的回收内存,或者是更快速的分配内存

image-20200707114019416

上图所示的eden区、s0区、s1区都属于新生代,tentired属于老年代。大部分情况下,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入S1或者S0,并且对象的年龄还会加1(eden区->survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15)就会被晋升到老年代中。对象到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。(两个Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。)

5.方法区

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

方法区也被称为永久代,两者关系如下:

方法区和永久代的关系就像Java中接口和类的关系,类实现了接口,永久代就是对方法区的一种实现方式。JDK1.8之后永久代被彻底移除,取而代之是元空间,元空间使用使用的是直接内存

为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢? 整个永久代有一个JVM本身设置固定大小,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError.你可以使用-XX: MaxMetaspaceSize标志设置最大元空间大小,默认值为unlimited,这意味着它只受系统内存的限制。-XX: MetaspaceSize 调整标志定义元空间的初始大小,如果未指定此标志,则Metaspace将根据运行时的应用程序需求动态地重新调整大小。

运行时常量池

运行时常量池时方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译器生成的各种字面量和符号引用),JDK1.7之后JVM已经将运行时常量池从方法区移了出来,在Java堆(Heap)中开辟了一块区域存放运行时常量池。换地方后则不受方法区内存的限制,避免了当常量池无法申请内存时抛出OutOfMemoryError异常

image-20200707151706708

直接内存

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

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

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

02.你知道JVM是如何运行起来的吗?堆内存中对象的分配的基本策略?

堆空间的基本结构:

image-20200707160926693

新生代:eden区、s0区、s1区 默认大小分配8:1:1

老年代:tentired区

Minor Gc(新生代垃圾回收)和Full/Major Gc(老年代垃圾回收)有什么不同?

大多数情况下,对象在新生代中eden区分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor Gc。

  • 新生代GC:指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般比较快
  • 老年代Gc:指发生在老年代的GC,出现Major GC经常会伴随至少一次Minor GC(并非绝对),Major GC的速度一般会比MinorGC慢10倍以上。

image-20200707161155372

03.说说JVM在哪些情况下会触发垃圾回收可以吗?

  1. 执行 system.gc()的时候
  2. 老年代空间不足,一次Full GC 之后,然后不足 会触发 java.outofmemoryError:java heap space
  3. minor之后 survior survivor放不下,放入老年代,老年代也放不下,触发FullGC, 或者新生代有对象放入老年代,老年代放不下,触发FullGC
  4. 新生代晋升为老年代时候,老年代剩余空间低于新生代晋升为老年代的速率,会触发老年代回收
  5. new 一个大对象,新生代放不下,直接到老年代,空间不够,触发FullGC

如何避免频繁GC

不要频繁的new 对象;不要显示的调研system.gc();不要使用Long Integer 尽量使用基本类型;少用静态变量 不会回收;可以使用null 进行回收

导致Full GC几种情况

  • 新生代设置过小,导致新生代GC发生频繁,增大系统消耗。二是大对象直接进入了老年代占据了空间而发生
  • 新生代设置过大导致老年代过小(堆总量一定),从而诱发Full GC。
  • Survivor过小,导致对象直接从Eden区到老年代区
  • Survuvir过大,导致eden过小增加GC频率 (一般来说新生代占整个堆得1/3比较合适)

04.对象什么时候转移到老年代?

1、长期存活对象(s0与s1之间转移次数达到15次)

2、S区放不下的对象

3、大对象

05.如何判断对象是否死亡(两种方法)?

对堆垃圾回收前的第一步就是要判断哪些对象已经死亡。

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能在被使用的。

可达性分析算法

这一算法基本思想就是通过一系列的成为“GC Roots”的对象作为起点,从这些结点开始向下搜索,结点所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连的话,证明此对象是不可用的。

image-20200708214858914

06.垃圾收集算法有哪些,以及各自的特点?

标记-清除算法

算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后同一回收所有被标记的对象。他是最基础的收集算法,后续算法都是对其不足改进得到的。这种算法有两个明显问题:

  1. 效率问题
  2. 空间问题(标记清除后产生大量不连续的碎片)

图解:

image-20200707213424565


复制算法

为了解决效率问题,“复制”收集算法出现了。他可以将内存分为大小相同的两块。每次使用其中一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后把使用的空间一次性清理掉。这样就使用每次的内存回收都是对内存区间的一半进行回收。

image-20200707213758182


标记-整理算法

根据老年代的特点而设计的算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象响一端移动,然后直接清理掉端边界以外的内存。

image-20200707220251709


分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新思想,只是根据对象存活周期的不同将内存分为几块。一般将Java堆分为新生代和老年代,这样可以根据年代的特点选择合适垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制就可以完成每次垃圾收集。而老年代的对象存活几率时比较高的,而且额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或者“标记整理”算法来进行垃圾收集。

07.HotSpot为什么要分为新生代和老年代?

主要为了提升GC效率。上面的分代收集算法已经很好的解释了这个问题。

08.常用的垃圾回收器都有什么(七种)?

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

img

image-20200708151207488

image-20200708155345524

注意:serial old 后来已经被弃用。

Serial收集器(串行)

Serial(串行)收集器收集是最基本、历史最悠久的垃圾收集器了。是一个单线程收集器,它的“单线程”的意义不仅仅意味着他只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在垃圾收集工作的时候必须暂停其他所有工作线程(Stop The World),直到它收集结束。这一缺点带来不良用户体验,但他也有优点简单高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器适用于运行在Client模式下的虚拟机。

对应JVM参数是:-XX:+UseSerialGC 开启后Serial(新生代)+Serial Old(老年代)的收集器组合。新生代采用复制算法,老年代采用标记-整理算法。

image-20200707231624036

Serial Old收集器

Serial收集器的老年代版本,单线程收集器。两大用途:一适用于JDK1.5以及以前的版本中于Parallel Scavenge收集器搭配使用,另一种作为CMS收集器的后备方案。18.之后弃用。


ParNew收集器(并行)

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等)和Serial收集器完全一样。ParNew收集器适用于运行在Server模式下的虚拟机。

image-20200707232104137

对应JVM参数:-XX:+UseParNewGC 启用ParNew收集器,只影响新生代,不影响老年代。

开启上述参数后:ParNew+Serial Old 收集器组合 (JDK1.8后二者不推荐组合使用)。新生代采用复制算法,老年代采用标记-整理算法。

并行和并发概念补充

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程扔出去等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能回事交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。

Parallel Scavenge收集器(并行 JDK8 默认回收器)

Parallel Scavenge收集器类似于ParNew收集器,在新生代和老年代都是多线程。

Parallel Scavenge收集器关注点时吞吐量(高效率的利用CPU)。CMS等垃圾收集器关注点更多是用户线程的停顿时间(提高用户体验)。吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间) (程序运行100分钟,垃圾收集时间1分钟 ,则吞吐量位99%)。与ParNew区别:有一个自适应调节策略(设置停顿时间及吞吐量 )——虚拟机根据当前系统。新生代采用复制算法,老年代采用标记-整理算法。

image-20200707232945832

常用JVM参数:-XX:+UseParallelGC或者-XX+UseParallelOldGC(可相互激活)使用Parallel Scavenge。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合都和使用。


CMS收集器(并发标记清除)

CMS(Concurrent Mark Sweep 并发标记清除)收集器是一种以获取最短回收停顿时间为目标的收集器。注重用户体验。

CMS收集器时HotSpot虚拟机第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字 Mark Sweep可以看出,CMS是一种“标记-清除”算法实现的,运作过程分为四个部分:

  • 起始标记:暂停所有的其他线程,并记录下直接与root相连的线程的对象,速度很快;
  • 并发标记:同事开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段,闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法宝成可达性分析的实时性。所以这个算法里会跟踪记录这这发生引用更新的地方。
  • 重新标记:重新标记为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分的标记记录,需要暂停所有线程。
  • 并发清除:开启用户线程,同事GC线程开始对位标记的区域做清扫。

image-20200707234706905

image-20200708163507773

主要优点:并发收集、低停顿(节约时间)。但也有缺点:

  • 对CPU压力大,一边垃圾回收 一边运行线程
  • 它使用的回收算法“标记-清除”算法会导致收集结束时产生大量空间碎片。

QQ图片20200708165116


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处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度位M毫秒的时间片段内。

G1收集器的运作步骤如下:

image-20200708180700355

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

image-20200708180616733

09.类加载过程

类加载过程:加载->链接->初始化。接着过程又分为三步:验证->准备->解析

img

加载(主要完成3件事):

  1. 通过全类名获取定义此类的二进制字节流。
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  3. 在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口。

image-20200708224757276

10.类加载器有哪些?

image-20200708225216026

11.双亲委派模型?

image-20200708225347928

image-20200708225422859

image-20200708225540561

12.Java对象的创建过程(重要)

image-20200708225920627

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

②分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种,选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 内存分配的两种方式: (补充内容, 需要掌握)

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

img

在创建线程时,保证线程安全两种方式:

  • CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而时假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
  • TLAB:为每个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或者TLAVB的内存已经用尽时,再采用上述的CAS进行内存分配

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

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

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

13. 对象的访问定位又哪两种方式?

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

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

    image-20200708231202589

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

image-20200708231214843

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

14.深拷贝和浅拷贝

Java深拷贝与浅拷贝

浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,

深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,

使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。

浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。

深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

15.Java中的引用类型?

  • 强引用:发生 gc 的时候不会被回收。
  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。