JVM必知必会

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

1.内存模型以及分区,需要详细到每个区放什么?

JVM必知必会_第1张图片

虚拟机将所管理的内存分为以下几个部分:

  • 程序计数器 记录的是正在执行的虚拟机字节码指令的地址,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成,是当前线程所执行的字节码的行号指示器,Java虚拟机的多线程实际上是线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,因此为了线程切换后能恢复到正确的执行位置,每条线程处理器都需要有一个独立的程序计数器,互不影响,独立存储线程私有的内存,且是JVM中唯一没有规定OOM情况的区域。
  • 虚拟机栈 线程私有、与线程生命周期相同描述Java方法执行的内存模型,存放局部变量表、操作数栈、动态链接、方法出口等信息,我们常说的"栈"即指虚拟机栈或者说其中的局部变量表部分。局部变量表中存放:编译期可知的各种基本数据类型、对象引用类型等。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机栈扩展时无法申请到足够的内存,将会抛出OOM异常。
  • 本地方法栈 与虚拟机栈作用非常相似,虚拟机栈为Java方法服务,本地方法栈为Native方法服务。
  • Java堆 所管理内存中最大的一块,线程共享,唯一目的是存放对象实例,垃圾收集器管理的主要区域,如果在堆中没有内存完成实例分配,并且堆再也无法扩展时,将会抛出OutOfMemoryError异常。
  • 方法区 (非堆) 线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译的代码等数据。对于HotSpot虚拟机方法区被称为:永久代(Permanent Generation),原因是使用永久代实现方法区而已。
  • 运行时常量池  方法区一部分,存放编译期生成的各种字面量和符号引用。字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名,字段名称和描述符,方法名称和描述符。运行时常量池在JDK1.6及之前版本的JVM中是方法区的一部分,而在HotSpot虚拟机中方法区放在了”永久代(Permanent Generation)”。所以运行时常量池也是在永久代的。 

在Java8中,永久代已经被移除,被一个称为“元数据区”(Metaspace)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

  • 直接内存(堆外内存)机器内存中,不属于堆内存的部分即为堆外内存。堆外内存会溢出,并且其垃圾回收依赖于代码显式调用System.gc()。考虑使用缓存时,本地缓存是最快速的,但会给虚拟机带来GC压力,使用硬盘或者分布式缓存的响应时间会比较长,这时候堆外缓存会是一个比较好的选择。存在两种分配堆外内存的方法,Unsafe和NIO ByteBuffer,也可以使用Ehcache,Ehcache支持堆内缓存、堆外缓存、磁盘缓存、分布式缓存。

2.堆里面的分区:Eden、From Survivor、To Survivor

JVM必知必会_第2张图片

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。 

(1)大多情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC;(2)大对象(需要大量连续内存空间的对象)直接进入老年代;(3)长期存活对象将进入老年代。

Minor GC(新生代GC):在新生代的垃圾收集动作,比较频繁,回收速度较快,采用复制算法。

Full GC(老年代GC,Major GC):发生在老年代的GC,速度较慢,采用标记-清除算法等。

回收过程:

