JVM-乱炖

283 阅读11分钟

【Java虚拟机是平台相关的吗?】

不是,JVM是跨平台的。Java源代码经过Java编译器生成.class字节码文件。Java通过字节码和JVM这种跨平台的抽象,屏蔽了操作系统和硬件的细节差异,即使CPU与操作系统不同,JVM也可以翻译出相同的机器码,然后运行得到相同的结果。这也就是“一次编译,到处执行”(补充:平台=CPU+OS)


【介绍一下Java的强引用、软引用、弱引用、虚引用】

不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。

在JDK1.2之前,判断引用的方法为:若Reference类型的数据存储另一块内存的地址,那么这个Reference类型数据就是引用类型。但我们有这样一个需求,有些“食之无味,弃之可惜”的对象,在内存充足时就留在内存中,当内存空间在GC后依然紧张就抛弃这些对象。所以在JDK1.2之后,有了强引用、软引用、弱引用、虚引用。

  • 强引用:指的是普通的对象引用,在任何情况下只要强引用关系还存在,被引用的对象就不可能被回收。
  • 软引用:指的是 指向非必须对象的引用,只有在JVM内存不足,即将发生内存溢出时才会去回收这些对象。软引用可以用来实现内存敏感的缓存,内存不足时就清除即可。--SoftReference对象类型。
  • 弱引用:它维护一种非强制的映射关系,但这些对象在垃圾回收时就会被回收。我们在获取对象时,若对象不存在则重新实例化即可 --WeakReference对象类型
  • 虚引用:它是最弱的一种引用类型,无法通过引用得到对象实例。它的作用仅仅是在对象被回收时发出一个系统通知。

【存放一个数组长度为3的HashMap<Integer,Long>占多少内存?】

  • 对于实例对象大小:Integer占16字节,Long占8字节。而HashMap底层有数组,一个原始数组类型需要24字节的头信息(包括16字节的对象开销、4字节的length、4个对齐字节)。所以此HashMap对象的内存 = 24 + 163 + 83 = 96字节。
  • 对于引用大小:引用所在方法没有被执行时,类变量引用在方法区中占4个字节;局部变量引用在栈中占1个字节。(这是32位的情况下,64位就占8个字节)

【new Object()占多大空间?】

Object对象占16字节(12字节的对象头,4字节的实例数据+对齐字节)。方法没有被执行时,它的指针在方法区中占4字节(64位系统占8字节);方法执行后,它的引用在栈中占1字节。


【局部变量不赋初值不能通过编译?】

我认为,局部变量是被存放在栈桢中的,随着方法的执行而入栈弹栈。所以局部变量的生命周期短暂,如果JVM为每个局部变量像成员变量那样赋予默认值,开销会很大。而不赋值可能会忘记赋值而导致不安全操作。所以局部变量不赋初值,编译就会报错。(成员变量时可以不赋初值的)

【值传递】:

值传递即方法形参的值传递,方法形参实际上是堆内存中对应数据的一份拷贝,然后压入此方法栈中,随着栈的消亡而消失,并不会影响到堆中的真实数据。

new对象的过程

当JVM执行到一条new指令时,首先会去检查new的这个Class对象的引用是否在运行时常量池中。

然后检查这个符号引用代表的类是否被加载过。若没有则先去执行此类的类加载过程;若此类已经被加载过了,则JVM会在Java堆的Eden元区为这个新对象分配一块内存。(因为Java堆的内存结构其实就是由垃圾收集器的特性来决定的,所以在使用Serial、ParNew等垃圾收集器时,JVM使用指针碰撞+CAS失败重试的方式,将空闲指针向着空闲内存的方向移动一段距离当作新内存;在使用CMS这样基于清除算法的垃圾收集器时,JVM就会维护一个可用内存列表,然后从这个列表中选取一块内存供新对象使用)

当调用new指令时,JVM会在Eden元区中划分出一块作为存储对象的内存,由于堆内存是线程共享的,所以在此划分内存需要进行同步。那JVM的做法就是:每个线程向JVM加锁申请一段连续的内存,然后当前线程维护两个指针,指向空余内存的初始和末尾。再执行new指令就是进行指针加法,加法后若空余内存指针仍小于指向末尾的指针,则代表分配成功。

分配完内存后,JVM会将分配到的内存都初始化为零值、对对象进行初始化信息设置。然后执行init()方法,也就是类的构造方法,对对象进行初始化。

并发情况下创建对象,会引发什么问题?如何解决?

正在为对象分配内存空间但指针还未修改时,其他线程来访问,导致错误或重复分配空间。 解决:使用CAS+失败重试的方法来保证更新指针操作的原子性;为每个线程在Java堆中预先分配一块缓冲区,用于线程隔离。

说一下JVM的类加载机制

image.png

类加载机制就是把Class文件加载到内存,并最终转换为被虚拟机直接使用的Java类型的过程。具体包括3个阶段:加载、连接、初始化。

加载:

