4.1 内存区域

线程私有

  • 程序计数器:可以看做当前线程所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器。JVM通过改变程序计数器依次读取指令,在多线程情况下,程序计数器用于记录当前线程执行的位置。
  • 虚拟机栈:线程私有,生命周期和线程相同,除了Native方法外,其他所有Java方法的调用都是通过栈实现的。方法嗲用的数据需要通过栈进行传递,每次方法调用会有一个对应的栈帧被压入栈中,结束调用则弹出。
    • 栈帧内部有:局部变量表、操作数栈、动态链接、方法返回地址。
    • 局部变量表:存放编译期间可知的各种数据类型、对象引用。
    • 操作数栈:主要作为方法调用的中转站,存放方法执行过程中的中间计算结果。
    • 动态链接:主要服务一个方法需要调用其他方法的场景。
  • 本地方法栈:为虚拟机使用到的Native方法服务,在HotSpot虚拟机中,和Java虚拟机栈合二为一。
  • StackOverFlowError:如果栈的内存不允许动态扩张,那么当线程请求栈的深度超过虚拟机栈的最大深度,则会报出此错误。
  • OutOfMemoryError:如果栈的内存可以动态扩张,如果虚拟机在动态扩张栈时无法申请到足够的内存空间,则报出此错误。

线程共享

  • 堆:存放对象实例,几乎所有的对象实例都在此分配内存。垃圾收集器管理的主要区域,也被称为GC堆。
    • 字符串常量池:是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
  • 方法区:方法区会存储已经被JVM加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。(永久代和元空间实际上是方法区的具体实现,JDK1.8之前是永久代,而之后是元空间,永久代受到JVM内存的上限,而元空间受到本机可用内存限制,相对来说溢出可能性变小,并且永久代会给GC带来不必要的复杂度)
    • 运行时常量池:Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有存放编译期生成的各种字面量和符号引用的常量池表。常量池表户会在类加载后放到方法区的运行时常量池中。
  • 直接内存(非运行时数据区的一部分):直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。

4.2 垃圾回收

  • 新生代内存(Eden, S0, S1)
  • 老生代内存(Tenured)
  • 永久代内存(PermGen[JDK1.7],MetaSpace[JDK1.8])

4.2.1 内存分配和回收原则

内存分配

  • 对象优先在Eden区分配内存
  • 大对象直接进入老年代
  • 长期存活对象进入老年代(经历过一次GC后仍然存活,如果能被Survivor空间收纳,则进入s0或s1,并将对象年龄设置为1,之后每经历一次GC,年龄+1,默认到达15岁后进入老年代)【HotSpot虚拟机对对象按照年龄从小到大累计内存,当内存大小超过Survivor区一半时,重新设置晋升老年代年龄阈值为当前年龄和阈值年龄中的较小值】

回收原则

  • 部分收集:
    • 新生代收集:只对新生代进行收集
    • 老年代收集:只对老年代收集
    • 混合收集:对整个新生代和部分老年代进行收集
  • 整堆收集:收集整个Java堆和方法区

4.2.2 空间分配担保

为了确保Minor GC之前老年代本身还有容纳新生代所有对象的剩余空间。

发生Minor GC之前,JVM判断老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果成立则证明Minor GC是安全的,否则查看是否允许担保失败的参数,如果允许,则检查老年代连续空间是否大于历次晋升到老年代的平均大小,如果大于则进行Minor GC,否则改为Full GC。

4.2.3 死亡对象判断方法

  • 引用计数法(难以解决循环引用问题)
  • 可达性分析算法:
    • GC ROOT:JVM虚拟机栈(局部变量表)中引用的对象、本地方法栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象,所有被同步锁持有的对象,被JNI引用的对象。
    • 对象可回收不一定会被回收,第一次会被标记,被标记的对象会被放在一个队列中进行第二次标记,除非这个对象与引用链上任何一个对象建立链接,否则就会被回收。
    • 引用类型:
      • 强引用:具有强引用的对象不会被回收
      • 软引用:只具有软引用的对象如果内存空间不足,则会被回收
      • 弱引用:只有弱引用的对象,一旦被发现,则会回收
      • 虚引用:不会决定是否被回收,主要用于跟踪对象被垃圾回收的活动

4.2.4 垃圾收集算法

  • 标记-清楚算法:效率不高,会有内存碎片
  • 标记-复制算法:内存缩小为一般,如果存活对象过大,复制性能会变差,不适合老年代
  • 标记-整理算法:标记后,让所有对象移动到一端。
  • 分带收集算法:新生代采用“标记-复制算法”,老年代采用“标记-清楚算法”或“标记-整理算法”

4.2.5 垃圾收集器

  • Serial收集器
  • ParNew收集器
  • Parallel Scavenge收集器
  • Serial Old收集器
  • Parallel Old收集器
  • CMS收集器
  • G1收集器
  • ZGC收集器

4.3 类加载

  • 加载(通过类名获取此类的二进制字节流,将字节流所代表的静态存储结构转换为方法区的运行时数据,内存中生成一个代表该类的Class对象,作为方法区数据的访问入口)【由类加载器完成】
  • 验证(确保Class文件的字节流包含的信息符合约束)
  • 准备(为类变量分配内存并设置变量初始值)
  • 解析(将常量池内的符号引用替换为直接引用)
  • 初始化(执行clinit)
  • 卸载(所有实例对象被GC,没有任何地方被引用,该类的类加载器已被GC后,类会才可能被卸载)

4.3.1 类加载器

用于加载Java类的字节码

JVM内置BootstrapClassLoader(启动类加载器,没有父级,由C++实现)、ExtensionClassLoader(扩展类加载器)和AppClassLoader(应用程序类及载器,面向用户的加载器)

双亲委派机制,打破双亲委派机制可以通过重写loadClass实现。

4.4 对象创建

  1. 类加载检查:在常量池中检查能否找到该类的符号引用,并且检查该符号引用代表的类是否被加载过、解析和初始化过。如果没有则进行类加载过程。
  2. 分配内存:
    • 指针碰撞:堆内存规整的情况下,移动分解指针,分配对象内存
    • 空闲列表:堆内存不规整的情况下,JVM维护一个列表,记录哪些内存块是可用的,分配时找一块足够大的内存进行分配。
  3. 初始化零值,保证对象的实例字段在Java代码中不赋初始值就可以直接使用(不包括对象头)。
  4. 初始化对象头:JVM对对象机械能必要设置,比如对象是哪个类的实例,如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
  5. 执行init方法。