Java JVM相关总结


title: JVM相关总结
tag: JVM
category: Java


JVM相关总结

内存区域和内存溢出异常

运行时数据区域

Java虚拟机运行时数据区Java虚拟机运行时数据区

  • 程序计数器
    是一块较小、线程私有的的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

    如果线程正在执行的是一个Java方法,计数器记录的就是正在执行的虚拟机字节码指令的地址;

    如果正在执行的是Native方法,计数器就为空值

    这个内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError的情况

  • Java虚拟机栈
    线程私有的内存空间,生命周期与线程相同。

    描述的是Java方法(字节码)执行的内存模型

    每个方法执行的同时都会创建一个栈帧用于存储局部变量、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成对应着一个栈帧在虚拟机栈中入栈到出站的过程

    其中局部变量表部分存放了编译器可知的各种基本数据类型、对象引用(reference类型)、returnAddress类型(指向了一条字节码指令的地址)。这些都是所执行的方法中所持有的。基本数据类型中的long和double类型的数据会占用2个局部变量空间(Slot),其余的占用1个。

    局部变量表的内存空间是在编译期间完成分配,方法运行期间不会改变其大小

    这个内存区域有两种异常情况:

    (1)StackOverflowError:如果线程请求的栈深度大于了虚拟机所允许的深度,将抛出此异常(特别是递归) 
    (2)OutOfMemoryError:如果虚拟机可以动态扩展,当扩展时无法申请到足够的内存
    
  • 本地方法栈
    作用与虚拟机栈类似,不过虚拟机栈是虚拟机执行Java方法的服务,而本地方法栈是为虚拟机使用的Native方法服务。

    在规范中并没有强制规定本地方法栈中方法使用的语言、使用方式和数据结构等,可以任意实现

    这个内存区域有两种异常情况:(与虚拟机栈差不多)
    (1)StackOverflowError:
    (2)OutOfMemoryError:

  • Java堆
    Java堆算是虚拟机中内存最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建

    唯一目的就是存放对象的实例,几乎所有的对象实例在这分配内存

    Java堆是垃圾收集器管理的主要区域,因此也可以称之为GC堆。

    从回收的角度看,Java堆还可以细分为新生代和老年代,再细致一点可以分为Eden空间、From Survivor空间、To Survivor空间等。

    从内存分配角度,可以划分出多个线程私有的分配缓冲区(TLAB)

    有一种异常情况:
    OutOfMemoryError:当堆中没有内存完成实例分配且堆无法扩展时,抛出

  • 方法区
    与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机记载的类信息、常量、静态常量、即时编译器编译后的代码等数据

    在虚拟机规范中这是Java堆的一个逻辑部分,但也叫Non-Heap(非堆)

    和堆一样不需要连续的内存,并且可以动态扩展;

    这块区域的垃圾回收主要目标是对常量池的回收和对类的卸载,一般比较难以实现

    1.8 HotShopt移除永久代,方法区移至元空间,不再虚拟机内存中:在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。

    一种异常情况:
    OutOfMemoryError:当方法区无法满足内存分配需求时,抛出

  • 运行时常量池
    是方法区的一部分,用于存放类加载后进入方法区的Class文件中的常量池信息,存放了编译器生成的各种字面量和符号引用

    一种异常情况:
    OutOfMemoryError:当常量池无法再申请到内存时,抛出

  • 直接内存
    不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存被频繁使用

    本机直接内存的分配不会手Java堆的大小限制

    使用Native函数库直接分配堆外内存,然后通过Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作

    一种异常情况:
    OutOfMemoryError:在给其他区域分配内存时忽略了直接内存,导致动态扩展时内个内存区域总和大于物理内存限制,就抛出

HotSpot虚拟机对象

  • 普通Java对象的创建
    (1)当遇到一个new指令时,虚拟机首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始化过。如果没有,就会先进行类加载。
    (2)当类加载检查通过后,就开始为新生对象分配内存。对象所需的内存大小在类加载完成后就完全确定。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

    指针碰撞:
    假设堆中内存绝对规整,所有用过的内存在一边,空闲的在一边,中间放着一个指针作为分界点的指示器,分配新的内存时就仅仅是把指示器这个指针想空闲空间挪动一段与对象大小相等的距离

    空闲列表:
    如果堆中内存不规整,用过的和空闲的相互交错,虚拟机必须维护一个表用于记录可用的内存块,在分配的时候从列表中找到一块足够大的空间灰分给对象表,并更新表上的记录

    Java堆是否规整取决于垃圾收集器是否带有压缩整理功能,不同的垃圾收集器分配方式不同。Serial、ParNew带有Compact过程就采用指针碰撞,CMS基于Mark-Sweep采用空闲列表

    还需要处理的问题:并发安全,可能出现正在给对象A分配内存,指正还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况
    解决方案:
    (a)对分配内存空间的动作进行同步处理:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
    (b)把内存分配的动作按照线程划分在不同的空间之中进行:即每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲区TLAB),分配内存时优先在自己的TLAB上分配,只有TLAB用完才进行同步锁定

    (3)分配完成后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)
    (4)接着,虚拟机对对象进行必要的设置:配置对象头,例如这个对象是哪个类的实例、如何找到类的元数据、对象的哈戏码、对象的GC分代年龄,是否使用对象锁
    (5)此时从虚拟机来说,一个新对象就产生了,从Java程序来说,才刚刚开始,因为<init>方法还没有执行,所有的字段都为0

  • 对象的内存布局
    在HotSpot虚拟机中,对象在内存中存储的布局分为如下:
    (1)对象头(Header)

    对象头包含两部分信息:

    第一部分用于存储对象自身的运行时数据,如哈戏码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit

    另一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个的实例

    如果对象是一个Java数组,还有一部分用于记录数组长度的数据

    (2)实例数据(Instance Data)
    对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段的内容,包括父类继承、子类定义的。

    HotSpot虚拟机中默认相同宽度的字段分配到一起

    (3)对齐填充(Padding)
    不一定存在,没有特别含义,仅仅是占位符的作用

  • 对象的访问定位
    (1)使用句柄访问
    Java虚拟对中将会划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息
    句柄访问句柄访问

    (2)直接指针访问
    reference中存储的直接就是对象的地址
    指针访问指针访问

    两种方式的优缺点:句柄访问最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,reference本身不修改;直接指针访问的最大好处就是速度更快,节省了一次指针定位的时间开销

内存溢出

  • Java堆溢出
    当对象数量到达最大堆的容量限制就会产生内存溢出异常

  • 虚拟机栈和本地方法栈溢出
    两种异常:StackOverflowError和OutOfMemoryError
    在单个线程下,即使内存无法分配,基本上只会抛出StackOverflowError;每个线程的栈分配的内存越大越容易抛出OutOfMemoryError
    每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽

    在大多数情况下,虚拟机默认栈深度可以达到1000~2000。如果建立过多线程导致的内存溢出,在不能减少线程数或者更换64位机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程(这样虚拟机栈和本地方法栈的内存就会更多)

  • 方法区和运行时常量池溢出
    JDK1.7开始,String.intern()方法是一个Native方法,作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则添加,并返回引用

    方法区溢出是一种常见的内存溢出异常。

  • 本机内存直接溢出
    unsafe.allocateMemory()


垃圾回收器与内存分配策略

判断对象已死

  • 引用计数算法
    给对象添加一个引用计数器,每当有地方引用它时,计数+1,引用失效时,计数-1,;任何时刻计数为0的对象是不可能再被使用的,但是无法解决对象间循环引用的问题,所以大部分Java虚拟机并没采取这种用法

  • 可达性分析算法
    现在主流采用的算法
    通过一系列的“GC Roots”对象作为起始点,从这些节点开始向下搜索,搜索的路径成为引用链,当一个对象到GC Roots对象没有任何引用链相连,则说明此对象是不可用的
    可达性分析算法可达性分析算法

    Java语言中,可以作为GC Roots对象有:
    (1)虚拟机栈中引用的对象
    (2)方法区中类静态属性引用的对象
    (3)方法区中常量引用的对象
    (4)本地方法栈中JNI引用的对象

  • 引用
    (1)强引用

    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
    如:Object o=new Object(); //强引用

    当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:
    o=null; //帮助垃圾收集器回收此对象

    显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象

    举个ArrayList的实现源代码

    private transient Object[] elementData;
    public void clear() {
        modCount++;
        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }
    

在ArrayList类中定义了一个私有的变量elementData数组,在调用方法清空数组时可以看到为每个数组内容赋值为null。不同于elementData=null,强引用仍然存在,避免在后续调用 add()等方法添加元素时进行重新的内存分配。使用如clear()方法中释放内存的方法对数组中存放的引用类型特别适用,这样就可以及时释放内存

(2)软引用

是用来描述一些还有用但非必需的对象

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。二次回收后还是内存不够才会抛出内存溢出异常。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存

在JDK 1.2后提供了SoftReference来实现软引用

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

(a)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建

(b)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出,这时候就可以使用软引用

Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get() != null) {
    rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
} else {
    prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
    sr = new SoftReference(prev); // 重新构建
}

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中

(3)弱引用

也是用来描述非必须对象,但是强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,不管内存是否足够,都会回收掉关联的对象。

在JDK 1.2后,提供了WeakReference来实现弱引用

引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。这个引用不会在对象的垃圾回收判断中产生任何附加的影响。

比如说Thread中保存的ThreadLocal的全局映射,因为我们的Thread不想在ThreadLocal生命周期结束后还对其造成影响,所以应该使用弱引用,这个和缓存没有关系,只是为了防止内存泄漏所做的特殊操作

(4)虚引用

也成为幽灵引用或者幻影引用,是最弱的一种引用关系。

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

在JDK 1.2后,提供了PhantomReference来实现虚引用

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存后,把这个虚引用加入到与之关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存回收后采取必要的行动。

