集册 Android&Java 技术笔记 《深入理解JVM》

《深入理解JVM》

—— 深入理解Java虚拟机

欢马劈雪     最近更新时间:2020-08-04 05:37:59

196

第二部分 自动内存管理机制

第二章 Java内存区域与内存溢出异常

  • JVM内存区域
    • 程序计数器:类似x86 EIP,每个线程都有一个程序计数器;执行native代码时计数器值为空;唯一不会抛出OOM的区域;
    • Java虚拟机栈:线程私有;即函数调用栈,保存函数局部变量;
    • Native方法栈:执行Native代码的函数调用栈;
    • Java堆:新创建的对象存放区域;
    • 方法区:保存已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;有以永久代实现此区域的,也有以独立GC实现的;
    • 运行时常量池:是方法区的一部分;String.intern()方法有可能对此区域造成大的影响(依赖于JVM实现);
    • 直接内存:NIO模块,为了提高内存访问效率,直接分配堆外内存,直接操作;
  • HotSpot虚拟机对象创建
    首先检查类的符号引用是否在常量池中,并检查该类是否已加载、解析、初始化;如果没有,则先进行加载;
    然后为对象分配内存空间,对象所占空间在类加载完成后即可确定;
    堆内存管理大致有两种方式:指针碰撞;空闲列表。依赖于GC时是否会对回收的内存进行压缩整理;
    分配内存的同步性保证:分配过程原子化;将内存分配按照线程划分在不同的空间中(本地线程分配缓冲,TLAB)。后者可通过JVM参数控制;
    内存分配完后,将内存空间初始化为零值,不包括“对象头”,初始化工作可以提前至TLAB分配时进行;
    设置“对象头”,例如:对象是哪个类的实例,类的元数据信息地址,对象hash值,GC分代年龄等信息;根据虚拟机当前的运行状态不同,是否启用偏向锁等,对象头会有不同设置方式;
    执行类的<init>方法;
  • HotSpot虚拟机对象的内存布局
    分为三部分:Header,实例数据,对其填充;
    实例数据部分,相同宽度的字段会分配到一起;满足该前提下,父类的变量出现在子类前;CompactFields参数为true时,子类中较窄的变量可能会插入到父类变量的空隙之中;
    对象大小8字节对其;
  • 对象的访问定位
    在堆上建立了对象,使用的时候基本都是通过栈上的reference来操作堆上的对象;
    目前主流有两种访问方式:使用句柄;直接指针;
    • 使用句柄
      在堆上划分一块内存作为句柄池,reference指向的是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息;句柄地址是稳定的,对象移动时只需修改句柄的内容,不必修改reference;
    • 直接指针
      reference直接指向堆上对象地址;访问速度快,只需一次指针定位;HotSpot使用直接指针方式;
  • 各种可能的OutOfMemoryError异常
    • 堆溢出
      java.lang.OutOfMemoryError: Java heap space...

      log中明确指出heap space,可通过JVM参数设置堆大小、最大大小、发生OOM后dump堆上数据;
      一般通过分析dump数据,确认内存中的对象是否必要,即确定是内存泄漏还是内存溢出;
      如果是内存泄漏,可进一步查看泄漏对象到GC Roots的引用链,根据引用链,基本就能定位泄漏代码的位置了;
      如果是内存溢出,则可通过增加JVM堆内存、审查代码,减少对象生命周期,减少程序运行期间的内存消耗;

    • 虚拟机栈与本地方法栈溢出
      StackOverFlowError,OutOfMemoryError
      当每个线程分配的栈容量越大时,发生StackOverFlowError时创建的线程数越少,因为每个进程的内存是有限的,栈容量越大,则允许的线程数越少;通过减少线程数、更换64位虚拟机(增加内存)、减少最大堆和栈容量,均可缓解这一错误;
    • 方法区与运行时常量池溢出
      java.lang.OutOfMemoryError: PermGen space...

      永久代区域内存大小可以配置,如果方法区以永久代实现,则String.intern()方法就可能导致该错误;
      类的卸载条件非常苛刻,如果运行时使用了大量的动态生成类,有可能导致该错误;

    • 直接内存溢出
      java.lang.OutOfMemoryError: ...

      在dump文件中看不出明显异常;
      直接内存区域可以通过JVM参数配置,默认与堆最大值一样;

