应届生面试要点总结(3)JVM相关

Java虚拟机运行时数据区分为以下几个部分:方法区、虚拟机栈、本地方法栈、堆、程序计数器。如下图所示:

Java堆:线程共享的,唯一目的就是用于存放对象实例,是垃圾收集器管理的主要区域;Java堆可以处于物理上不连续的内存空间中,只需要逻辑上连续即可,就像磁盘上空间存储文件一样。堆上也有可能有部分区域是线程私有的,线程共享的堆中可能划分出多个线程私有的分配缓冲区TLAB。

Java虚拟机栈:线程私有的,每个方法在执行的同时都会创建一个栈帧用于存储局部变量等,局部变量表存放了编译器可知的各种基本数据类型和对象引用;每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法栈:和虚拟机栈类似,不过它是为Native方法服务;

程序计数器:线程私有的,可以看作是当前线程所执行的字节码的行号指示器,以便线程切换后恢复执行使用;唯一没有OutOfMemoryError的区域。

方法区:线程共享的,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;该区域的内存回收主要是针对常量池的回收和类型的卸载(特别是要注意一些动态字节码框架和自定义ClassLoader的场景下);在HotSpot里经常被称为永久代,在Java 8里已被废除了,被元空间取代;

 

常量池:byte, short, int, long, char, boolean包装类实现了常量池(float, double没有)。byte, short, char相同级别,不能相互转换,但可强转。

常量池主要存放两大类常量:字面量、符号引用。字面量相当于java语言层面常量的概念,符号引用包括类和接口的全限定名,字段名称和描述名称,方法名称和描述符。

运行时常量池有动态性,java语言并不要常量一定只有在编译时产生,也就是并非预置入class文件中的常量池的内容才能放入常量池,运行期间有新的常量也可以放入池中,比如String的intern方法。优点:对象共享,节约内存空间,节省运行时间。

 

Hotspot虚拟机对象的创建

首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有没有就执行下一步。

检查通过后,虚拟机会为新生的对象分配内存,主要由两种内存分配策略,一种是指针碰撞,一种是空闲列表。所谓指针碰撞就是把Java堆中的内存一分为二,一边是所有用过的内存(这部分内存不能被分配了),一边是空闲的内存,是可以被分配的,这样的话,在可用于不可用的内存之间会有一个分割点指示器,那么为对象分配内存实际上就是从这个分界点指示器往空闲内存的一边拨动一段空间就可以了。而空闲列表则没有这个假设,已使用的内存与空闲内存可能是交叉在一起的,那么使用指针碰撞的方式分配内存就会产生问题,但是虚拟机维护着一张列表,这张列表记录了哪些区域的内存是可用的,那么在分配内存的时候就从选择可以容纳对象要求大小的内存区域分配给这个对象。

 

在并发情况下给对象分配内存是线程不安全的,为了解决这个问题,有两种方案:

对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试方法保证更新操作的原子性;把内存分配的动作按照线程划分在不同的空间上进行。即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否用TLAB可以通过-XX:+/-UseTLAB设定。

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

接下来虚拟机需要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。根据虚拟机当前的运行状态的不同,例如是否用偏向锁等,对象头会有不同的设置方式。

上面步骤进行完毕之后,在虚拟机的角度,一个新的对象就已经产生了。

 

对象的内存布局,主要包括三部分的信息:对象头、实例数据和对齐填充。

对象头又包括两部分信息,第一部分用于存储对象自身的运行时数据,比如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等;第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。当然,类型指针不是必须的。可能有疑问,如果没有类型指针,怎么知道这个对象是哪个类的实例呢?答案是知道对象属于哪个类并不一定需要通过对象本身。若对象是一个java数组,则对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组元数据中无法确定数组的大小。

实例数据部分是对象真真好存储的有效信息,也是程序代码中所定义的各种类型的字段内容。

第三部分对齐填充不是必然存在的,只是起到占位符的作用,因为HotspotVM规定对象的起始地址必须是8字节的整数倍。所以很有可能以上两部分的大小不够8字节的整数倍,那么这个字段就可以发挥作用了。

 

对象的访问定位

主要是通过Java栈中的reference数据,通过这个reference数据只是一个指向对象的引用,那么对象的访问方式就可以不同。目前主流的对象访问方式主要由句柄和直接指针两种。通过句柄访问的话,会在Java堆中划分出一块句柄池,句柄池中的句柄存放了对象的实例数据和类型指针,而reference数据则存放了句柄的地址引用。使用直接指针访问对象,那么reference数据存放的就是对象的地址。

使用句柄访问的最大好处是reference中存储的稳定的句柄地址,当对象的地址发生了改变可以不用去关心。而直接指针的最大好处是速度更快,在于节省了一次指针定位的时间。

 

class文件为二进制字节流,以8位字节为基础。每个class文件的头4个字节称为魔数,他的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件。紧接着魔数的4个字节是class文件的版本号(5,6字节为次版本号,7,8字节为主版本号)。紧接着主版本号是常量池入口。紧接着两个字节为访问标志。类索引,父类索引,接口索引集合。字段表。方法表。属性表(java程序方法体中的代码经过java编译后,最终变为字节码指令存在code属性中)。java虚拟机操作码只有一个字节。

 

哪些对象可以作为GCroots:虚拟机栈(帧栈中的本地变量表)中的引用对象。方法区中类的静态属性引用的对象。方法区中常量引用的对象。本地方法栈中JNI(Java Native Interface)引用的对象。

 

对象是否可用以及引用类型

由于引用计数法无法解决循环引用的问题,所以一般都是使用可达性分析来判断的,即通过一系列称为“GC Roots”的对象(比如虚拟机栈引用的对象、方法区中的类静态属性和常量引用对象)作为起点,从这些节点一直往下搜索,走过的路径称为引用链;而那些没有与引用链相连的对象即为不可达,会被回收;

可以通过覆盖finalize方法来实现对象的“自救”,避免在标记后被回收,但通常不建议这么做;

对象的引用类型可分为:强引用、软引用(在内存溢出前会将这种类型的对象进行第二次回收)、弱引用(弱引用对象只能生存到下次垃圾回收之前)、虚引用(不会对生存时间存在影响,也无法通过它获取对象,主要目的就是在回收时收到一个系统通知);

 

永久代的垃圾回收包括两部分:废弃常量和无用的类。

废弃常量的回收比较好理解,因为只要没有任何对象引用常量池中的某个对象,那么这个对象就会被回收(废弃常量回收的是运行时常量池中的对象,所以只需要一次标记就好)。

类要满足下面3个条件才是无用类:所有实例被回收;加载该类的classloader被回收;该对象的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射调用该类的方法。

 

GC算法

停止-复制算法:将可用内存按照容量划分为大小相等的两块,每次只能使用其中的一块。当这一块内存用完了,则将还存活的对象复制到另一块内存上,然后把已经使用过的内存空间一次清理掉。(商业虚拟机:将内存分为一块较大的eden空间和两块较小的survivor空间,默认比例是8:1:1,即每次新生代中内存空间为整个新生代的90%,每次使用eden和其中一个survivor,最后清理掉eden和刚才用过的survivor,若另一块survivor空间没有足够的内存空间存放上次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代)。新创建的对象都会被分配到Eden区,这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时(15),就会被移动到年老代中。“From”和“To”会交换他们的角色。

标记-清除算法缺陷:产生大量不连续的内存碎片;标记和清除的效率都不高。

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

 

分代收集:新生代&#x

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值