由于Object.finalize()方法的不安全性、低效性,常常使用虚引用完成对象回收后的资源释放工作。

当你创建一个虚引用时要传入一个引用队列,如果引用队列中出现了你的虚引用,说明它已经被回收,那么你可以在其中做一些相关操作,主要是实现细粒度的内存控制。比如监视缓存,当缓存被回收后才申请新的缓存区。

  • 是否回收,判断死亡
    即使在可达性分析算法中不可达对象也不是“非死不可”,这时只是一个标记阶段,真正判断回收至少还需要经历两次标记过程:

    (1)如果对象进行可达性分析后,没有与GC Roots的引用链,那么会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”(finalize只能执行一次,不管是手动还是系统调用)。

(2)如果对象被判定有必要执行finalize()方法,那么会讲对象放置在F-Queue队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行(所谓的执行是虚拟机会出发该方法,但并不承诺等待它运行结束)

(3)finalize()方法是对象逃脱死亡的最后一次机会,稍后GC会对F-Queue中的对象进行第二次小规模的标记,如果在finalize()中从新与引用链连上,就会被移除“即将回收”的集合
  • 回收方法区
    Java虚拟机规范中不要求在方法区实现垃圾收集

    永久代(方法区)的垃圾收集主要是两部分:
    (1)废弃常量
    与回收堆中的对象类似

    (2)无用的类
    必须满足以下条件才能判定为无用的类

    该类所有的实例都已被回收,Java堆中不存在该类的任何实例
    加载该类的ClassLoader已经被回收

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

    判定为无用的类后也只是可以回收,并不是一定回收

    垃圾收集算法

标记擦除算法

分为标记和清除两个阶段,首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象
标记过程就是对象判定死亡过程
标记擦除算法标记擦除算法

  • 主要不足:
    (1)效率问题:标记和清除的效率都不高
    (2)空间问题:标记清除后会产生大量不连续的内存碎片,碎片太多导致分配大内存时没有连续的内存就不得不触发新一次的回收

复制算法

将内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完后,就将存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每一次都是对整个半区进行内存回收,内存分配时就不用考虑碎片的情况了,只要一动栈顶指正,按顺序分配即可
复制算法复制算法

  • 不足:
    内存缩小了一半,代价太高

商业虚拟机基本采用这种回收算法

标记整理算法

针对老年代的算法,标记后,不是直接清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存
标记整理算法标记整理算法

分代收集算法

根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,然后根据不同的特点采用最恰当的算法
新生代中采用复制算法,老年代中采用标记-清理或标记-整理算法

HotSpot的算法实现

  • 枚举根节点
    HotSpot使用一组OopMap的数据结构来记录哪些地方有对象引用、对象内偏移量上的数据类型,便于在枚举根节点时提高速度(枚举根节点几乎是所有的虚拟机在GC要做的事情

  • 安全点
    HotSpot只会在特定的位置(特定的指令)去生成OopMap,这些特定的位置就叫安全点。也就是程序执行时并非在所有地方都停顿下来开始GC,只有在到达安全点时才暂停去GC

    安全点的选定:

    1. 指令序列复用
    2. 会让程序长时间执行的代码。例如方法调用、循环跳转、异常条状等会产生安全点

如何在GC发生时让所有的线程都到达安全点:
(1)抢断式中断:不需要线程的代码主动配合,GC时,首先把所有的线程中断,如果发现有线程没有到达安全点,就恢复让它到达安全点(基本不采用了)
(2)主动式中断:当GC需要中断线程时,不直接对线程操作,仅仅简单设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起;轮询标志的地方和安全点重合

  • 安全区域
    在一段代码中,引用关系不会发生变化。这个区域的任意地方开始GC都是安全的

垃圾收集器

HotSpotHotSpot

  • Serial收集器
    单线程的收集器,只会使用一个CPU或一条收集线程去GC,并且在进行GC时必须暂停其他所有的工作线程,知道GC结束
    在Client模式下默认新生代收集器

  • ParNew收集器
    是Serial收集器的多线程版本,使用多条线程进行GC,除此外并无啥区别
    在Server模式下是首选的新生代收集器,因为只有它能和CMS配合工作

  • Parallel Scavenge收集器
    一个新生代收集器,使用复制算法,并行的多线程收集器
    也称为“吞吐量优先”收集器,因为它的目标就是达到一个可控制的吞吐量

  • Serial Old收集器
    Serial的老年代收集器,单线程,使用“标记-整理”算法

  • Parallel Old收集器
    Parallel Scavenge的老年代收集器,使用多线程和“标记-整理”算法

  • CMS收集器
    一种以最短回收停顿时间为目标的收集器,GC过程是与用户线程一起并发执行的,使用“标记-清除”算法

    GC过程:
    (1)初始标记
    (2)并发标记:耗时较长
    (3)重新标记
    (4)并发清楚:耗时较长

    缺点:
    (1)对CPU资源非常敏感
    (2)无法处理浮动垃圾:标记过后,GC无法在当次回收的的就是浮动垃圾
    (3)采用“标记-清除”会导致大量的空间碎片

  • G1收集器
    面向服务端的垃圾收集器,最牛逼的一个(未来),将内存化整为零

    特点:
    (1)并行与并发:充分利用多CPU、多核,来缩短停顿时间
    (2)分代收集
    (3)空间整合:G1整体看似基于“标记-整理”,但在局部是基于“复制”算法,不会留下空间碎片
    (4)可停顿的预测

    过程:
    (1)初始标记
    (2)并发标记
    (3)最终标记
    (4)筛选回收

内存分配和回收策略

对象主要分配在新生代的Eden区上,如果有本地线程缓冲,将按线程优先在TLAB上分配,少数情况可能会直接分配在老年代中

  • 内存分配策略

    1. 对象优先在Eden分配
      大多数情况下,对象在新生代Eden区分配,当Eden不够时,会触发一次Minor GC(新生代GC),也可能直接将新对象直接分配到老年代
    2. 大对象直接进入老年代
      需要大量连续内存空间的Java对象(长字符串、数组)很容易导致内存还有不少空间就提前触发GC,所以可以直接在老年代分配
    3. 长期存活的对象将进入老年代
      每个对象有一个对象年龄计数器(经过的GC次数,仍存活),当这个年龄到达一定的次数(默认15),这个对象就进入老年代
    4. 动态对象年龄判定
      如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须达到最大限制次数(比如年龄为5,但是有多个,并且占用内存很大,它们的总和大于了Survivor的一半,那么它们中大于或等于5的都可以直接进入老年代)
    5. 控件分配担保
      在新生代GC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,那么新生代GC是安全的,如果不成立,虚拟机会查看是否允许担保失败。如果允许,就继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次新生代GC,如果小于,则不允许冒险,就进行一次老年代GC(Full GC)
  • Full GC的触发条件

    对于Minor GC(年轻代GC),当Eden区满了就会触发一次;而Full GC需要有一下的条件

    1. 调用System.gc():调用此方法只是建议虚拟机去执行,虚拟机不一定真正去执行
    2. 老年代空间不足
    3. 空间分配担保失败
    4. 1.7及以前的永久代空间不足
    5. Concurrent Mode Failure:执行CMS GC过程中同时有对象进入老年代,而此时老年代空间不足(可能时GC过程中浮动垃圾过多导致暂时性的空间不足)就会报这个错并触发Full GC

类文件结构(class文件)

image.pngimage.png
Java虚拟机只与Class文件(二进制)所关联

Class类文件结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但类或接口不一定都得定义在文件里

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按顺序紧凑地排列在文件中,中间没有添加任何分隔符。当遇到占用8位字节以上空间的数据会按照高位在前的方式分割成若干个8位字节

包含两种数据类型:无符号和表
无符号数属于基本的数据类型,u1、u2、u4、u8分别代表1、2、4、8个字节的无符号数,可以用于描述数字、索引引用、数量值或按照UTF-8编码构成的字符串值
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,以_info结尾,整个Class文件本质上是一张表

Class文件格式:

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count
  • 魔数
    每个Class文件的头4个字节成为魔数,也就是magic,唯一作用是确定这个文件是为一个能被虚拟机接受的Class文件。值为:0XCAFEBABE(咖啡宝贝?)
    很多文件存储都会使用魔数来进行身份识别,如gif、jpeg

  • Class文件的版本
    第5、第6个字节是次版本号(minor_version)
    第7、第8个字节是主版本号(major_version)

    Class文件版本号:

    编译器版本 -target参数 十六进制版本号 十进制版本号
    JDK 1.1.8 不能带target参数 00 03 00 2D 45.3
    JDK 1.2.2 不带(默认target 1.1) 00 03 00 2D 45.3
    JDK 1.2.2 target 1.2 00 00 00 2E 46.0
    JDK 1.3.1_19 不带(默认1.1) 00 03 00 2D 45.3
    JDK 1.4.2_19 target 1.3 00 00 00 2F 47.0
    JDK 1.4.2_10 不带(默认1.2) 00 00 00 2E 46.0
    JDK 1.4.2_10 1.4 00 00 00 30 48.0
    JDK 1.5.0_11 不带(默认1.5) 00 00 00 31 49.0
    JDK 1.5.0_11 -target 1.4 -source 1.4 00 00 00 30 48.0
    JDK 1.6.0_01 不带(默认1.6) 00 00 00 32 50.0
    JDK 1.6.0_01 1.5 00 00 00 31 49.0
    JDK 1.6.0_01 -target 1.4 -source 1.4 00 00 00 30 48.0
    JDK 1.7.0 不带(默认1.7) 00 00 00 33 51.0
    JDK 1.7.0 1.6 00 00 00 32 50.0
    JDK 1.7.0 -target 1.4 -source 1.4 00 00 00 30 48.0
  • 常量池
    constant_pool,Class文件中的资源仓库,与其他项目关联最多,占用Class文件空间最大的数据项目之一,第一个出现表类型数据项目

    由于常量池中常量的数量不定,所以先用一个u2型的constant_pool_count表示常量池容量计数值,从1开始(只有这个值是从1开始,其余都是从0开始)

    常量池中主要存放字面量和符号引用:
    (1)字面量:接近于Java语言的常量概念,如文本字符串、声明为final的常量值
    (2)符号引用:编译原理概念,包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

    常量池中的每一个常量都是一张表
    常量池的项目类型:

    常量类型 标志 项目结构 类型 描述
    CONSTANT_Utf8_info 1 UTF-8编码的字符串
    tag u1 值为1,标志位
    length u2 UTF-8编码的字符串占用的字节数
    bytes u1 长度为length的UTF-8编码的字符串
    CONSTANT_Integer_info 3 整型字面量
    tag u1 值为3
    bytes u4 按照高位在前存储的int值
    CONSTANT_Float_info 4 浮点型字面量
    tag u1 值为4
    bytes u4 按照高位在前存储的float值
    CONSTANT_Long_info 5 长整型字面量
    tag u1 值为5
    bytes u8 按照高位在前存储的long值
    CONSTANT_Double_info 6 双精度浮点型字面量
    tag u1 值为6
    bytes u8 按照高位在前存储的double值
    CONSTANT_Class_info 7 类或接口的符号引用
    tag u1 值为7
    name_index u2 一个索引值,指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类(或接口)的全限定名
    CONSTANT_String_info 8 字符床类型字面量
    tag u1 值为8
    index u2 指向字符串字面量的索引
    CONSTANT_Fieldref_info 9 字段的符号引用
    tag u1 值为9
    index u2 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
    index u2 指向字段描述符CONSTANT_NameAndType的索引项
    CONSTANT_Methodref_info 10 类中方法的符号引用
    tag u1 值为10
    index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
    index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项
    CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
    tag u1 值为11
    index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引项
    index u2 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
    CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
    tag u1 值为12
    index u2 指向该字段或方法名称常量项的索引
    index u2 指向该字段或方法名称描述符常量项的索引
    CONSTANT_MethodHandle_info 15 表示方法句柄
    tag u1 值为15
    reference_kind u1 值必须在1~9之间,决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
    reference_index u2 值必须是对常量池的有效索引
    CONSTANT_MethodType_info 16 标识方法类型
    tag u1 值为16
    descriptor_index u2 值必须是对常量池的有效索引,常量池在该索引处必须是CONSTANT_Utf8_info结构,表示方法的描述符
    CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点
    tag u1 值为18
    bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表中的bootstrap_methods[]数组的有效索引
    name_and_type_index u2 值必须是对当前常量池的有效索引,常量池也该在索引处的项必须是CONSTANT_NameAndType_indo结构,表示方法名和方法描述符
  • 访问标志
    在常量池后面紧接着2个字节表示访问标志,用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型、如果是类是否声明为final等

    标志名称 标志值 含义
    ACC_PUBLIC 0x0001 是否为public类型
    ACC_FINAL 0x0010 是否被声明为final,只有类可设置
    ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意
    ACC_INTERFACE 0x0200 标识这是一个接口
    ACC_ABSTRACT 0x0400 是否为abstract类型,对于类或接口为真
    ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
    ACC_ANNOTATION 0x2000 标识这是一个注解
    ACC_ENUM 0x4000 标识只是一个枚举
  • 类索引、父类索引、接口索引集合
    类索引(this_class)、父类索引(super_class)都是一个u2类型,接口索引集合(interfaces)是一组u2类型的集合,入口第一项表示集合大小

    Class文件通过这三项来确定类的继承关系

  • 字段表集合
    field_info(字段表)用于描述接口或类中声明的变量,字段包括类级变量以及实例级变量,但不包括在方法内存声明的局部变量

    字段修饰符access_flags与类中的access_flags项目类似,都是一个u2的数据类型,包括是否为public、private、protected、static、final、volatile、transient、enum、是否由编译器自动产生

    name_index代表字段的简单名称,指没有类型和参数修饰的方法或字段名称,对应常量池的引用

    descriptor_index代表字段和方法的描述符,对应常量池的引用

  • 方法表集合
    同字段表结构一样,包括了access_flags、name_index、descriptor_index、attributes

    访问标志access_flags中没有了volatile和transient关键字的标志,多了synchronized、native、strictfp、abstract关键字的标志

  • 属性表集合
    Class文件、字段表、方法表都会携带自己的属性表集合,用于表述某些场景专有的信息
    属性表集合不再要求各个属性表具有严格的顺序

    主要看看Code属性:Java程序方法体中的代码经过Javac编译处理后,最终变为字节码指令存储在Code属性内


类加载机制

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

类的加载、连接和初始化过程都是在运行期完成的

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有的类

类的生命周期

类或接口从被加载到虚拟机内存中开始到卸载出内存为止,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading),其中验证、准备、解析3个部分称为连接(Linking)
类加载过程类加载过程

