面试整理-JVM

1、Java内存模型

方法区:

         方法区是线程共享区,用于存储虚拟机加载的类信息(版本,字段,方法,接口),常量,静态变量,即时编译后的代码等数据,逻辑上属于堆的一部分,但是与堆进行了区分;

        HotSpot虚拟机使用永久代来实现方法区,使得HotSpot虚拟机的垃圾收集器可以像管理内存一样管理这部分内存,方法区与永久代并不 等价,对于虚拟机来说,并不存在永久代的概念,方法区可以选择不实现垃圾收集,一般来说这部分区域对内存回收的条件比较苛刻,但是这部分区域的回收确实是必要的,当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。

常量池:

        运行时常量池:运行时常量池是方法区的一部分,class文件除了有类的版本、字段、方法、接口等信息描述外,还有一项就是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后放入方法去运行时常量池存放。运行时常量池相当于class文件中的常量池,不同的是它具备动态性,class文件中常量池中的常量在编译器就定义好了,而运行时常量池在运行期间也可以将常量加入该常量池中,最常见的方法就是调用String类中的intern()方法。

        字符串常量池:全局字符串常量池是在类加载完成,经过验证准备阶段之后在堆中生成的字符串实例,然后该字符串对象实例引用存到string pool中,string pool中存的是引用值,而不是具体的实例对象,具体的实例对象是在堆中开辟了一块空间存放的。在Hotspot中,实现的string pool功能是一个string table类,他是一个hash表,里面存的是驻留字符串的引用,而不是字符串实例本身,在堆中某些字符串实例被string table引用之后就等于被赋予了驻留字符串的身份,string table的实例只有一份,被所有类和线程共享。

例:

String str1 = "abc";
String str2 = new String("def"); 
String str3 = "abc";
String str4 = str2.intern(); 
String str5 = "def"; 
System.out.println(str1 == str3);//true 
System.out.println(str2 == str4);//false 
System.out.println(str4 == str5);//true

   上面程序的首先经过编译之后,在1.该类的class常量池中存放一些符号引用,2.然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,3.然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),4.然后将这个对象的引用存到全局String Pool中,也就是StringTable中,5.最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。(先存放类的符号引用(类名 描述符等)之类的东西然后类加载之后把这些东西存放在运行时常量池  然后在堆中生成实例  然后把对象的引用值存在字符串常量池中 然后解析阶段把,要把运行时常量池中的符号引用替换成直接引用)
  回到上面的那个程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值,最后str5在解析的时候就也是指向存在于StringTable中的”def”的引用值,那么这样一分析之后,下面三个打印的值就容易理解了。

        class文件常量池:class文件除了包含类的版本、字段、方法、接口等信息,还有一些常量池存放编译器生成的各种字面量和符号引用,字面量就是常量概念,如文本字符串、被声明为final的常量值等,符号引用时一组符号来描述所引用的目标,符号可以是任意形式的字面量,一般包括三类常量:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符;

堆空间:

        JVM管理的最大快内存区域,存放实例的线程共享区,是垃圾收集的主要区域,称GC堆。从内存回收的角度分类,可分为新生代(Eden区、From Survivor区、To Survivor区)和老年代;从内存分配的角度分类,为解决分配内存的线程安全问题,线程共享的Java堆可能分出多个线程私有的分配缓冲区(TLAB);堆在物理存储上不一定连续,在逻辑上连续即可;服务启动时可通过Xms-Xms参数指定运行时堆内存的大小,空间不足时会抛出OutOfMemoryError异常。

虚拟机栈:

        每个线程都有一个私有的栈,随着线程的创建而创建,生命周期与线程相同;虚拟机栈中存放着栈帧,每个方法都会创建一个栈帧,栈帧中存放着局部变量表、操作数栈、动态链接、方法出口等信息。

        局部变量表存放了编译期可知的基本数据类型和对象的引用类型,通常所说的栈内存就是指局部变量表;64位的long和double类型数据会占用两个局部变量表的空间,其余的数据类型只占一个;局部变量表所需要的内存空间在编译期完成分配,进入一个个方法时,这个方法 需要在栈帧中分配多少内存是固定的,运行期间不会改变局部变量表的大小;

