对象面试官系列之JVM--面试官看了都说好

1 Java内存模型(JMM)

概念:

所有变量都存储在主内存上,所有线程都可访问,线程对于变量的操作(赋值、读取等)必须在工作内存进行,操作完成首在写回主内存

扩展:

1. 工作内存:寄存器,高速缓存

2. 主内存:硬件的内存

3. 内存间操作:

对象面试官系列之JVM--面试官看了都说好_第1张图片

程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作(JVM可能对指令进行重排序,只能保证单线程顺序)

锁定规则:必须先对锁进行了释放操作,后面才能继续进行lock操作

volatile变量规则:对一个的写操作先行发生于对这个变量的读操作

传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

Start规则:threadB.start先行发生于B的操作

Join规则:B.join():B的操作先于B返回后的操作

2 jvm内存模型

线程私有:

2.1 程序计数器(唯一不会抛出OOM异常的)

当前线程所执行的字节码的行号指示器

2.2 Java虚拟机栈

a.有一个栈帧组成,包括局部变量表(用来存储方法中的局部变量);指向运行时常量池的引用(存储程序执行时可能用到常量的引用);操作数栈;方法返回地址。每次函数调用压入一个栈帧,return或者抛出异常弹出栈帧

b.会抛出StackOverFlowError和OutOfMemoryError异常

StackOverFlowError (栈溢出):栈的内存大小不允许动态扩展,线程请求栈的深度超过当前Java 虚拟机栈的最大深度(不断递归调用函数)

OutOfMemoryError(内存不足):栈的内存允许动态扩展,当线程请求栈时内存用完了,无法再动态扩展(不断新建新线程)

2.3 本地方法栈

线程共享:

2.4 堆

存放对象的实例

2.5 方法区

存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

JDK1.7前放在堆中作为永生代,1.8作为元空间放在直接内存中

2.6 运行时常量池

方法区的一部分,存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到运行时常量池中;字符串常量池1.7以后放在堆中,运行时常量池放在元空间中

对象面试官系列之JVM--面试官看了都说好_第2张图片

2.7 直接内存

3 对象创建步骤

3.1 类加载检查

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

3.2 分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种

扩展:

1. 内存分配的两种方式:

对象面试官系列之JVM--面试官看了都说好_第3张图片

2. 内存分配并发问题:

CAS+失败重试:CAS是乐观锁的一种实现方式。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。

TLAB(本地线程分配缓冲):为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配

3.3 初始化零值

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

3.4 设置对象头

对象头主要包括对象自身的运行时数据,比如哈希码,对象分代年龄等,还有类型指针,确定对象是哪个类的实例。

扩展(对象在堆中的存储布局):

1 对象头(如果对象是数组还需要存储数组长度):

1.1 对象自身的运行时数据

对象面试官系列之JVM--面试官看了都说好_第4张图片

2 实例数据:定义的各种类型的字段内容

3 对齐填充:保证对象的大小是8字节的整数倍

3.5 执行构造方法

4 对象的访问

4.1 句柄

Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含对象实例数据与类型数据各自的具体地址信息;

对象面试官系列之JVM--面试官看了都说好_第5张图片

4.2 直接指针

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

对象面试官系列之JVM--面试官看了都说好_第6张图片

5 类加载机制

5.1 加载

根据类的全限定名获取类的二进制字节流,并转化成方法区的运行时数据结构,然后生成一个对应的Class对象,作为方法区中该类各种数据的访问入口。(数组由虚拟机创建而不是类加载器)

扩展(Class文件内容):

1.4个字节的魔数:确定该文件能否被虚拟机接受

2.4个字节的版本号

3.常量池

4.2个字节的访问标志位,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型等

5.类索引、父类索引、接口索引

6.字段表、方法表、属性表

5.2 验证

确保class文件符合规范:文件格式验证;字节码验证;元数据验证;符号引用验证

5.3 准备

静态变量赋零值,静态常量赋初始值

5.4 解析

将常量池的符号引用替换为直接引用(可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄)的过程

5.5 初始化

初始化静态变量和静态代码块。静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问

6 类初始化时机

6.1 主动引用

a当程序创建一个类的实例对象;程序访问或设置类的静态变量(不是静态常量,会在编译时被加载到运行时常量池);程序调用类的静态方法

b对类进行反射调用的时候

c当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

d当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

6.2 被动引用(不会触发)

1.通过子类引用父类的静态字段,不会导致子类初始化。

2.通过数组定义来引用类,不会触发初始化(数组由虚拟机直接创建)。

3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

7 类加载器

7.1 分类

启动类加载器:/lib目录下的jar包和类

扩展类加载器:/lib/ext目录下的jar包和类

应用程序类加载器:加载当前应用classpath下的所有jar包和类

7.2 双亲委派机制

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载

扩展:

1 优点:避免类的重复加载;避免核心类被篡改

2 SPI利用线程上下文加载器,启动类加载器可以获得子类加载器加载的类

Tomcat:自定义加载器,tomcat和应用共享的加载器;tomcat私有加载器;应用共享加载器;应用私有加载器