当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳(上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。

注:有的资料还有持久代的概念,存储类信息、常量、静态变量、类方法。

3.内存分配与回收策略

自动内存管理解决的主要问题:给对象分配内存以及回收分配给对象的内存。内存分配规则并非完全固定,取决于当前使用的垃圾回收器组合还有虚拟机内存参数设定。以下是普遍的内存分配规则:

  • 对象优先在Eden分配,如果Eden没有足够空间分配时,将发起一次Minor GC。
  • 大对象直接进入老年代,大对象是指需要大量连续内存空间的Java对象,如很长的字符串和数组。
  • 长期存活的对象将进入老年代,对象年龄计数器判断,为适应不同程序的运行状况,如果在Survivor空间相同年龄所有对象大小的总和大于Survivor的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保,如果Minor GC存活后的对象突增,远远高于平均值的话,将会导致老年代内存分配担保失败,此时会触发Full GC(具体可以根据参数设置是否允许冒险,如果允许,则根据平均值进行Minor GC尝试,如果不允许冒险则会进行一次Full GC)。

4.对象的创建方法,对象的内存分配,对象的访问定位

对象的创建

(1)检查常量池是否可以定位到一个类的符号引用,检查是否被加载、解析和初始化过

(2)加载检查通过后,为新生对象分配内存(内存分配方式:指针碰撞、空闲列表)

(3)内存分配完成后,初始化零值

(4)对对象进行必要的设置,如对象是哪个类的实例、元数据信息、对象的哈希码、

GC分代信息,这些信息存放在对象头中。

(5)执行方法,把对象按照程序员的意愿进行初始化。

对象的内存布局

三块区域:对象头(对象自身运行时数据;类型指针->对象指向它的类元数据的指针,用于确定该对象属于哪个类的实例)、实例数据(对象真正存储的有效信息)、对齐填充(起到占位符的作用)。

对象的访问定位

通过栈上的reference数据来操作堆上的具体对象。主流的访问方式有:使用句柄、直接指针。

(1)使用句柄:划分内存作为句柄池,reference中存储对象的句柄地址,最大的好处是存储的句柄地址是稳定的,在对象移动时reference本身不需要修改。

(2)使用直接指针:reference中存储的直接就是对象对象地址,速度更快,节省了一次指针定位的时间开销。HotSpot采用。

5.GC的两种判定方法(对象已死吗)

引用计数法:通过引用计数器实现,任何时刻计数器为0的对象就是不可能再被使用的。实现简单,判定效率很高,但很难解决对象之间的互相循环引用问题。

可达性分析算法:通过一系列的称为“GC ROOTS”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC ROOTS没有任何引用链相连时(图论的观点,就是从GC ROOTS到这个对象不可达),则证明对象是不可用的。

6.GC的垃圾收集算法

标记-清除算法:分为两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。存在的两个问题:效率问题;产生大量不连续的内存碎片。

复制算法:每次只使用其中一块,当一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。在存活对象不多的情况下,性能高,能解决内存碎片问题。常用于回收新生代。万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间,而老年代由于没有其额外内存空间进行分配担保,一般不采用复制算法。

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

分代收集算法:根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率较高、没有额外空间对它进行分配担保,就必须使用后面两种算法来进行回收。

7.GC收集器有哪些?

Serial垃圾收集器(单线程、复制算法):Serial 是一个单线程的收集器,它不但只会使用一个CPU或一条线程去完成垃圾收集工 作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。是java 虚拟机运行在Client模式下默认的新生代垃圾收集器。

ParNew垃圾收集器(Serial+多线程),Serial收集器的多线程版本,使用复制算法,是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。

Parallel Scavenge 收集器(多线程复制算法、高效)

SerialOld收集器(单线程标记整理算法)

ParallelOld收集器(多线程标记整理算法)

CMS收集器(多线程标记清除算法:一种以获取最短回收停顿时间为目标的收集器,在重视服务的响应速度、注重用户体验的场景下非常适用。基于“标记-清除”算法,特点是:并发收集、低停顿。具体分为四个阶段:初始标记->并发标记->重新标记->并发清除。

初始标记和重新标记两个步骤需要STW,sun官方文档称作并发低停顿收集器,主要有以下局限性:(1)对CPU资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是会占用一部分线程或者说CPU资源导致应用程序变慢,总吞吐量降低,默认启动的回收线程数是(CPU数量+3)/4。(2)无法处理浮动垃圾,运行期间预留的内存无法满足程序需要,出现Concurrent Mode Fail, 可能导致另一次Full GC的发发生。(3)内存碎片,可以牺牲停顿时间开启内存碎片的合并整理过程。

G1收集器:面向服务端应用的垃圾收集器,可能代替CMS收集器,特点:并行与并发分代收集(可以不需要其他收集器配合就能独立管理整个堆,采用不同的方式分代收集),空间整合(整体上是“标记-整理”算法实现,局部上是“复制”算法实现,不会产生内存空间碎片),可以预测的停顿时间(建立可预测的停顿时间模型)。具体可以分为四个阶段:初始标记->并发标记->最终标记->筛选回收。

引入分区的思路,弱化了分代的概念,使用G1垃圾收集器时,Java堆的内存布局与其他收集器有很大的差别,它将整个堆划分对多个大小相等的独立区域(region),新生代、老年代的概念依然保留,不再是物理隔离而是一部分region(不需要连续)的集合,回收时则以分区为单位进行回收。

G1的设计原则是"首先收集尽可能多的垃圾"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是使用启发式算法,跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,优先回收价值最大的region(这是garbage first名字的由来)。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小。

G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

8.Minor GC与Full GC分别在什么时候发生?

Minor GC(新生代GC):在新生代的垃圾收集动作,比较频繁,回收速度较快,采用复制算法。

Full GC(老年代GC,Major GC):发生在老年代的GC,速度较慢,采用标记-清除算法等。

9. 几种常用的内存调试工具:jmap、jstack、jconsole

jmap:用于生成堆转储快照(heap dump或dump文件),还可以查询finalize队列,java堆和永久代的详细信息,如空间使用率、当前使用的收集器等。

jstack:java堆栈跟踪工具,生成虚拟机当前时刻的线程快照,主要目的在于定位线程长时间停顿的原因,如出现死锁、死循环、请求外部资源导致的长时间等待。线程出现停顿的时候通过jstack查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么或者等待什么资源。

jconsole:可视化监视、管理工具,内存监控(相当于可视化的jstat命令,用于监视受收集器管理的虚拟机内存的变化趋势),线程监控(相当于可视化得jstack命令,遇到线程停顿时可以进行监控分析)

10.类加载的五个过程:加载、验证、准备、解析、初始化

类加载或者初始化的三个步骤:加载、连接、初始化

(1)加载:将类的class文件读入内存,并为之创建一个java.lang.Class对象,类的加载由类加载器完成。

(2)连接:连接阶段负责把类的二进制数据合并到JRE中,具体又分为如下三个阶段:验证,验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致;

准备,类准备阶段负责为类的类变量分配内存,并设置默认初始值。

解析,将类的二进制数据中的符号引用替换为直接引用

(3)初始化:虚拟机负责对类初始化,主要就是对类变量进行初始化。假设这个类还没有被加载和连接,则程序先加载并连接这个类,假设该类的直接父类还没有被初始化,则先初始化其直接父类,假设类中有初始化语句,则系统依次执行这些初始化语句。

11.双亲委派模型

BootstrapClassLoader(根类加载器)、ExtensionClassLoader(扩展类加载器)、ApplicationClassLoader(应用程序类加载器或者叫系统类加载器)

(1)根类加载器:也被称为引导(原始)类加载器,负责加载Java的核心类,并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。

(2)扩展类加载器,负责加载JRE的扩展目录中JAR包的类,通过这种方式,就可以为java扩展核心类以外的新功能,只要把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。

(3)应用程序类加载器,负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。如果没有特别指定,用户自定义的类加载器都以系统类加载器作为父加载器。

如何创建并使用自定义的类加载器?

继承ClassLoader并重写其findClass()方法。

URLClassLoader---系统类加载器和扩展类加载器的父类(继承关系),既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的父加载器都是如此,因此所有的请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。双亲委派模型对于保证JAVA程序的稳定运作很重要。

12. 分派:静态分派与动态分派。

(暂时跳过)

13.Java内存模型JMM

定义了一些规则,保证多个线程间可以高效地、正确地协同工作,关键的工作围绕多线程的原子性、可见性和有序性展开。

(1)原子性指一个操作不可被中断

(2)可见性是指一个线程修改了共享变量的值,其他线程是否能知道这个修改

(3)有序性是指程序在执行的过程中会发生指令重排,指令重排可以保证串行语义的一致性,但不能保证多线程下的语义也一致。

*为何进行指令重排?

尽量少的中断流水线,提高CPU性能。

哪些指令不能进行重排?(Happen-before规则)

(1)程序顺序原则:一个线程内保证语义的串行性

(2)volatile规则:volatile变量的写先于读,保证了volatile变量的可见性

(3)锁规则:解锁(unlock)先于加锁(lock)

......(详见:《实战Java高并发程序设计》)

以上原则保证了指令重排不会破坏原有的语义结构。

14.JAVA 四中引用类型

  • 强引用

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之 一。

  • 软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

  • 弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM 的内存空间是否足够,总会回收该对象占用的内存

  • 虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是对象在被收集器收集时收到一个系统通知

15. 逃逸分析

浅谈HotSpot逃逸分析

 

 

 

转载于:https://my.oschina.net/u/2939155/blog/911133

你可能感兴趣的:(JVM必知必会)