在这里插入图片描述

        方法的调用到执行完毕,对应的就是栈帧的入栈和出栈的过程,栈的大小可以固定,也可以动态扩展;固定大小的情况下,当栈调用深度大于JVM所允许的范围,会抛出StackOverFlowError异常;在动态扩展的情况下,若扩展时无法申请足够的内存,会抛出OutOfMenoryError异常

        

 在这里插入图片描述

 本地方法栈:

        和虚拟机栈相似,区别就是虚拟机栈为执行Java方法服务,本地方法栈为执行native方法服务;hotshot虚拟机不区分虚拟机栈与本地方法栈,两者在一块;

程序计数器:

        程序计数器是一块较小的空间,可以看作当前线程所执行的字节码的行号指示器;若线程是Java方法,计数器记录的是正在执行虚拟机字节码指令的地址;

        JVM的多线程是通过线程轮流切换并分配CPU的执行时间片的方式来实现的,任一时刻,一个CPU只会执行一条线程中的指令,为保证线程切换后能恢复到正常的执行位置,每条线程都需要一个独立的 程序计数器,各线程中独立存储,互不影响;

        程序计数器是唯一一个Java虚拟机规范中没有规定任何OutOfMenoryError的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作; 

对象创建过程:

在这里插入图片描述

对象在内存中分为三块区域:

对象头:

        mark word(标记字段):默认存储对象的hashcode,分代年龄和锁标志位信息。根据对象的状态复用自己的存储空间,也就是说运行期间,mark word中存储的数据会随着标志位的变化而变化;

        class point(类型指针):指向类元数据的指针,虚拟机通过这个指针确定对象是哪个类的实例;

实例数据:主要存放类的数据信息,父类的信息;

对其填充:由于虚拟机要求对象起始地址必须是8的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐;(一个空对象占8个字节,因为对齐填充的关系,不到8字节对齐填充会自动补齐)

2、引用类型

        强引用(Strong Reference):默认使用的引用,即new一个对象,只要强引用存在,被引用的对象就不会被回收;

        软引用(Soft Reference):只有在内存不足的情况下,才会被回收,例SoftReference<Object> soft = new SoftReference<>(obj);

        弱引用(weak Reference):只要垃圾回收执行,就会被回收,无论内存是否充足,例WeakReference<Object> weak = new WeakReference<>(obj);

        虚引用:PhantomReference作用是个跟踪垃圾收集器收集对象的活动,再GC中,如果发现虚引用,就会将引用放入ReferenceQueue中,由开发自己处理,调用Reference.pull()方法,将引用从ReferenceQueue中移除之后,Reference对象会变成nactive状态,意味着被引用的对象可以被回收;例:

public void usePhantomReference(){
        ReferenceQueue<Object> rq = new ReferenceQueue<>();
        Object obj = new Object();
        PhantomReference<Object> phantomReference = new PhantomReference<>(obj,rq);
        obj = null;
        log.info("{}",phantomReference.get());
        System.gc();
        Reference<Object> r = (Reference<Object>)rq.poll();
        log.info("{}",r);
    }

3、error和exception的区别

error表示恢复不是不可能,但很困难的严重问题,比如内存溢出,不可能让程序处理;exception表示一种设计或实现问题,如果程序正常运行,就不会发生。

4、final、finally、finalize的区别

        final:修饰类、变量、方法,修饰的类不能被继承,修饰的方法不能被重写,修饰的变量应该是一个常量,不能被重新赋值;

        finally:一般作用于try-catch代码块,处理异常最终要执行的finally代码块;

        finalize:是一个方法,属于object类,一般由垃圾回收器来调用;

5、JVM类加载过程