对象面试官系列之JVM--面试官看了都说好_第7张图片

8 GC

8.1 如何分配内存和回收

8.1.1 对象优先在eden区分配

对象在新生代中eden区分配。当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。先把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果ServicorTo不够位置了就放到老年区),同时把这些对象的年龄+1;然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区

8.1.2 大对象直接进入老年代(字符串,数组)

8.1.3 长期存活的对象进入老年代

Eden区域对象经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。

8.1.4 动态对象年龄判定

如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代。

8.1.5 空间分配担保

在发生Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果不成立的话虚拟机会查看是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC;如果小于,或者不允许冒险,那么就要进行一次Full GC。

8.1.6 Full GC的触发条件

对于Minor GC,其触发条件非常简单,当Eden空间满时,就将触发一次Minor GC。而Full GC则相对复杂,有以下条件:

1.调用System.gc()

只是建议虚拟机执行Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2.老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过-Xmn虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过-           XX:MaxTenuringThreshold调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3.空间分配担保失败

使用复制算法的Minor GC需要老年代的内存空间作担保,如果担保失败会执行一次Full GC。

4. Concurrent Mode Failure

执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是GC过程中浮动垃圾过多导致暂时性的空间不足),便会报Concurrent Mode Failure错误,并触发Full GC。

5. JDK 1.7及以前的永久代空间不足

8.2 如何判断是否是垃圾

8.2.1 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。缺点:对象循环引用时无法被回收

8.2.2 可达性分析算法

当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的。

扩展:

1.可作为GC Roots的对象:(当前存活的对象)

1.虚拟机栈(栈帧中的本地变量表)中的引用的对象;

3.方法区中常量引用的对象(字符串常量池的引用);

4.Native方法引用的对象

2.如何获取GC Roots

采用一个OopMap的数据结构来记录系统中存活的“GC Roots”,在类加载完成的时候,虚拟机就把对象内什么偏移量上是什么类型的数据计算出来保存在OopMap,也会在安全点时记录栈和寄存器中哪些位置是引用

3.如何解决跨代引用

在新生代中建立记忆集(Remembered Set)的数据结构,比如卡表;卡表是一个字节数组,数组元素为一个内存区域,只要内存区域存在跨代指针,该元素就为1,就会进行扫描(用写屏障进行数据更新)

4.并发扫描对象消失问题

增量更新:黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次(CMS)

原始快照(SATB):当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次(G1)

8.2.3 强,软,弱,虚引用

强引用:绝不会回收

软引用:内存空间足够,垃圾回收器就不会回收它(适合做缓存)

弱引用:检测到只具有弱引用的对象都会回收它的内存(适合做队列)

虚引用:任何时候都会被回收,必须和引用队列联合使用,程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收

8.2.4 不可达的对象并非“非死不可”

可达性分析法中不可达的对象先判断是否有必要执行finalize方法。当对象没有覆盖finalize方法,或finalize方法已经被虚拟机调用过时,虚拟机将直接回收。被判定为需要执行的对象将会被放在一个队列中,除非在finalize方法中重新与引用链建立联系,否则直接回收。

8.2.5 回收方法区

常量:常量池常量中没有被引用

类:

1.Java堆中不存在该类的任何实例。

2.加载该类的类加载器已经被回收。

3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

8.3 垃圾回收算法

8.3.1 标记-清除算法

缺点:效率,标记清除后会产生大量的碎片

8.3.2 复制算法

将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收

8.3.3 标记-整理算法

标记后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

8.3.4 分代收集算法

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

8.4 垃圾收集器

8.4.1 串行收集器

新生代复制,老年代标记整理;单线程收集,且其他线程暂停直到结束

8.4.2 并行收集器

新生代复制,老年代标记整理;同串行收集器,支持多线程并行收集

8.4.3 吞吐量优先收集器

复制,可以通过两个参数控制吞吐量(用户代码运行时间/用户代码运行时间+gc时间)(垃圾收集最大停顿时间+直接设置吞吐量大小);jdk1.9之前默认收集器(吞吐量优先收集器)

8.4.4 Serial Old收集器(串行收集器的老年代版本,标记整理)

8.4.5 Parallel Old收集器(吞吐量优先收集器收集器的老年代版本,标记整理)

8.4.6 CMS收集器

初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快;

并发标记:同时开启GC和用户线程,标记所有可达的对象

重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,会暂停其他线程(增量更新)

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

扩展(缺点):

1.吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高。

2.无法处理浮动垃圾。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次GC时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,触发fullGC;这时虚拟机将临时启用Serial Old来替代CMS。

8.4.7 G1收集器

将连续的Java堆划分为多个大小相等的独立区域(Region),Humongous区域,专门用来存储大对象;G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region;jdk1.9默认

初始标记

并发标记:扫描后重新处理SATB记录下的在并发时有引用变动的对象

最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录

筛选回收:首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。需要暂停用户线程,采用复制算法。

扩展(Stop the world):

安全点:当垃圾收集需要中断线程的时候,设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起(方法调用、循环跳转、异常跳转)

你可能感兴趣的