加载、验证、准备、初始化、卸载5个阶段的顺序是确定的,加载过程必须按照这个顺序。但解析不一定:在某些情况下可以在初始化之后再开始(为了支持动态绑定或晚期绑定)

类加载的时机

  • 主动引用

    5种情况必须初始化(初始化之前加载、验证、准备已经执行完):

    1. 遇到new、setstatic、putstatic、invokestatic这4条字节码指令时,如果类或接口没有进行初始化,则需要先进行初始化
      常见场景:使用new实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)、调用一个类的静态方法时
    2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果没有初始化,触发初始化
    3. 当初始化一个类的时候,如果发现其父类还没有初始化,则先触发父类进行初始化
    4. 当虚拟机启动时,用户需要指定一个要执行的类(包含main函数的ActivityThread),虚拟机会先初始化这个主类
    5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHanlde实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则先触发初始化

只有主动引用(上面5种情况)才能触发初始化,被动引用不可以。

  • 被动引用
    1. 对于静态字段,只有直接定义这个字段的类才会被初始化,通过其子类来引用父类中定一个的静态字段,只会触发父类的初始化而不会触发子类的初始化,但加载、验证是否被触发就跟虚拟机相关了
    2. 通过数组定义类引用类,不会触发此类的初始化
      MyClass[] mycls = new MyClass[10];
    3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类(常量传播优化,存储到引用的类的常量池),因此不会触发定义常量的类的初始化,直接调用常量不会触发类的初始化

接口基本与类相同,但是没有static静态语句块,但编译器会为接口生成<clinit>()类构造器来初始化接口中定义的成员变量,接口在初始化的时候不会要求父接口的都完成了初始化,只有使用父接口的时候,父接口才会初始化

类加载的过程

  • 加载

    1. 通过一个类的全限定名来获取定义此类的二进制字节流

    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

      非数组类的加载(获取类的二进制字节流的动作)可以使用系统提供的引导类加载器来完成,也可以通过用于自定义的类加载器完成

      数组类由虚拟机直接创建的,不通过加载器,创建遵循规则:

      (1)如果数组的组件类型(去掉一个维度的类型)是引用类型(如MyClass[]),那就递归采用加载器去加载,数组类将在加载该组件类型的类加载器的类名称空间上被标识

(2)如果数组的组件类型不是引用类型(如int[]),Java虚拟机会把这个数组类标记为与引导类加载器关联

(3)数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,可见性则默认public

加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区的外部接口