加载(Java类加载器负责将编译好的Java class文件加载到JVM中的运行时数据区,供执行引擎调用):

        (1)通过全类名获取定义此类的二进制字节流

        (2)将字节流所代表的静态存储结构转换为方法区的运行时数据结构

        (3)在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

        类加载器:

        根加载器(BootStrap):一般用于本地代码实现,负责加载JVM基础核心类库(rt.jar)

        扩展类加载器(ExtClassLoader):从java.ext.dirs系统属性所指定的目录中加载类库,父加载器是BootStrap

        系统类加载器(AppClassLoader):又叫应用类加载器,它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,是用户自定义加载器的默认父加载器;

        用户自定义加载器:继承AppClassLoader;

注意:一个类只可以被加载一次,父加载器加载后子加载器就不会再加载了。

        类加载机制(双亲委派模型:当前类加载器加载一个类时,委托给其双亲先进行加载,双亲类加载器在加载时同样委托给自己的双亲,直到某个类加载器没有双亲为止,通常情况下指双亲为null,也即为当前的双亲为扩展类加载器,其parent为启动类加载器,然后依次在各自的类路径下寻找、加载class类);

        双亲委派模型的好处:

        1、避免重复加载。

        2、避免核心类被篡改,系统类由系统加载器进行加载;

验证:

准备:正式为变量分配内存并设置初始值;

        1、此时进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象初始化时随着对象一块分配在Java堆中;

        2、这里所设置的初始值通常是数据类型默认的0值,如0,0L,null,false等

解析:

        解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

初始化:

        初始化是真正执行类中定义的Java程序 代码(字节码),是执行类构造器<clinit>()方法的过程。对于<clinit>()方法的调用,虚拟机回自己确保在其多线程环境下的安全性。因为<clinit>()是带锁线程安全,所以在多线程环境下进行类初始化可能会引起死锁,并且这种死锁很难被发现。

        在初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化:

        1、当遇到new、getstatic、putstatic或invokestatic自个条字节码指令时,比如new一个类,读取一个静态字段(未被final修饰),或调用一个类的静态方法时;

        2、使用java.lang.reflect包的方法对类进行反射调用时,如果类没有初始化,应先进性初始化;

        3、初始化一个类,如果其父类没有被初始化,应先初始化其父类;

        4、虚拟机启动时,用户需要定义一个main方法主类,虚拟机会先初始化这个类;

        5、

  1. 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。

6、GC

垃圾收集算法:

        引用计数法:对象被引用一次,在对象头上加一次引用次数,如果引用次数为0,则对象可回收;不足点:无法解决循环引用的问题;

        可达性分析算法:目前的主流算法。以GC Roots对象为起点出发,引出他们指向的下一个节点,再以下个节点为起点引出下一个节点,依次递归遍历,形成一条条引用链,直到所有的节点都遍历完毕,如果对象不在任意一条引用链中,则对象可回收。

        (注:如果对象可回收,也不一定会被马上回收,当发生GC时,会先判断对象是否执行了finalize方法,如果未执行,会先执行finalize方法,我们在此方法里可将对象与GC Roots对象关联,这样执行finalize方法后,GC再次判断对象是否可达,如果不可达,则被回收,可达则不回收,finalize方法只会被执行一次)

GC Roots包括以下几类:

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

        方法区中静态属性引用的对象

        方法区中常量引用的对象

        本地方法栈中JNI(即native方法)引用的对象

垃圾回收的主要方法:

        标记清除算法:先根据可达性分析算法标记出相应的可回收对象,然后对对象进行回收,问题是会产生内存碎片;

        复制算法:把堆分成两块,A和B,区域A负责分配对象,区域B不分配,区域A使用标记法把存活的对象标记出来,然后把区域A存活的对象都复制到区域B,最后把区域A对象全部清理掉。缺点是内存浪费,且效率低下。

           标记整理法:前面和标记清除法一样,不同的是在标记后添加了一个整理的过程,将所有的存活对象都往一端移动,紧密排列,再清掉另一端的所有数据。缺点是每次清理都要频繁的移动对象,效率低下;

        分代收集算法:根据对象存活周期不同分为新生代和老年代,默认的比例是1:2,新生代又分为Eden区,from survivor区(S0),to survivor区(S1),三者比例为8:1:1,根据新老代的特点选择最合适的垃圾回收算法,新生代的GC称为yong gc(minor gc),老年代的GC称为old gc(full gc)

