Java虚拟机知识点总结

1. JVM内存分哪几个区,每个区的作用是什么?

  • 程序计数器
  • 虚拟机栈
  • 方法区
  • 本地方法栈
  • 运行时常量池

程序计数器:为线程私有,占用内存空间较小,是当前线程所执行的字节码的行号指示器。

虚拟机栈:为线程私有,每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行结束的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

:为线程共享区域,占用绝大部分内存。用于存放对象实例,是垃圾收集器管理的主要区域。

方法区:为线程共享区域,也被成为‘永久代’。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区可以选择不实现垃圾收集,但并不是说数据进入了方法区就可以永久存在,另外这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,但条件比较苛刻。

本地方法栈:为线程私有,作用与虚拟机栈类似,区别是虚拟机栈服务于Java方法,而本地方法栈服务于虚拟机使用到的Native方法。

运行时常量池:是方法区的一部分,用于存放类文件在编译期生成的各种字面量和符号引用。当常量池无法再申请到内存时,会抛出OutOfMemoryError异常。

2. Java普通对象的创建过程

(1)检查new指令的参数是否能在常量池中定位到该类的符号引用,并且确定该符号引用代表的类是否已被加载、解析和初始化,如果没有,必须先执行相应的类加载过程;

(2)类加载检查通过后,虚拟机将在堆中为新生对象分配内存(对象所需的内存大小在类加载完成后便可确定),其中分配方式有‘指针碰撞’和‘空闲列表’两种,具体采用哪种,依据采用的垃圾收集器而定;

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

(4)虚拟机对对象的对象头进行信息设置,包括类的元数据信息、对象的哈希码、对象的GC分代年龄等信息;

(5)执行方法,将对象按照用户自定义的内容进行初始化,至此,对象创建完成。

3. 如何判断对象是否存活?

Java虚拟机采用‘可达性分析算法’来判定对象是否存活。当一个对象到GC Roots没有任何引用链相连时,会被判定为可回收对象。

4. 垃圾收集算法有哪几种?

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法

标记-清除算法:先标记出所有需要回收的对象,之后统一回收。缺点:标记和清除两个过程的效率都不高,并且标记清除之后会产生大量不连续的内存碎片,这会导致以后程序在运行过程中需要分配较大的对象时,因找不到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法:考虑到新生代中的对象的存活率较低,将内存分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden和其中一块Survivor,当回收时,将Eden和该Survivor中还存活的对象一次性复制到另一块Survivor中,然后清理掉Eden和刚才使用过的Survivor。Eden和Survivor的大小比例一般为8:1。

标记-整理算法:老年代采用的算法。老年代中对象的存活率较高,没有额外的空间进行分配。标记过程仍然与‘标记-清除’算法一样,但后续是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:根据对象存活的周期不同,一般将Java堆分为新生代和老年代,然后在这两个代中根据各自的特点采用最适当的收集算法。新生代中,每次垃圾收集时都会有大批对象死去,少量存活,那就选用赋值算法;老年代中对象存活率高,则采用‘标记-清理’或‘标记-整理’算法。

5. Minor GC 和 Major GC 各自的含义?

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major/Full GC):指发生在老年代的GC,出现了Major GC,通常会伴随至少一次的Minior GC,Major GC的速度一般比Minor GC慢10倍以上。

6. 老年代的内存分配担保机制

新生代发生Minor GC时,如果Eden和Survivor中存活的对象较多,导致另一块Survivor内存空间大小不足以容纳这些对象,那么这些对象将通过分配担保机制直接进入老年代。

7. 简述Java内存分配规则与回收策略?

(1)对象优先在新生代Eden区分配,当Eden区没有足够空间时,虚拟机将发起一次 Minor GC。

(2)大对象直接进入老年代。大对象是指,需要大量连续内存空间的Java对象,比如很长的字符串以及数组。

(3)长期存活的对象将进入老年代。虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden区出生并经过第一次Minor GC后仍然存在,并且能被Survivor容纳的话,将被移动到Survivor空间,并且对象年龄设为1,对象在Survivor区每熬过一次Minor GC,年龄就增加1,当年龄达到15时(默认为15),将被晋升到老年代中。

(4)动态对象年龄判定。如果在Survivor空间里相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

(5)空间分配担保。在发生Minor GC时,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

  • 若条件成立,则可确保Minor GC顺利安全的进行(老年代分配担保机制);
  • 若不成立,则虚拟机会查看HandlePromotionFailture设置参数是否允许老年代担保失败,如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minior GC(有风险),如果小于,或者不允许担保失败,就会改为进行一次Full GC。

8. 简述虚拟机的类加载机制?

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

9. 类加载的过程

(1)加载

  • a. 通过一个类的全限定名来获取定义此类的二进制字节流;
  • b. 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构;
  • c. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

(2)验证:确保Class文件中的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机。这一步的校验包括:文件格式校验、元数据验证、字节码验证、符号引用验证。

(3)准备:这一阶段是为类变量(被static修饰的变量)分配内存并设置类变量的初始值,需要注意的是:

  • a. 这些类变量所使用的内存是在方法区进行分配;
  • b. 仅为类变量进行内存分配,而不包括实例变量(实例变量将会在对象实例化时随着对象一起分配在Java堆中);
  • 这里的初始值,通常指数据类型的零值,比如:
    public static int value = 123;
    变量value在准备阶段的初始值为0而不是123将value赋值为123的动作是在后面的初始化阶段才会执行
    
    但若有final修饰
    public static final int value = 123;
    则在准备阶段就会为value赋值为123.
    

(4)解析:解析阶段是虚拟机将常量池中的符号引用替换成直接引用的过程。

(5)初始化:类加载的最后一步,是执行类构造器<clinit>()方法的过程,根据用户设定值去初始化类变量和其他资源。

  • <clinit>(),编译器会自动收集所有类变量的赋值动作和静态语句块,语句的前后顺序影响会最终的执行结果,如:
static class TestClass {
      public static int A = 1;
      static {
          A = 2;
      }
}
  
public static void main(String[] args) {
      System.out.println(TestClass.A); 
}
输出结果:TestClass.A=2,而不是1
  • <clinit>()方法与类的构造函数(实例构造器()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
  • 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优于子类中的变量赋值操作。
  • 如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  • 接口和类一样都会生成<clinit>()方法,但不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会初始化。另外,接口的实现类在初始化的时候也一样不会执行接口的<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其余线程将阻塞等待。

虚拟机参数:

  • -XX:+PrintGCDetails:发生垃圾收集行为时打印内存回收日志,并且在进程退出时输出当前的内存各区域的分配情况。
  • -XX:MaxTenuringThreshold:设置对象晋升老年代的年龄阈值
Comments
Write a Comment