加载阶段和连接阶段是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始
  • 验证
    验证是连接的第一步,确保Class文件的字节流中包含的信息符号当前虚拟机的要求且不会危害虚拟机自身的安全

    1. 文件格式验证
      验证字节流是否符合Class文件格式的规范,且能被当前虚拟机处理
      验证点:
      (1)是否以魔数0xCAFEBABE开头

      (2)主次版本号是否在当前虚拟机处理范围内

      (3)常量池的常量中有不被支持的常量类型(常量tag标志)

      (4)指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量

      (5)CONSTANT_Utf8_info型的常量是否有不符合UTF8编码的数据

      (6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息

    2. 元数据验证

      对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范

      验证点:

      (1)这个类是否有父类

      (2)这个类的父类是否继承了不允许继承的类(final修饰的类)

      (3)如果这个类不是抽象类,是否实现了父类或接口中要求实现的所有方法

      (4)类中的字段、方法是否与父类产生矛盾

    3. 字节码验证

      是整个验证过程最复杂的阶段,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
      如果一个类方法体的字节码没有通过字节码验证,那肯定有问题,但如果通过了,也不能说明一定安全

    4. 符号引用验证
      在虚拟机将符号引用转化为直接引用的时候验证,这个转化动作在连接的第三阶段——解析阶段发生
      是对类自身以外的信息进行匹配性校验

      检验点:
      (1)符号引用中通过字符串描述的全限定名是否恁找到对应的类

      (2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

      (3)符号引用中的类、字段、方法的访问性是否可被当前类访问

  • 准备
    正式为类变量分配内存并设置类变量初始值的阶段,在方法区中进行内存分配。
    进行分配的仅包括类变量(static修饰的变量)而不包括实例变量,实例变量将会在对象实例化时伴随对戏那个一起分配在堆中,这里的初值是数据类型的零值。
    如果类字段的字段属性表中存在ConstantValue属性,那就在这个阶段进行初始化赋值,如public static final int value = 123;就会在准备阶段直接赋值为123而不是零值

  • 解析
    虚拟机将常量池内的符号引用替换为直接引用的程
    符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量。引用的目标不一定已经加载到内存中

直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄

主要针对以下符号引用解析:  
1. 类或接口的解析 
    假设当前代码所处的类为D,符号引用N解析为一个类或接口C的直接引用; 
    (1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载C 

    (2)如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“|[Ljava/land/Integer”的形式,就会按照(1)加载数组元素类型;如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象 

    (3)如果(1)(2)没有任何异常,那么C在虚拟机中实际上就是一个类或接口了,但在解析完成之前之前还要进行符号引用验证,确认D是否具备对C的访问权限  

2. 字段解析 
    首先将对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用,C解析成功后,对C后续字段的搜索 

    (1)如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束 

    (2)否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束  

    (3)否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束  

    (4)否则查找失败  

    查找过程成功返回了引用后,还将对这个字段进行权限验证

3. 类方法解析  

    第一个步骤与字段解析的第一个步骤一样,也需要先解析出方法表类的class_index项中索引的方法所属的类或接口的符号引用,解析成功后进行类方法搜索  

(1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就抛出异常  

    (2)如果通过了(1),在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束  

    (3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束  

    (4)否则,在类C实现的接口列表及它们的父接口中递归查找是否有简单名称和描述符都与目标想匹配的方法,如果存在,则说明类C是一个抽象类,查找结束,抛出异常  

    (5)否则,宣告方法查找失败,抛出异常  

4. 接口方法解析  

    先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,进行后续的接口方法搜索  

    (1)如果在接口方法表中发现class_index中的索引C是个类不是接口,直接抛出异常  

    (2)否则,在接口C中查找是否有简单名称和符号引用都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束  

    (3)否则,在接口C的父接口中递归查找,知道java.lang.Object类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束  

    (4)否则,宣告查找失败
  • 初始化

    类加载过程的最后一步。

    在这个阶段,根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,初始化阶段是执行类构造器<clinit>()方法(不是实例构造器,平时所说的是实例构造器)的过程

    <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量前面的静态语句块可以赋值但不能访问

    <clinit>()方法与类的构造函数(类的实例构造函数**<init>)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前(也就意味着父类定义的静态语句块的执行要优先于子类),父类的<clinit>()方法**执行完毕,因此虚拟机上第一个执行的类是Object

    <clinit>()方法对类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产**<clinit>()**

    接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成**<clinit>()方法**,但执行接口的**<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定一个的变量使用时,父接口才会初始化。接口的实现类在初始化的时候也一样不会执行接口的<clinit>()方法**

    虚拟机会保证一个类的**<clinit>()方法在多线程环境中被正确地枷锁、同步,如果多个线程同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法**,其他线程都需要阻塞等待,知道活动线程执行**<clinit>()方法**完毕

类加载器

  • 类与类加载器
    类加载器用于实现类的加载动作。每一个类加载器都有一个独立的类名称空间(比较两类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使两个类来源于同一个Class文件,但类加载器不同,必定不相等)

  • 双亲委派模型
    两种类加载器:一种是启动类加载器,用C++实现,是虚拟机自身的一部分呢;另一种是所有其他的类加载器,由Java语言实现,独立于虚拟机外部,且全继承自抽象类java.lang.ClassLoader

    三种系统提供的类加载器:

    (1)启动类加载器:负责将存放在<JAVA_HOME>\lib目录中的或者被-Xbootclasspath参数所指定的路径中的且是虚拟机识别的类库加载到虚拟机内存中。无法被Java程序直接引用

(2)扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中的或者被java.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用

(3)应用程序类加载器:是ClassLoader的getSystemClassLoader()方法的返回值,一般也称为系统类加载器

类加载器之间的层次模型就是双亲委派模型,除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器  

![双亲委派模型](https://upload-images.jianshu.io/upload_images/4061843-f27f2e4b8899540d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父加载器反馈无法完成时,子加载器才会尝试自己加载
  • 破坏双亲委派模型
    (1)双亲委派模型出现之前,已经有了自定义类加载器

    (2)模型自身的缺陷导致(基础类需要调回用户的代码

JNDI服务,使用线程上下文类加载器,父类加载器请求子类加载器去完成类加载的动作

(3)由于用户对程序动态性的追求而导致的(代码热替换、模块热部署)

虚拟机字节码

虚拟机字节码执行引擎的过程:输入的是字节码文件,处理的过程是字节码解析的等效过程,输出的是执行结果

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,就是虚拟机运行时数据区中的虚拟机栈的栈元素

栈帧存储了方法的局部变量表、操作数栈、动态连接、方法返回地址等信息
在活动线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧,与这个栈帧相关联的方法称为当前方法。所有字节码指令都只针对当前栈帧进行操作
栈帧结构概念图栈帧结构概念图

  • 局部变量表
    是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,建立在线程的堆栈上,线程私有。在编译为Class文件时,在方法的Code属性的max_locals确定了变量表的最大容量

    局部变量的容量以变量槽(Slot)为最小单位,每个boolean、byte、char、short、int、float、reference(对象实例引用)、returnAddress类型的数据应该占一个Slot。一个Slot可以存放一个32位以内的数据类型
    64位中,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间,访问必须同时访问两个Slot

    方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含参数。
    局部变量表中的Slot可以重用

  • 操作数栈
    也叫操作栈,后入先出栈,最大深度在写入到Code属性的max_stacks数据项中。32位数据类型所占栈容量为1,64位则为2

    栈帧重叠:部分重叠,实现栈帧之间的数据共享

  • 动态连接
    字节码中的方法调用是以常量池中指向方法的符号引用作参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就直接转化为直接引用,这就是静态解析
    另外一部分将在每一次运行期间转化为直接引用,称为动态连接

  • 方法返回地址
    当一个方法执行后,只有两种方式可以退出:
    (1)执行引擎遇到任意一个方法返回的字节码指令,正常完成出口(正常退出)
    (2)方法执行过程中遇到了异常,且没有在方法体内得到处理,异常完成出口(异常退出)

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(调用哪一个方法,特别是重载、重写的情况),暂时不涉及方法内部的具体运行过程

  • 解析
    调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析
    类加载阶段能确定下来的方法:静态方法、私有方法、实例构造器、父类方法、final修饰的方法,类加载的时候会直接将符号引用解析成直接引用

    解析调用一定是个静态的过程,编译期间完全确定

  • 分派
    多态的最基本的实现就是通过分派来实现

    1. 静态分派
      依赖于静态类型来定位方法执行版本的分派动作叫静态分派

      典型应用:方法重载,具体调用哪个方法取决于参数参数的数量和数据类型
      Human man = new Man();中,Human称为变量的静态类型(或者叫外观类型),Man称为变量的实际类型。静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译器可知的;而实际类型变化的结果是运行器才可确定,编译器在编译程序并不知道一个对象的实例是什么
      虚拟机在重载时是通过参数的静态类型(静态类型在编译时可知)而不是实际类型作为判断依据来决定使用哪个方法,这里的选择只是最优的,如果没有就会选择次优的(如果没有最优匹配,注意自动转换、自动装箱)

    2. 动态分派
      在运行期根据实例类型确定方法执行版本的动作叫做动态分派

      典型应用:重写,调用父类还是子类的方法取决于调用者的实例类型
      实例类型只有运行期才能确定

    3. 单分派与多分派
      方法的接收者与方法的参数统称为方法的宗量
      静态多分派,动态单分派

    4. 虚拟机动态分派的实现
      虚方法表虚方法表
      虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口一致,都指向父类的实现入口。如果重写了方法,子类方法表中的地址将会替换为子类实现版本的入口地址

  • 动态类型语言支持

    1. 动态类型语言:类型检查的主体过程是在运行期而不是编译期,如APL、PHP…
    2. JDK 1.7与动态类型
    3. java.lang.invoke包:提供一种新的动态确定目标方法的机制

基于栈的字节码解释执行引擎

  • 解释执行
  • 编译执行

编译期优化

  • 解析与填充符号表过程
    1. 词法、语法分析
    2. 填充符号表
  • 插入式注解处理器的注解过程
  • 分析与字节码生成过程
    1. 标注检查
    2. 数据及控制流分析
    3. 解语法糖
    4. 字节码生成

内存模型与线程

内存模型:在特定的操作协议下,对特定的操作内存或高速缓存进行读写访问的过程抽象

Java内存模型

Java内存模型的主要目标是定义程序中各个变量(包括视力字段、静态字段、构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有,不会被共享也就不会有竞争)的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这些底层的细节。

  • 主内存与工作内存
    Java内存模型规定了所有的变量都存储在主内存,每条线程有自己的工作内存(有点类似于高速缓存),线程的工作内存中保存了被该线程使用到的变量的内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存中的。不同线程间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
    Java内存模型Java内存模型
  • 内存间交互操作
    Java内存模型定义了8种操作来完成内存间的交互(主内存和工作内存),这几种操作都是原子的、不可再分的(对于double和long的load、store、read、write在某些平台有例外)
    1. lock(锁定):作用于主内存变量,把一个变量标识为一条线程独占的状态
    2. unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定
    3. read(读取):作用于主内存变量,把一个变量的值从主内存传输到线程工作内存中,以便后续的load使用
    4. load(载入):作用于工作内存变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
    5. use(使用):作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
    6. assign(赋值):作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
    7. store(存储):作用于工作内存变量,把工作内存中一个变量的值传送到主内存中,以便随后的write使用
  1. write(写入):作用于主内存变量,把store操作从工作内存中得到的变量放入主内存的变量中

如果变量从主内存到工作内存,必须顺序执行read和load;从工作内存到主内存,必须顺序执行store和write

以上8种操作需满足规则:
(1)不允许read和load、store和write操作单一出现,即不允许一个变量从主内存读取了但工作内存不接受或者从工作内存发起回写了但主内存不接受
(2)不允许一个线程丢弃它的最经的assign操作,即变量在工作内存改变了之后必须把该变化同步到主内存

(3)不允许一个线程无原因(没有assign操作)地把数据从工作内存同步回主内存

(4)一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量。也就是说对一个变量实施use、store操作之前,一定要先执行assign、load

(5)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程多次执行,lock的次数和unlock次数对应才会解锁

(6)如果对一个变量执行lock操作,那么将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值  

(7)如果一个变量事先没有被lock锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量  

(8)对一个变量执行unlock之前,必须先把此变量同步回主内存中(执行store、write)
  • 对于volatile型变量的特殊规则

    关键字volatile是实现最轻量级的同步机制

    当一个变量用volatile修饰后,具有两种特性:第一是保证此变量对所有线程的可见性(当一条线程修改变量的值,其他线程立即得知),但是并非原子性操作,并发下不安全。第二是禁止指令重排序优化

    对一个变量操作时,必须是load后面跟着use,use前面是load,assign后面是store,store前面是assign这样的顺序

  • 对于long和double型变量的特殊规则
    对于64位的数据类型(long和double),允许虚拟机将没有被volatile修饰的数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read、write这四个操作的原子性

  • 原子性、可见性、有序性
    原子性:

Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write,大致可以认为基本数据类型的访问读写具有原子性,lock、unlock也是,synchronized也具有原子性

可见性: 
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。 
Java内存模型通过变量修改后同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现 

volatile、synchronized、final都可以实现可见性

有序性: 

Java提供了volatile(本身禁止指令重排序)和synchronized(一个变量在同一时刻只允许一条线程对其进行lock操作)来保证线程间操作的有序性

  • 先行发生原则
    是判断数据是否存在竞争、线程是否安全的主要依据
    先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说发生在B之前,操作A产生的影响能被操作B观察到

    天然的先行发生关系:

    1. 程序次序规则:在一个线程内,按照程序代码顺序(控制流顺序)执行,在前的先执行
    2. 管程锁定规则:一个unlock先行发生于后面对同一个锁的lock操作
    3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
    4. 线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作
    5. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
    6. 线程中断操作:对线程interrupt方法的调用先行于被中断线程的代码检查到中断事件的发生
    7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行于发生于它的finalize方法的开始
    8. 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么A一定先行于C

Java与线程

  • 线程的实现

    1. 使用内核线程实现
      内核线程(KLT):直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。支持多线程的内核叫做多线程内核

      程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(LWP),就是我们通常说的线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。一对一模型
      内核线程实现内核线程实现

    2. 使用用户线程实现
      广义上来讲一个线程只要不是内核线程都算是用户线程。这样轻量级线程也算是用户线程,但其实现是内核线程。
      狭义上的用户线程是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁、调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作非常快速且低消耗,也可以支持规模更大的线程数量。一对多的模型
      用户线程实现用户线程实现

    3. 使用用户线程加轻量级进程混合实现
      内核线程和用户线程一起使用。N:M模型
      用户线程加轻量级进程用户线程加轻量级进程

  • Java线程调度
    线程调度:系统为线程分配处理器使用权的过程

    主要调度方式:

    1. 协同式线程调度:线程执行的时间由现场本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上,最大的好处就是实现简单,坏处就是线程执行时间不可控

    2. 抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定;线程的执行时间是系统可控的,不会有进程阻塞问题

      Java中采用抢占式,但是有10个级别的线程优先级,优先级越高越容易执行

  • 状态转换
    线程一共有5中状态,任何时刻,一个线程只能有一种状态

    1. 新建(New):创建后尚未启动的线程处于这种状态
    2. 运行(Runable):包括了操作系统线程状态中的Running和Ready,此状态的线程可能执行可能等待CPU分配时间
    3. 无限期等待(Waiting):这种状态的线程不会被分配CPU执行时间,要等待被其他线程显式地唤醒
      以下方法会让线程陷入Waiting:
      (1)没有设置Timeout参数的Object.wait()
      (2)没有设置Timeout参数的Thread.wait()
      (3)LockSupport.park()
    4. 限期等待(Timed Waiting):这种状态的线程不会被分配时间,不过无须等待被其他线程显式地唤醒,一定时间后就由系统自动唤醒
      以下是进入限期等待:
      (1)Thread.sleep()
      (2)设置了Timeout参数的Object.wait()
      (3)设置了Timeout参数的Thread.join()
      (4)LockSupport.parkNaons()
      (5)LockSupport.parkUntil()
    5. 阻塞(Blocked):线程被阻塞了,阻塞状态和等待状态的区别是:阻塞状态在等待着获取到一个排他锁,这个事件将在另一个线程放弃这个锁的时候发生;而等待状态则是在等待一段时间或者唤醒动作的发生,在程序等待进入同步区域的时候就是进入这种状态

线程安全与锁优化

线程安全

  • Java语言中的线程安全

    1. 不可变:不可变的对象一定是线程安全的,用final修饰符,例如String、枚举、Number的部分子类、Long、Double的包装类型、BigInteger、BigDecimal都是不可变的
    2. 绝对线程安全:Java API中标注自己是线程安全的类大多数是不安全的,例如Vector在多线程进行操作的时候仍然需要手动保证安全(上锁)
    3. 相对线程安全:通常意义上就是我们所说的线程安全,需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做保障措施,比如Vector、HashTable、Collections的synchronized、Collection方法包装的集合
    4. 线程兼容:指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发的环境中安全使用
    5. 线程对立:指无论调用端是否采用了同步措施,都无法在多线程环境中并发使用的代码,例如线程的操作suspend和resume,必须对立不同同时操作调用这两个方法
  • 线程安全实现方法

    1. 互斥同步:
      (1)通过synchronized关键字来实现,这是一个重量级的操作

      (2)还可以用ReentrantLock(重入锁)来实现

      一些区别:
      等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助

      公平锁:多个线程在等待同一个锁时,必须按照申请所得时间顺序来依次获得锁,非公平锁不保证这一点,synchronized、ReetrantLock的锁是非公平的

      锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait和notify、notifAll方法可以实现一个隐含的条件,如果要和多于一个条件关联的时候,就不得不额外枷锁,而ReentrantLock只需要多次调用newCondition方法就可以了

    2. 非阻塞同步:
      互斥同步最主要的问题就是进行线程阻塞和唤醒带来的问题,从处理问题的方式来说,互斥同步属于一种悲观的并发策略

      基于冲突检测的乐观并发策略:先进性操作,如果没有其他线程争用共享数据,那操作成功;如果有争用,产生了冲突,那就再采取其他的补偿措施(不断重试),因为不需要把线程挂起,所以这就是非阻塞同步

    3. 无同步方案:
      保证线程安全并不一定要进行同步,两者没有因果关系。如果一个方法不涉及共享数据,就无需同步措施保证正确性

      可重入代码:也叫纯代码,可以在代码执行的任何时刻去中断它,转而去执行另一段代码,而在控制权返回后原来的程序不会出现任何错误

      线程本地存储:如果一段代码中所需要的数据必须是与其他代码共享,那就可以考虑这些共享数据在同一个线程,就无需同步也能保证线程之间不出现争用

锁优化

  • 自旋锁、自适应自旋
    自旋锁:为了让线程等待,让线程执行一个忙循环(自旋),默认自旋次数10

    自适应自旋锁:自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋可能再次成功,进而允许自旋等待持续相对更长的时间

  • 锁消除

    虚拟机即时编译器在运行时,对一些代码上要求同步,但不能被检测到不可能存在共享数据竞争的锁进行消除。

    锁小吃的主要判定依据来源于逃逸分析的数据支持,如果在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的

  • 锁粗化

  • 轻量级锁
    相对于使用操作系统互斥量来实现传统所而言的,传统的锁机制成为“重量级”锁。
    轻量级锁并不是代理重量级锁的,是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

  • 偏向锁
    目的是为了消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能

JVM、DVM、ART的区别

JVM:Java虚拟机

DVM:Dalvik虚拟机, Android中的

ART:Android L开始引入的

  1. Java虚拟机运行的时Java字节码,Dalvik虚拟机运行的dex字节码
  2. Dalvik可执行文件体积更小
  3. Java虚拟机编译后,Java字节码保存在class文件中;Dalvik则会将Java字节码转换为dex字节码,并打包在dex可执行文件中
  4. JVM基于栈,DVM基于寄存器
  5. apk都是dex文件,但DVM执行的是dex字节码,依靠JIT即时编译器解释执行,运行时动态将dex翻译成本地机器码;ART执行的是本地机器码,依靠AOT预编译,在安装时将dex文件翻译成本地机器码;所以DVM安装块,启动慢,ART安装时间长,启动快

相关面试题

  1. GC回收策略
    (1)对象优先在Eden分配
    大多数情况下,对象在新生代Eden区分配,当Eden不够时,会触发一次Minor GC(新生代GC),也可能直接将新对象直接分配到老年代
    (2)大对象直接进入老年代
    需要大量连续内存空间的Java对象(长字符串、数组)很容易导致内存还有不少空间就提前触发GC,所以可以直接在老年代分配
    (3)长期存活的对象将进入老年代
    每个对象有一个对象年龄计数器(经过的GC次数,仍存活),当这个年龄到达一定的次数(默认15),这个对象就进入老年代
    (4)动态对象年龄判定
    如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须达到最大限制次数(比如年龄为5,但是有多个,并且占用内存很大,它们的总和大于了Survivor的一半,那么它们中大于或等于5的都可以直接进入老年代)
    (5)控件分配担保
    在新生代GC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,那么新生代GC是安全的,如果不成立,虚拟机会查看是否允许担保失败。如果允许,就继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次新生代GC,如果小于,则不允许冒险,就进行一次老年代GC(Full GC)

  2. 对 Dalvik、ART 虚拟机有基本的了解;
    Dalvik虚拟机执行的是dex字节码,是安卓中使用的虚拟机,所有安卓程序都运行在安卓系统进程里,每个进程对应着一个Dalvik虚拟机实例,应用每次运行的时候,字节码都需要通过即时编译器(JIT)转换为本地机器码

    ART虚拟机执行的是本地机器码,从 Android L 开始引入,应用在第一次安装的时候,会使用设备上的dex2oat工具进行字节码转码,把字节码预先编译成本地机器码,使其成为真正的本地应用(这个过程叫做预编译),加快了启动速度,但相对安装时间加长,安装所需空间增大

    ART取代了Dalvik

    JAVA虚拟机、Dalvik虚拟机和ART虚拟机简要对比
    JVM、Dalvik、ART 介绍

  3. 类加载机制
    加载、验证、准备、解析、初始化、使用、卸载
    其中验证、准备、解析三个阶段又统称为连接阶段,加载、验证、准备、初始化、卸载5个阶段的顺序是确定的

  4. 双亲委派模型
    类加载器之间的层次模型就是双亲委派模型,除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器

  5. 垃圾收集机制 对象创建,新生代与老年代
    通过可达性分析算法判断一个对象是否真的已死(经过两次标记后),确定后就会GC
    对象主要分配在新生代的Eden区上,如果有本地线程缓冲,将按线程优先在TLAB上分配,少数情况可能会直接分配在老年代中,如果遇到大对象(长字符串、数组等)会直接分配进入老年代

  6. JVM内存模型,内存区域
    Java内存模型规定了所有的变量都存储在主内存,每条线程有自己的工作内存(有点类似于高速缓存),线程的工作内存中保存了被该线程使用到的变量的内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存中的。不同线程间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
    Java内存模型Java内存模型

    Java虚拟机运行时数据区Java虚拟机运行时数据区

  7. 强引用置为null,会不会被回收?
    不会,显式地设置为null,GC会认为该对象不存在,就可以被回收

  8. Java中内存区域与垃圾回收机制
    Java虚拟机运行时数据区Java虚拟机运行时数据区

    GC机制:
    (1)判断对象已死:通过可达性分析,两次标记后判定为对象已死
    (2)GC收集算法:通过收集算来来回收内存空间,现在常用分代收集算法
    (3)内存分配:内存分配的策略对回收也很重要

    图解Java 垃圾回收机制

  9. 垃圾回收机制与调用System.gc()区别
    垃圾回收机制是到达安全点或安全域然后自动触发一次GC回收,当内存不足或应用程序空闲时也会触发一次GC,调用System.gc()是手动触发强制GC回收

  10. 解释一下栈帧
    栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,就是虚拟机运行时数据区中的虚拟机栈的栈元素

    栈帧存储了方法的局部变量表、操作数栈、动态连接、方法返回地址等信息
    在活动线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧,与这个栈帧相关联的方法称为当前方法。所有字节码指令都只针对当前栈帧进行操作
    栈帧结构概念图栈帧结构概念图

  11. Android中弱引用与软引用的应用场景
    弱引用:当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。这个引用不会在对象的垃圾回收判断中产生任何附加的影响。比如说Thread中保存的ThreadLocal的全局映射,因为我们的Thread不想在ThreadLocal生命周期结束后还对其造成影响,所以应该使用弱引用

    软引用:例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢

  12. SOF你遇到过哪些情况。 (JVM)
    StackOverflowError,程序中一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过默认大小而导致溢出。

    栈溢出的原因:
    递归调用
    大量循环或死循环
    全局变量是否过多
    数组、List、map数据过

  13. 对象创建方法,对象的内存分配,对象的访问定位
    对象创建方法:
    (1)当遇到一个new指令时,虚拟机首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始化过。如果没有,就会先进行类加载。
    (2)当类加载检查通过后,就开始为新生对象分配内存。对象所需的内存大小在类加载完成后就完全确定。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
    (3)分配完成后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)
    (4)接着,虚拟机对对象进行必要的设置:配置对象头,例如这个对象是哪个类的实例、如何找到类的元数据、对象的哈戏码、对象的GC分代年龄,是否使用对象锁
    (5)此时从虚拟机来说,一个新对象就产生了,从Java程序来说,才刚刚开始,因为<init>方法还没有执行,所有的字段都为0

    对象的内存分配:
    (1)对象头(Header)
    对象头包含两部分信息:第一部分用于存储对象自身的运行时数据,如哈戏码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit。另一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个的实例

    (2)实例数据(Instance Data)
    对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段的内容,包括父类继承、子类定义的。
    (3)对齐填充(Padding)
    不一定存在,没有特别含义,仅仅是占位符的作用

    对象的访问定位:
    (1)使用句柄访问
    Java虚拟对中将会划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息
    句柄访问句柄访问

    (2)直接指针访问
    reference中存储的直接就是对象的地址
    指针访问指针访问

  14. GC收集器有哪些?CMS收集器与G1收集器的特点

    • Serial收集器
      单线程的收集器,只会使用一个CPU或一条收集线程去GC,并且在进行GC时必须暂停其他所有的工作线程,知道GC结束
      在Client模式下默认新生代收集器
    • ParNew收集器
      是Serial收集器的多线程版本,使用多条线程进行GC,除此外并无啥区别
      在Server模式下是首选的新生代收集器,因为只有它能和CMS配合工作
    • Parallel Scavenge收集器
      一个新生代收集器,使用复制算法,并行的多线程收集器
      也称为“吞吐量优先”收集器,因为它的目标就是达到一个可控制的吞吐量
    • Serial Old收集器
      Serial的老年代收集器,单线程,使用“标记-整理”算法
    • Parallel Old收集器
      Parallel Scavenge的老年代收集器,使用多线程和“标记-整理”算法
    • CMS收集器
      一种以最短回收停顿时间为目标的收集器,GC过程是与用户线程一起并发执行的,使用“标记-清除”算法
      缺点:
      (1)对CPU资源非常敏感
      (2)无法处理浮动垃圾:标记过后,GC无法在当次回收的的就是浮动垃圾
      (3)采用“标记-清除”会导致大量的空间碎片
    • G1收集器
      面向服务端的垃圾收集器,最牛逼的一个(未来),将内存化整为零
      特点:
      (1)并行与并发:充分利用多CPU、多核,来缩短停顿时间
      (2)分代收集
      (3)空间整合:G1整体看似基于“标记-整理”,但在局部是基于“复制”算法,不会留下空间碎片
      (4)可停顿的预测
  15. Minor GC与Full GC分别在什么时候发生?
    Eden区分配内存不足时会触发Minor GC
    在新生代GC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,那么新生代GC是安全的,如果不成立,虚拟机会查看是否允许担保失败。如果允许,就继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次新生代GC,如果小于,则不允许冒险,就进行一次老年代GC(Full GC)

  16. 分派:静态分派与动态分派
    静态分派:
    依赖于静态类型来定位方法执行版本的分派动作叫静态分派

    典型应用:方法重载,具体调用哪个方法取决于参数参数的数量和数据类型
    虚拟机在重载时是通过参数的静态类型(静态类型在编译时可知)而不是实际类型作为判断依据来决定使用哪个方法,这里的选择只是最优的,如果没有就会选择次优的(如果没有最优匹配,注意自动转换、自动装箱)

    动态分派:
    在运行期根据实例类型确定方法执行版本的动作叫做动态分派

    典型应用:重写,调用父类还是子类的方法取决于调用者的实例类型
    实例类型只有运行期才能确定

  17. java内存模型,五个部分,程序计数器、栈、本地栈、堆、方法区。每个部分的概念、特点、作用。
    Java虚拟机运行时数据区Java虚拟机运行时数据区

    • 程序计数器
      是一块较小、线程私有的的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

      如果线程正在执行的是一个Java方法,计数器记录的就是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,计数器就为空值

      这个内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError的情况

    • Java虚拟机栈
      线程私有的内存空间,生命周期与线程相同。

      描述的是Java方法(字节码)执行的内存模型:每个方法执行的同时都会创建一个栈帧用于存储局部变量、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成对应着一个栈帧在虚拟机栈中入栈到出站的过程

      其中局部变量表部分存放了编译器可知的各种基本数据类型、对象引用(reference类型)、returnAddress类型(指向了一条字节码指令的地址)。这些都是所执行的方法中所持有的。基本数据类型中的long和double类型的数据会占用2个局部变量空间(Slot),其余的占用1个。
      局部变量表的内存空间是在编译期间完成分配,方法运行期间不会改变其大小

      这个内存区域有两种异常情况:
      (1)StackOverflowError:如果线程请求的栈深度大于了虚拟机所允许的深度,将抛出此异常(特别是递归)
      (2)OutOfMemoryError:如果虚拟机可以动态扩展,当扩展时无法申请到足够的内存

    • 本地方法栈
      作用与虚拟机栈类似,不过虚拟机栈是虚拟机执行Java方法的服务,而本地方法栈是为虚拟机使用的Native方法服务。在规范中并没有强制规定本地方法栈中方法使用的语言、使用方式和数据结构等,可以任意实现

      这个内存区域有两种异常情况:(与虚拟机栈差不多)
      (1)StackOverflowError:
      (2)OutOfMemoryError:

    • Java堆
      Java堆算是虚拟机中内存最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建

      唯一目的就是存放对象的实例,几乎所有的对象实例在这分配内存

      Java堆是垃圾收集器管理的主要区域,因此也可以称之为GC堆。从回收的角度看,Java堆还可以细分为新生代和老年代,再细致一点可以分为Eden空间、From Survivor空间、To Survivor空间等。从内存分配角度,可以划分出多个线程私有的分配缓冲区(TLAB)

      有一种异常情况:
      OutOfMemoryError:当堆中没有内存完成实例分配且堆无法扩展时,抛出

    • 方法区
      与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机记载的类信息、常量、静态常量、即时编译器编译后的代码等数据

      在虚拟机规范中这是Java堆的一个逻辑部分,但也叫Non-Heap(非堆)。

      一种异常情况:
      OutOfMemoryError:当方法区无法满足内存分配需求时,抛出

    • 运行时常量池
      是方法区的一部分,用于存放类加载后进入方法区的Class文件中的常量池信息,存放了编译器生成的各种字面量和符号引用

      一种异常情况:
      OutOfMemoryError:当常量池无法再申请到内存时,抛出

  18. 类加载的过程,加载、验证、准备、解析、初始化。每个部分详细描述。

    • 加载

      1. 通过一个类的全限定名来获取定义此类的二进制字节流

      2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

      3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

        非数组类的加载(获取类的二进制字节流的动作)可以使用系统提供的引导类加载器来完成,也可以通过用于自定义的类加载器完成

        数组类由虚拟机直接创建的,不通过加载器,创建遵循规则:
        (1)如果数组的组件类型(去掉一个维度的类型)是引用类型(如MyClass[]),那就递归采用加载器去加载,数组类将在加载该组件类型的类加载器的类名称空间上被标识
        (2)如果数组的组件类型不是引用类型(如int[]),Java虚拟机会把这个数组类标记为与引导类加载器关联
        (3)数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,可见性则默认public

        加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区的外部接口

        加载阶段和连接阶段是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始

    • 验证
      验证是连接的第一步,确保Class文件的字节流中包含的信息符号当前虚拟机的要求且不会危害虚拟机自身的安全

      1. 文件格式验证
        验证字节流是否符合Class文件格式的规范,且能被当前虚拟机处理
      2. 元数据验证
        对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范
      3. 字节码验证
        是整个验证过程最复杂的阶段,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
        如果一个类方法体的字节码没有通过字节码验证,那肯定有问题,但如果通过了,也不能说明一定安全
      4. 符号引用验证
        在虚拟机将符号引用转化为直接引用的时候验证,这个转化动作在连接的第三阶段——解析阶段发生
        是对类自身以外的信息进行匹配性校验
    • 准备
      正式为类变量分配内存并设置类变量初始值的阶段,在方法区中进行内存分配。
      进行分配的仅包括类变量(static修饰的变量)而不包括实例变量,实例变量将会在对象实例化时伴随对戏那个一起分配在堆中,这里的初值是数据类型的零值。
      如果类字段的字段属性表中存在ConstantValue属性,那就在这个阶段进行初始化赋值,如public static final int value = 123;就会在准备阶段直接赋值为123而不是零值

    • 解析
      虚拟机将常量池内的符号引用替换为直接引用的过程
      符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量。引用的目标不一定已经加载到内存中
      直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄

      主要针对以下符号引用解析:

      1. 类或接口的解析
        假设当前代码所处的类为D,符号引用N解析为一个类或接口C的直接引用;
        (1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载C
        (2)如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“|[Ljava/land/Integer”的形式,就会按照(1)加载数组元素类型;如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象
        (3)如果(1)(2)没有任何异常,那么C在虚拟机中实际上就是一个类或接口了,但在解析完成之前之前还要进行符号引用验证,确认D是否具备对C的访问权限

      2. 字段解析
        首先将对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用,C解析成功后,对C后续字段的搜索
        (1)如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
        (2)否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
        (3)否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
        (4)否则查找失败

        查找过程成功返回了引用后,还将对这个字段进行权限验证

      3. 类方法解析
        第一个步骤与字段解析的第一个步骤一样,也需要先解析出方法表类的class_index项中索引的方法所属的类或接口的符号引用,解析成功后进行类方法搜索
        (1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就抛出异常
        (2)如果通过了(1),在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束
        (3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束
        (4)否则,在类C实现的接口列表及它们的父接口中递归查找是否有简单名称和描述符都与目标想匹配的方法,如果存在,则说明类C是一个抽象类,查找结束,抛出异常
        (5)否则,宣告方法查找失败,抛出异常

      4. 接口方法解析
        先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,进行后续的接口方法搜索
        (1)如果在接口方法表中发现class_index中的索引C是个类不是接口,直接抛出异常
        (2)否则,在接口C中查找是否有简单名称和符号引用都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束
        (3)否则,在接口C的父接口中递归查找,知道java.lang.Object类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束
        (4)否则,宣告查找失败

    • 初始化
      类加载过程的最后一步。
      在这个阶段,根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,初始化阶段是执行类构造器<clinit>()方法(不是实例构造器,平时所说的是实例构造器)的过程

      <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量前面的静态语句块可以赋值但不能访问

      <clinit>()方法与类的构造函数(类的实例构造函数<init>)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法执行完毕,因此虚拟机上第一个执行的类是Object

      <clinit>()方法对类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()

      接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成<clinit>()方法,但执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定一个的变量使用时,父接口才会初始化。接口的实现类在初始化的时候也一样不会执行接口的<clinit>()方法

      虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地枷锁、同步,如果多个线程同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,知道活动线程执行<clinit>()方法完毕

  19. 加载阶段读入.class文件,class文件是二进制吗,为什么需要使用二进制的方式?
    是二进制字节流流,至于为什么,应该二进制是机器码,相对于其他格式效率更高速度更快

  20. 验证过程是防止什么问题?验证过程是怎样的?加载和验证的执行顺序?符号引用的含义?
    确保Class文件的字节流中包含的信息符号当前虚拟机的要求且不会危害虚拟机自身的安全

    (1)文件格式验证

    验证字节流是否符合Class文件格式的规范,且能被当前虚拟机处理  
    

    (2)元数据验证

    对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范  
    

    (3)字节码验证

    是整个验证过程最复杂的阶段,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的  
    如果一个类方法体的字节码没有通过字节码验证,那肯定有问题,但如果通过了,也不能说明一定安全
    

    (4)符号引用验证

    在虚拟机将符号引用转化为直接引用的时候验证,这个转化动作在连接的第三阶段——解析阶段发生  
    是对类自身以外的信息进行匹配性校验  
    

    加载和验证是交互进行的

    符号引用的含义:以一组符号来描述所引用的目标,符号可以是任何形式的字面量。引用的目标不一定已经加载到内存中,编译原理概念,包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

  21. 准备过程的静态成员变量分配空间和设置初始值问题。
    进行分配的仅包括类变量(static修饰的变量)而不包括实例变量,实例变量将会在对象实例化时伴随对戏那个一起分配在堆中,这里的初值是数据类型的零值。
    如果类字段的字段属性表中存在ConstantValue属性,那就在这个阶段进行初始化赋值,如public static final int value = 123;就会在准备阶段直接赋值为123而不是零值

  22. 解析过程符号引用替代为直接引用细节相关。
    主要针对以下符号引用解析:

    1. 类或接口的解析
      假设当前代码所处的类为D,符号引用N解析为一个类或接口C的直接引用;
      (1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载C
      (2)如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“|[Ljava/land/Integer”的形式,就会按照(1)加载数组元素类型;如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象
      (3)如果(1)(2)没有任何异常,那么C在虚拟机中实际上就是一个类或接口了,但在解析完成之前之前还要进行符号引用验证,确认D是否具备对C的访问权限

    2. 字段解析
      首先将对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用,C解析成功后,对C后续字段的搜索
      (1)如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
      (2)否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
      (3)否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
      (4)否则查找失败

      查找过程成功返回了引用后,还将对这个字段进行权限验证

    3. 类方法解析
      第一个步骤与字段解析的第一个步骤一样,也需要先解析出方法表类的class_index项中索引的方法所属的类或接口的符号引用,解析成功后进行类方法搜索
      (1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就抛出异常
      (2)如果通过了(1),在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束
      (3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束
      (4)否则,在类C实现的接口列表及它们的父接口中递归查找是否有简单名称和描述符都与目标想匹配的方法,如果存在,则说明类C是一个抽象类,查找结束,抛出异常
      (5)否则,宣告方法查找失败,抛出异常

    4. 接口方法解析
      先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,进行后续的接口方法搜索
      (1)如果在接口方法表中发现class_index中的索引C是个类不是接口,直接抛出异常
      (2)否则,在接口C中查找是否有简单名称和符号引用都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束
      (3)否则,在接口C的父接口中递归查找,知道java.lang.Object类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回方法的直接引用,查找结束
      (4)否则,宣告查找失败

  23. 初始化过程jvm的显式初始化相关。
    在这个阶段,根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,初始化阶段是执行类构造器<clinit>()方法(不是实例构造器,平时所说的是实例构造器)的过程

    <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量前面的静态语句块可以赋值但不能访问

    <clinit>()方法与类的构造函数(类的实例构造函数<init>)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法执行完毕,因此虚拟机上第一个执行的类是Object

    <clinit>()方法对类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()

    接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成<clinit>()方法,但执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定一个的变量使用时,父接口才会初始化。接口的实现类在初始化的时候也一样不会执行接口的<clinit>()方法

    虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地枷锁、同步,如果多个线程同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,知道活动线程执行<clinit>()方法完毕

  24. 类卸载的过程及触发条件。
    只有由用户自定义的类加载器加载的类是可以被卸载的。
    举个例子举个例子
    loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。

    如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期, MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。

    当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载; 如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)

    在类使用完之后,满足下面的情形,会被卸载:
    (1)该类在堆中的所有实例都已被回收,即在堆中不存在该类的实例对象。
    (2)加载该类的classLoader已经被回收。
    (3)该类对应的Class对象没有任何地方可以被引用,通过反射访问不到该Class对象。
    如果类满足卸载条件,JVM就在GC的时候,对类进行卸载,即在方法区清除类的信息。

  25. 三种类加载器,如何自定义一个类加载器?
    (1)启动类加载器:负责将存放在<JAVA_HOME>\lib目录中的或者被-Xbootclasspath参数所指定的路径中的且是虚拟机识别的类库加载到虚拟机内存中。无法被Java程序直接引用
    (2)扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中的或者被java.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用
    (3)应用程序类加载器:是ClassLoader的getSystemClassLoader()方法的返回值,一般也称为系统类加载器

    自定义类加载器:自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法

    自定义一个类加载器

  26. JVM内存分配策略,优先放于eden区、动态对象年龄判断、分配担保策略等。

    • 对象优先在Eden分配
      大多数情况下,对象在新生代Eden区分配,当Eden不够时,会触发一次Minor GC(新生代GC),也可能直接将新对象直接分配到老年代
    • 大对象直接进入老年代
      需要大量连续内存空间的Java对象(长字符串、数组)很容易导致内存还有不少空间就提前触发GC,所以可以直接在老年代分配
    • 长期存活的对象将进入老年代
      每个对象有一个对象年龄计数器(经过的GC次数,仍存活),当这个年龄到达一定的次数(默认15),这个对象就进入老年代
    • 动态对象年龄判定
      如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须达到最大限制次数(比如年龄为5,但是有多个,并且占用内存很大,它们的总和大于了Survivor的一半,那么它们中大于或等于5的都可以直接进入老年代)
    • 控件分配担保
      在新生代GC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,那么新生代GC是安全的,如果不成立,虚拟机会查看是否允许担保失败。如果允许,就继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次新生代GC,如果小于,则不允许冒险,就进行一次老年代GC(Full GC)
  27. 四种垃圾回收算法标记-清除、复制、标记-整理、分代收集。
    (1)标记-擦除:
    分为标记和清除两个阶段,首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象
    标记过程就是对象判定死亡过程
    标记擦除算法标记擦除算法
    (2)复制:
    将内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完后,就将存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
    这样使得每一次都是对整个半区进行内存回收,内存分配时就不用考虑碎片的情况了,只要一动栈顶指正,按顺序分配即可
    复制算法复制算法
    (3)标记-整理:
    针对老年代的算法,标记后,不是直接清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存
    标记整理算法标记整理算法
    (4)分代收集:
    根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,然后根据不同的特点采用最恰当的算法
    新生代中采用复制算法,老年代中采用标记-清理或标记-整理算法

  28. JVM中的垃圾回收器,新生代回收器、老年代回收器、stop-the-world概念及解决方法。
    新生代收集器:Serial收集器、ParNew收集器、Parallel Scavenge收集器
    老年代收集器:Serial Old收集器、Parallel Old收集器

    stop-the-world:在可达性分析枚举根节点时,在整个分析期间整个执行系统看起来就像被冻结在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,导致GC进行时必须停顿所有的执行线程
    这个是无法避免的,只有尽量减少停顿的时间,现在使用CMS收集器采用并发,所以用户感觉不到这个停顿

  29. java虚拟机的特性
    特点:
    java语言的重要特点是与平台无关性,java虚拟机是实现这一特点的关键。

    对比高级语言
    一般高级语言要想在不同平台运行,至少需要生成不同目标代码。而java虚拟机屏蔽了与具体系统平台信息,只要编译生成在java虚拟机运行的字节码,就可以在多种平台运行,不需要重复编译。

    解释字节码
    java虚拟机在执行字节码时,把字节码解释成具体平台的机器指令执行。

  30. JVM的引用树,什么变量能作为GCRoot?GC垃圾回收的几种方法
    通过一系列的“GC Roots”对象作为起始点,从这些节点开始向下搜索,搜索的路径成为引用链,当一个对象到GC Roots对象没有任何引用链相连,则说明此对象是不可用的
    可达性分析算法可达性分析算法

    Java语言中,可以作为GC Roots对象有:
    (1)虚拟机栈中引用的对象
    (2)方法区中类静态属性引用的对象
    (3)方法区中常量引用的对象
    (4)本地方法栈中JNI引用的对象

    回收算法:标记-擦除、复制、标记-整理、分代收集

  31. 简述字节码文件的组成
    任何一个Class文件都对应着唯一一个类或接口的定义信息,但类或接口不一定都得定义在文件里

    Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按顺序紧凑地排列在文件中,中间没有添加任何分隔符。当遇到占用8位字节以上空间的数据会按照高位在前的方式分割成若干个8位字节。包含了魔数、Class文件版本、常量池、访问标志、类索引、父类索引、接口索引集合、字段表集合、方法表集合、属性表集合

  32. 关于JAVA内存模型,一个对象(两个属性,四个方法)实例化100次,现在内存中的存储状态,几个对象,几个属性,几个方法
    一个对象,两个属性,四个方法
    只会在主内存中实例化对象,所有的变量都会存储在主内存中,其他工作线程的只会拷贝这些数据使用,且修改后要同步回来

  33. GC内存泄露
    判断对象是否可以回收,资源对象没关闭造成的内存泄露

    android GC内存泄露问题
    GC可达性实践-内存泄露分析

  34. 你能手动调用垃圾回收吗
    可以,System.gc()

  35. 软引用、弱引用区别
    弱引用的强度比软引用更弱一些,弱引用关联的对象只能生存到下一次GC之前,第二次一定会回收;软引用是将其列进第二次回收的范围中但不一定回收,当内存不足才会回收

  36. java四中引用
    强引用、软引用、弱引用、虚引用

  37. Java对象的引用方法
    四种引用:强引用、软引用、弱引用、虚引用

  38. 简单说说 Java 为什么需要不同的引用类型
    Java 的内存回收是虚拟机垃圾回收器来主动触发的,我们基本无法直接像 C 语言一样控制内存的实时申请释放操作,而在 Java 中有时候我们需要适当的控制对象被回收的时机,所以就诞生了引用类型,可以认为不同引用类型的诞生实际是对 GC 回收时机不可控的一种矛盾妥协;譬如我们可以利用软引用和弱引用解决 OOM 问题,通过软引用实现 Java 对象的高速缓存(即我们创建一个类,如果每次频繁操作都重新构建一个实例就会引起大量对象的消耗和 GC,如果通过软引用和 HashMap 结合实现高速缓存就能显著提供性能)

    Java 几种引用类型

  39. 简单说说 Java PhantomReference(虚引用/幽灵引用)的作用
    Java 垃圾收集过程中对象的可触及状态改变时垃圾收集器会把要回收的对象添加到引用队列 ReferenceQueue,这样在可触及性发生变化的时候上层代码就能得到通知,因为在 Java 中 finalize 方法本来是用来在对象被回收的时候来做一些操作的,但是对象被 GC 垃圾收集器什么时候回收是不固定的,所以 finalize 方法就很尴尬,故虚引用就可以解决这个问题,虚引用的作用就是在 GC 要回收时 GC 收集器把这个对象添加到 ReferenceQueue 中,这样我们如果检测到 ReferenceQueue 中有我们感兴趣的对象时则说明 GC 将要回收这个对象了,此时我们可以在 GC 回收之前做一些其他事情

  40. Class文件结构

    类型 名称 数量
    u4 magic 1
    u2 minor_version 1
    u2 major_version 1
    u2 constant_pool_count 1
    cp_info constant_pool constant_pool_count - 1
    u2 access_flags 1
    u2 this_class 1
    u2 super_class 1
    u2 interfaces_count 1
    u2 interfaces interfaces_count
    u2 fields_count 1
    field_info fields fields_count
    u2 methods_count 1
    method_info methods methods_count
    u2 attributes_count 1
    attribute_info attributes attributes_count
  41. 如果想不被 GC 怎么办
    保持与GC Root保持引用链

  42. java内存模型、导致线程不安全的原因。及操作,原子性可见性和有序性等。
    当无法保证操作变量的原子性、可见性、有序性等,容易导致线程不安全

    原子性:
    Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write,大致可以认为基本数据类型的访问读写具有原子性,lock、unlock也是,synchronized也具有原子性

    可见性:
    当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
    Java内存模型通过变量修改后同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现
    volatile、synchronized、final都可以实现可见性

    有序性:
    Java提供了volatile(本身禁止指令重排序)和synchronized(一个变量在同一时刻只允许一条线程对其进行lock操作)来保证线程间操作的有序性

  43. 描述一下进程回收的过程
    Android进程与Linux进程根据OOM_ADJ阈值进行区分,当Android系统察觉设备内存不足时,会按照阈值从大到小杀死进程

    (1)首先移除package被移走的无用进程.
    (2)基于进程当前状态,更新oom_adj值,然后进行以下操作.
    移除没有activity在运行的进程
    如果AP已经保存了所有的activity状态,结束这个AP.
    (3)最后,如果目前还是有很多activities 在运行,那么移除那些activity状态已经保存好的activity.
    当系统内存短缺时Android的Low Memory Killer根据需要杀死进程释放其内存

  44. 为什么现在流行分代?好处和优势在哪里?
    降低单次GC的时间长度,、提高GC的工作效率。内存管理可以以尽可能小的计算代价(20%)来维持内存的低碎片化(80% goal),尽量推迟要消耗更大的计算(80%)来整理最后那一点碎片问题(20%,因为老生代相对比较稳定)

    (1)分代回收可以对堆中对象采用不同的gc策略。在实际程序中,对象的生命周期有长有短。进行分代垃圾回收,能针对这个特点做很好的优化
    (2)分代以后,gc时进行可达性分析的范围能大大降低

  45. 简单说说 dexopt 与 dex2oat 的区别(JVM)
    jvmjvm
    通过上图可以很明显的看出 dexopt 与 dex2oat 的区别,前者针对 Dalvik 虚拟机,后者针对 Art 虚拟机。

    dexopt 是对 dex 文件 进行 verification 和 optimization 的操作,其对 dex 文件的优化结果变成了 odex 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码(譬如优化调用虚拟指令等)。

    dex2oat 是对 dex 文件的 AOT 提前编译操作,其需要一个 dex 文件,然后对其进行编译,结果是一个本地可执行的 ELF 文件,可以直接被本地处理器执行。

    除此之外在上图还可以看到 Dalvik 虚拟机中有使用 JIT 编译器,也就是说其也能将程序运行的热点 java 字节码编译成本地 code 执行。所以其与 Art 虚拟机还是有区别的,Art 虚拟机的 dex2oat 是提前编译所有 dex 字节码,而 Dalvik 虚拟机只编译使用启发式检测中最频繁执行的热点字节码

发布了43 篇原创文章 · 获赞 10 · 访问量 6998

猜你喜欢

转载自blog.csdn.net/baidu_36959886/article/details/82858438
今日推荐