分代收集工作原理:

        对象一般分配在Eden区,当Eden区将满时,触发minor gc,经过minor gc只有少部分对象会存活,会被移动到S0区,同时对象年龄加一,最后把Eden区的对象全部清理。当触发下一次minor gc时,会把Eden区和S0区中的存活对象一起移动到S1区,对象年龄加一,同时清空Eden和S0空间,再次minor gc,重复上一步操作。Eden区采用的是复制算法。

        当对象年龄达到设定的阈值,晋升到老年代;当某个对象分配需要大量的连续内存时,对象会直接分配到老年代,因为如果把大对象分配到Eden区,反复移动需要很大的开销,也会很快沾满S0和S1区;当S0(或者S1)区相同年龄的对象大小之和大于S0(或S1)空间的一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代;

空间分配担保:

        在发生minor gc之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,可以确保minor gc是安全的;如果不大于,那么虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,则会检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,大于则进行minor gc,否则可能进行一次full gc。

Stop The World(STW)

        在minor gc和full gc期间,只有垃圾回收器线程在工作,其他线程则被挂起;如果老年代满了,会出发full gc,同时回收新生代和老年代(对整个堆进行GC)会导致Stop The World,造成挺大的性能开销;

        一般full gc会导致工作线程停顿时间较长,若此时server收到很多请求,则会被拒绝服务。由于full gc会影响性能,所以要在一个合适的时间点发起GC,这个时间点被称为safe point,主要指一下特定位置:循环的末尾、方法返回前、调用方法的call之后、抛出异常的位置;由于新生代的特点(大部分对象经过minor gc后就会消亡),minor gc采用复制算法,而老年代由于对象比较多占用空间较大,使用复制算法会有较大的开销,所以老年代一般采用标记整理法进行回收。

垃圾收集器的种类:

Serial: Serial收集器工作在新生代,单线程的垃圾收集器,只会使用一个CPU或一个线程来完成垃圾回收,对于单个CPU环境来说,简单高效,适用于Client模式,是Client模式下新生代的默认收集器;

ParNew:ParNew收集器是Serial的多线程版本;ParNew主要工作在Server模式,多线程可以让垃圾收集的更快,减少了SWT时间。只有ParNew能与CMS收集器配合工作,是许多运行在server模式下的虚拟机的首选;

Parallel Scavenge收集器:使用复制算法,多线程,工作在新生代的垃圾收集器;它目标是达到一个可控制的吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间)),适合后台运算等不需要太多用户交互的任务;

Serial Old收集器:工作在老年代的垃圾收集器,单线程主要在Cilent模式下使用,在server模式下,有两大用途,一是在JDK1.5之前与Parallel Scavenge配合使用,一种是作为CMS收集器的后备元,在并发收集失效时使用;

Parallel Old收集器:使用多线程和标记整理法,实现了吞吐量优先的目标;

CMS收集器:是以实现最短STW时间为目标的收集器;采用标记清除法,主要有一下四个步骤:初始标记,并发标记,重新标记,并发清除;它是真正意义上的并发收集器,实现了垃圾收集与用户线程同时工作;相比Parallel,STW的时间减少,但是minor gc的次数大量增加;而且无法处理浮动垃圾;堆CPU资源非常敏感;采用标记清除法,产生内存碎片;

G1:面向服务端的垃圾收集器,和CMS一样,可以与应用程序线程并发执行;整理空闲空间更快;需要的GC时间更好预测(用户可以指定期望停顿时间);不会像CMS一样牺牲大量的吞吐性能;不需要更大的Java heap;G1各代的存储地址不是连续的,每一代都时使用了n个不连续的带线啊哦相同的Region,每个Region占有一块连续的虚拟内存地址;除了传统的新老生代,Region还多了一个H,表示这些Region存储的是巨大对象(大小大于等于Region一半),这样超大对象就直接分配到老年代,防止反复拷贝移动。是Java9的默认垃圾收集器;Java8默认的GC回收器为Parallel Scavenge + Parallel Old;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值