根据类的全限定类名获取其二进制字节流,JVM将这个二进制字节流代表的静态数据结构转化为方法区中的运行时数据结构,生成对应的Class对象,为类中的数据提供访问入口。对于“根据全限定类名获取二进制字节流”这一规则,JVM并没有明确限制是哪个类,就提高了类加载的灵活性,比如动态代理、自定义类加载器等。

连接

① 验证是连接阶段的第一步:验证就是确保Class对象的字节流合法。它大致包括了四个步骤:

  • 文件格式验证:确保输入的字节流能够被解析并存储到方法区中。
  • 元数据验证:对类的元数据进行语义校验。比如是否继承了final类、是否实现了接口所有方法。
  • 字节码验证:就是对类中的方法体进行验证。比如数据类型的赋值与转换是否正确。
  • 符号引用验证:这个发生在连接的第三步解析阶段。验证所依赖的类、方法、字段是否存在,外部类是否可访问等。 ② 准备是连接阶段的第二步:就是为类中的静态变量赋予零值。

③ 解析是连接阶段的第三步:就是将运行时常量池中的符号引用替换为直接引用的过程。JVM会对解析结果进行缓存,供其他解析请求使用【RE:这个书上没怎么看明白】

初始化:

前面的步骤都是Java虚拟机主导(除了自定义类加载器),直至初始化阶段JVM才开始真正地执行类中编写的Java程序,将主导权交给应用程序。初始化阶段就是执行类构造器clinit方法的过程。具体步骤就是执行类变量的赋值动作和静态语句块。类的clinit方法执行前必须去执行父类的clinit方法,接口的clinit方法执行前不一定要执行父接口的方法(因为父接口可能没有静态变量需要赋值,当然接口也没有构造器)。

类不执行初始化阶段的案例:①通过子类引用父类的静态字段,子类不会执行初始化、②创建类的数组时,不会执行类的初始化、③类调用静态final字段时不会被初始化,因为它访问的是自身常量池的引用,与类的引用无关了

类加载机制都是在程序运行时期完成的,所以为Java提供了动态扩展的特性。比如接口可以在运行时指定它的实际实现类;Java用户可以通过自定义类加载器,使得Java程序在运行后还能去加载其他二进制流。

JVM类加载器

【双亲委派机制了解吗?(加载类的安全问题)】

image.png

这得从类加载器开始说起了。JVM设计团队把类加载阶段中 “通过一个类的全限定类名来获取该类的二进制字节流”的这个动作放到了JVM外部去实现,我们可以使用这个动作在程序运行时获取类对象。所以,实现这个动作的代码就是类加载器!(补充:两个对象只有在同一个类加载器加载的前提下,比较对象是否相等才有意义;若两个类的类加载器不同,则他们一定不相等。)

JDK8之前都是三层类加载器,即启动类加载器、扩展类加载器、应用程序类加载器。这三层类加载器之间的关系就用到了双亲委派模型。它的工作过程是:一个类加载器收到了类加载的请求,它会先把这个请求委托给父类加载器,直至最顶层的启动类加载器。若父类加载器无法加载此类,则会交由子类加载器尝试加载。它的底层实际上是每个加载器的loadClass()方法都去调用父类的loadClass(),父类抛异常则才会去调用子类加载器尝试加载。

双亲委派机制使得所有类在加载时都会到顶层判断是否有此类,这样就可以防止用户自己编写核心类并放入clsspath中造成混乱。而且双亲委派机制把所有的加载请求都向上委托,防止了一个类的多个加载器进行重复加载。

【双亲委派机制看起来很好,但实际上造成了一种类加载的枷锁。所以就有了打破双亲委派机制的三次情况】

  • JDK1.2时,为了兼容已经存在的自定义类加载器,在loadClass()方法调用失败后,会回过头来调用自己的findClass()方法,使得用户按照自己的意愿去加载类。
  • 如果核心类需要调用用户代码,但loadClass()到达启动类加载器就找到了对应类,就不再使用用户类加载器了。所以在JDK1.6时通过配置责任链模式的信息来解决这一问题。
  • 对于动态性需求,即热部署之类的,需要去调用大量同级类加载器去完成热部署,打破了双亲委派。
  • JDK9之后引入Java模块化系统,解决了只有在运行时才会报出缺少依赖等异常的情况。模块之间进行显示依赖,JVM启动时就直接验证依赖关系,不用等到运行使用时才发现缺少依赖。模块化系统下依然维持三层类加载器和双亲委派架构,只不过在委派到父类加载器之前,会先委托到依赖模块判断是否可以加载成功,若成功则直接返回。

【列举一下从上到下的类加载器吧】

启动类加载器:加载存放在java\lib目录下的类库,比如rt.jar、tools.jar。

扩展类加载器:加载存放在java\lib\ext目录下的类库。

应用程序类加载器:加载存放在classPath目录下的类库。若用户没有指定自定义类加载器,则此为默认类加载器。