第三章 垃圾收集器与内存分配策略

  • 是否可被回收
    • 引用计数法
      为每个对象添加一个引用计数器,当计数器为0时,可被回收;Python等语言使用,Java不是;无法解决循环引用的问题;
    • 可达性分析
      从GCRoots出发,对所有的对象引用关系图进行一次遍历,不可达的对象可被回收;Java使用;GCRoots包括:虚拟机栈中引用的对象;方法区中静态类属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI引用的对象;
    • 四种引用类型
      • 强引用
        普通赋值即为强引用;只要有强引用,就不会被GC;
      • 软引用(SoftReference)
        用来引用那些有用,但非必须的对象;在系统将要发生内存溢出错误之前,会将软引用指向的对象列入回收范围并进行二次回收,如果仍内存不足,将抛出OOM;
      • 弱引用(WeakReference)
        作用与软引用类似,但强度更弱;GC过程中,无论内存是否足够,只被弱引用指向的对象,都将被回收;
      • 虚引用(PhantonReference)
        对对象的生存周期完全没有影响,也无法通过虚引用来获取对象实例,仅仅能在对象被回收时,得到一个系统通知(只能通过是否被加入到ReferenceQueue来判断是否被GC,这也是唯一判断对象是否被GC的途径);
    • finalize()方法
      当可达性分析结果为不可达时,GC器首先会判断是否需要执行finalize()方法,只有当类重写了finalize方法,且该对象的finalize方法未被调用,才需要执行;
      finalize方法被放在一个虚拟机自动建立、低优先级的线程调用,且不保证执行完毕;
      不需要执行finalize时,将被GC;
      在finalize方法中,对象可以通过把自己挂到GCRoots引用链上进行GC逃逸,但只有一次机会;
      而如果使用虚引用,则可以避免GC逃逸的问题出现,这也是虚引用的第二个使用场景;
      千万不要把finalize()方法和C++的析构函数等同;
    • 回收方法区
      包括常量、类,这两部分;
      常量与普通对象类似,判断引用状态即可;
      类的判断比较苛刻:该类的所有对象均已被回收;加载该类的ClassLoader已被回收;该类对应的java.lang.Class对象未在任何地方被引用。而且这只是充分条件;
      在使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI等频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以避免方法区溢出;
  • 垃圾收集算法
    • 标记-清除算法
      第一阶段,判定对象可回收之后,将其标记,第二阶段,统一回收被标记的对象;
      两个阶段效率都不高;另外存在碎片化问题;
    • 复制算法
      将可用内存均分为两块,每次只使用其中一块,当一块用完之后触发GC,将存活的对象复制到另一块,再一次性清理已使用的一块;
      效率高,不存在内存碎片;但是内存缩小一半,空间开销大;
      现代商用JVM均使用复制算法来回收新生代,HotSpot虚拟机默认将内存分为8:1:1三块,一块Eden空间,两块较小的Survivor空间,每次使用Eden和一块Survivor,用完后将存活对象复制到另一块Survivor空间,集中回收已用部分;当一块Survivor不够复制时,将通过分配机制进入老年代;
    • 标记-整理算法
      标记过程同“标记清除算法”,标记结束后,通过将所有存活对象都向一端移动,然后直接清理边界外的内存;
    • 分代收集算法
      现代商用JVM均采用分代收集算法;一般把堆分为新生代和老年代;
      新生代一般98%的对象都会很快被回收,使用复制算法;
      老年代使用标记-清理,或者标记-整理算法;
  • HotSpot的算法实现
    ...

第七章 虚拟机类加载机制

展开阅读全文