Giter Site home page Giter Site logo

notes-jvm's People

Contributors

dengchengchao avatar

notes-jvm's Issues

《深入理解JVM》 读书笔记第八章

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和执行的数据结构,栈帧主要存储了:

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 方法返回地址
  • 。。。

每一个方法的调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面入栈到出栈的过程。

方法调用

方法调用分为一下步骤:

  • 解析: 将方法的符号引用解析为内存地址

  • 分派:由于Java是一门面向对象的语言,因此需要多态调用方法。重载和重写的区别就在于一个是静态调用,一个是动态调用。

    JVM实现重载的过程如下:

    • 找到该方法对象的实际类型,记做为C
    • 如果在C中找到与该方法特征完全符合的方法,并且权限校验能通过,则返回这个方法的引用
    • 否则,按照继承关系从下往上依次对C的各个父类进行搜索和验证
    • 如果始终没有找到方法,则抛出AbstractMethodError异常。

    可以看出来,由于方法的每次动态调用都会涉及到依次搜索过程,但是由于动态调用是一个非常频繁的操作,因此为了节约性能,JVM会在方法区中建立一个虚方法表,虚方法表存放了各个方法的入口地址,如果某个方法子类没有被重写,那么子类方法与父类方法入口地址一致,如果重写了方法,则替换子方法的方法地址即可。

    虚方法表一般在类的加载的连接阶段进行初始化,在准备了类的变量初始值后,虚拟机会将方发表也初始化完毕。

《深入理解JVM》 读书笔记第三章

GC应该思考的3件事:

  1. 哪些内存需要回收?
  2. 如何回收这些内存
  3. 是什么时候回收这些内存

哪些内存需要回收?

首先需要明白的一点是,引用都是在栈上的,栈上的引用指向了堆中的对象,而方法执行都是依靠操作这些栈的引用进行,随着方法执行的完成,栈上引用也会随之回收。当没有引用指向某个对象的时候,这么对象则为无用对象,此时即可回收这个对象。

那么如何确定是否有引用指向某个对象呢?

  1. 引用计数法

    给一个对象添加一个引用计数器,每当有一个引用引用这个对象的时候,则计数器加一,引用失效的时候,计数器减一。当计数器为0的时候,则可以进行GC.

    但是这样无法解决的一个问题便是循环引用

  2. 可达性分析算法

    类似链表的遍历,算法通过一个称为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,所走过的路径称为引用链。当某个对象到GC Roots节点没有任何的引用链存在的时候,则证明此对象是不可用的。

    在Java中,会作为GC Roots的对象包括以下几种:

    • 虚拟机栈中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈JNI引用的对象

    注意:GC Roots 是可作为索引的引用的集合,也就说GC Roots里面存储的是引用

    2.2 对于引用的种类:

    • 强引用:只要引用存在,则永远不会被GC掉被引用的对象
    • 软引用(SoftReference):用来描述一些有用但是非必须的对象,在系统即将OOM的时候,才会考虑回收这些对象
    • 弱引用(WeakReference):用来描述非必须的对象,在系统进行下一次GC的时候,会回收掉这些对象
    • 虚引用(PhantomReference):最弱的一种引用关系,不会对对象的生存时间有任何影响,它的存在主要是在这个对象被GC的时候能够收到一个系统通知。

    2.3 finalize

    在经过可达性算法后,不可达的对象需要经过两次标记才会被真正的回收

    • 第一次:判断此对象是否需要执行finalize()方法,当对象没有覆盖 finalize()方法或者已经执行过 finalize()方法,那么虚拟机会视作没必要执行 finalize()方法
    • 对于需要执行 finalize()方法的对象,虚拟机会将其放在一个由虚拟机自动建立的,低优先的线程执行的 F-Queue队列中。虚拟机并不保证等待方法执行完成以防止过久等待。执行完该方法后,将该对象标记为没必要执行
    • 再次发现不可达并且没必要执行,则进行回收

    任何对象的 finalize()方法都只会被调用一次。

    不建议在 finalize()方法中做任何工作,对于资源回收,应该使用try-finally. finalize()方法具有不确定性,并且 finalize()方法具有不确定性,并且运行代价昂贵。

    2.4 回收方法区

    卸载/回收一个类的条件有3个

    • 该类的所有实例都已经被回收,也就是Java堆中不包含任何该类的任何实例
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有再任何地方被引用,无法在任何地方通过反射访问该类的方法

    对类的回收并不是无用即回收。HotSpot提供了参数进行配置什么时候卸载类。

回收的具体实现

回收具体实现的遇到的问题
  1. 可达性分析具体实现的问题

    • 对于可达性分析需要消耗大量的时间

    有可达性分析知道,GC Roots主要在全局性引用(常量或静态属性)和执行上下文(栈帧的本地变量表),而这些数据量都比较大,因此要逐个检查引用会消耗很多的时间

    • 检索GC Roots需要GC 停顿

    可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化

    就好想妈妈到扫卫生的时候,你不能再往地上扔垃圾,否则你就会挨骂~

    这种现象称为Stop The World

    Stop The World 是JVM 在后台自动发起和自动完成的,在用户不可见的情况下,把用户的正常工作线程全部停掉

  2. HotSpot JVM 对GC Roots查找的优化

    查找GC Roots的时候,一般主流的JVM都是准确式GC,可以直接得知哪些地方存放着对象的引用,所以在Stop The World的时候,并不需要全部、逐个检查完全局性和执行上下文的引用位置。

    HotSpot中,是使用的一组称为OopMap 的数据结构来实现准确式GC:

    在类加载的时候,计算对象内什么偏移量上是什么类型的数据

    在JIT编译时,也会记录栈和寄存器中哪些位置是引用

  3. OopMap带来的问题

    HotSpot在OopMap的帮助下可以快速且准确的完成GC Roots枚举,但是有这样一个问题

    :运行过程中,非常多的指令会导致引用关系变化,如果为这些执行都生成对应的OopMap,那么需要的空间成本太高

    问题解决:

    :只在特定的位置记录OopMap的引用关系,这些位置,则称为安全点(Safepoint)

    特定的位置指的是程序只能运行到安全点才能停下来进行GC

  4. 安全点的选定

    安全点不能太少,否则GC等待的时间太长;也不能太多,空间开销过大,因此安全点基本上是以是否具有让程序长时间执行为特征为标准选定,”长时间执行“最明显的便是指令序列复用:方法调用,循环跳转,循环的末尾,异常跳转等;

    使用安全点的规则,解决了OopMap对象过多的问题

  5. 如何在安全点上停顿

    虽然规定的安全点规则,但是如何在需要GC的时候让所有的线程到达安全点的时候再停顿呢?

    • 抢先式中断

    不需要线程配合,实现如下:

    1. 在GC发生的时候,首先中断所有的线程
    2. 检查如果发现有不在安全点上的线程,就恢复其运行直到到达安全点

    现在几乎没有JVM使用这种方式

    • 主动式中断

      在GC发生的时候,不直接操作线程中断,而是简单的设置一个标志,让各个线程主动去轮询这个标志,发现中断标志为真的时候就自己中断挂起,而轮询标志的地方和安全点是重合的,因此可以直接挂起

  6. 安全区(Safe Region)

    对于安全点(Safe Point)还有一个问题:对于程序没有CPU时间的时候(Sleep 或 Block 状态),无法运行到Safe Point 上中断再挂起,于是这个时候需要安全区。

    • 什么是安全区

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

    • 如何利用安全区

      线程进入安全区的时候,首先标志自己已经进入了安全区。

      线程被唤醒离开安全区的时候,检查系统是否已经完成的根节点枚举(或者整个GC),如果已经完成,则继续执行,否则必须等待,直到收到可以安全离开安全区的信号通知。

关于OopMap,安全点等,详细可以参考R大--找出栈上的指针/引用

如何进行回收

标记-清除算法

最基础的标记清除算法分为两个阶段,第一个阶段为标记出所有需要回收的对象,在标记完成后回收所有被标记的对象。

标记清除算法不足之处在于两点:

  • 效率:标记和清除的过程效率都不高
  • 空间:使用标记清除后会产生大量的内存碎片

复制算法

为了解决效率问题,在标记清除的基础上,产生了复制算法,复制算法将可用内存按照容量大小分为两个大小相等的块,每次都值使用其中的一块,当这一块使用完了以后,再复制存活的对象到另外一块中,然后再统一清理,这样的回收算法实现简单,运行高效。

HotSpot中并未按照1:1划分空间,而是将空间分为一块较大的Eden空间和两块较小的Survivor空间,每次使用只使用Eden和其中一块Survivor空间,当回收的时候,将剩余对象赋值到另外一块Survivor空间中,最后清理刚刚的控件。同时EdenSurvivor的比例为8:1:1

1/10的内存不够使用的时候,存活的对象会直接进入老年代。

标记-整理算法

复制算法有一个缺点便是在对象存活率较高的时候紧要进行较多的复制,这时效率便会降低,更加关键的是如果不是使用50%的空间,就需要进行空间担保。所以在老年代一般不使用这种算法。

标记整理算法是基于标记-清除算法的一种改进,对于标记过程仍然不变,不过清除的时候并不是直接清除,而是将存活的对象都向同一个方向移动,然后直接清理不其余的控件。

实际使用:分代收集算法

在实际使用的时候,结合各个收集算法的特点,一般商用虚拟机都会根据对象的存活周期将对象分为几块。一般把Java堆分为新生代和老年代。一般新生代中每次收集都会有大批对象死去,这时使用复制算法比较好,而在老年代中,由于对象的存活率比较高,因此一般使用标记-清理算法或标记整理算法。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。HotSpot主要提供了以下几种垃圾收集器,用于应对不同的场景。

Serial收集器

  • 区域:新生代

  • 类型:单线程收集器(只使用一个线程完成收集工作,并且会Stop the world)

  • 特点:使用复制算法

Serial收集器是最基本,发展历史最久的收集器。

Serial主要应用场景为:Client模式,相对于其他收集器而言,它的有点在于:简单而高效,因为是单线程运行,没有线程交互的开销,因此效率比较高。

ParNew 收集器

  • 区域:新生代

  • 类型:并行多线程收集器(多线程进行垃圾回收,并且会Stop the world)

  • 特点:使用复制算法

ParNew收集器作为Serial的多线程版本,除了使用多线程进行垃圾回收以外,其他的行为基本和Serial收集器一样。ParNew一般作为Server模式的虚拟机的首选新生代收集器,主要原因是只用它能和CMS收集器配合工作。

注意:ParNew由于是多线程收集器,因此它在单CPU下工作效率由于线程开销,一般不如Serial收集器。

Parallel Scavenge 收集器

  • 区域: 新生代
  • 类型: 并行多线程收集器
  • 特点: 使用复制算法

Parallel Scavenge的特点是它关注的是控制吞吐量,也就是CPU用于运行用户代码的时间和CPU总消耗的时间的比值。加入虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99%

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:

  • -XX:MaxGCPauseMillis: 控制最大垃圾收集停顿时间
  • -XX:GCTimeRatio : 设置希望的吞吐量大小

Serial Old 收集器

  • 区域: 老年代
  • 类型: 单线程
  • 特点: 使用标记-整理算法

Serial OldSerial收集器的老年代版本,它的主要意义在于给Client模式下虚拟机使用,如果在Server模式中,它还有两个用途:

  • 在JDK1.5以及之前的版本中与Parrallel Scavenge收集器搭配使用
  • 作为CMS收集器的后备方案

Parallel Old 收集器

  • 区域: 老年代
  • 类型: 并行多线程
  • 特点: 使用标记- 整理算法

Parallel OldParallel Scavenge收集器的老年代版本,它是在JDK 1.6中才开始提供,在此之前只能使用Serial Old收集器

CMS 收集器

  • 区域: 老年代
  • 类型: 并发多线程
  • 特点: 使用标记- 清除算法

CMSConcurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器。其运行主要包含以下4个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中,初始标记和重新标记这两个步骤仍然需要Stop The World,初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快。

CMS是一个比较优秀的收集器,并发收集,低停顿。但是其也有比较明显的3个缺点:

  • CMSCPU比较敏感,由于是并发的,因此CMS需要占用一部分CPU资源
  • CMS收集器无法处理浮动垃圾,并且可能出现Concurrent Mode Failure失败而导致Full GC的产生。
  • CMS收集器由于是标记-清除算法,因此会产生内存碎片

G1 收集器

  • 区域: 新生代+老年代
  • 类型:并行与并发
  • 特点:整体上看属于标记-整理 ,局部上看基于复制算法

G1 (Garbage-First)收集器是当今收集器技术发展的前沿成果之一,G1 具有如下特点:

  • 并行与并发: G1能充分利用多CPU,多核环境下的硬件优势,使用多CPU能缩短Stop The World的停顿时间,
  • 分代收集: G1不需要其他收集器就能完成整个GC堆的回收。
  • 空间整合: G1从整体上看是基于 标记-清理算法实现,从局部上看,属于复制算法,因此G1不会产生内存碎片
  • 可预测的停顿: G1可以建立可预测的停顿时间模型。

G1运行可以大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

初始标记和最终标记的时候,需要Stop The World

理解GC日志

打印GC日志参数: -XX:+PrintGCDetails

开启GC日志后,可以看到:

[GC (System.gc()) [PSYoungGen: 5243K->874K(76288K)] 5243K->882K(251392K), 0.0013200 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 874K->0K(76288K)] [ParOldGen: 8K->800K(175104K)] 882K->800K(251392K), [Metaspace: 3441K->3441K(1056768K)], 0.0050647 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 76288K, used 655K [0x000000076af00000, 0x0000000770400000, 0x00000007c0000000)
  eden space 65536K, 1% used [0x000000076af00000,0x000000076afa3ee8,0x000000076ef00000)
  from space 10752K, 0% used [0x000000076ef00000,0x000000076ef00000,0x000000076f980000)
  to   space 10752K, 0% used [0x000000076f980000,0x000000076f980000,0x0000000770400000)
 ParOldGen       total 175104K, used 800K [0x00000006c0c00000, 0x00000006cb700000, 0x000000076af00000)
  object space 175104K, 0% used [0x00000006c0c00000,0x00000006c0cc8158,0x00000006cb700000)
 Metaspace       used 3448K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

上面的日志一共两次GC

第一段:

  • [GC(System.gc())]: 说明GC 的类型是System.gc()触发。

​ 同时如果还带有[Full GC] 的话说明此次GCStop The World的。

  • [PSYoungGen] :表示GC发生的区域

    对于GC的区域,不同的收集器的命名不同。

    • Serial: DefNew全称为:Defaulr New Generation
    • ParNew : ParNew 全称为: Default new Generation
    • Parallel Svavenge : PSYongGen
  • 方括号内5243K->874K(76288K): GC前已使用内存容量->GC后已使用容量(总容量)

  • 方括号外5243K->882K(251392K): GCJava堆已使用容量->GCJava堆已使用容量(Java堆总容量)

内存分配与回收策略

  • 新生代GC:(Minor GC):指发生在新生代的垃圾收集动作。
  • 老年代GC:(Major GC/Full GC): 指发生在老年代的GC,出现了Major GC经常会伴随着至少一次Minor GCMajor GC一般比Minor GC慢10倍以上

内存分配策略:

  • 新生对象在Eden区中分配,当Eden区没有足够的控件的时候,虚拟机将发起一次Minor GC

  • 大对象直接进入老年代JVM提供了一个-XX:PretenureSizeThreshold参数,当对象大小大于这个设置值的时候,直接将大对象放入老年代,避免导致新生代内存不足频繁Minor GC

  • 长期存活对象进入老年代,对象每经过一个Minor GC依然存活的,年龄就加一,当年龄增加到一定程度(默认15,可以通过-XX:MaxTenuringThreshold设置),将会晋升到老年代中

  • 动态年龄判定,如果在Survivor空间中相同年龄的所有对象大小达到Survivor大小的一半,则年龄大于或等于该年龄的对象会直接进入老年代

  • 空间分配担保,在发生Minor GC之前,虚拟机需要使用老年代担保新生代空间足够使用,如果老年代最大可用连续空间小于新生代所有对象的空间,如果允许担保失败,则此时会尝试触发一次Minor GC,如果不允许担保失败,则此时会进行一次Full GC

    实际判断并不是使用老年代最大可用连续空间是否大于新生代所有空间,而是判断老年代最大可用来连续空间是否大于每一次晋升到老年代的大小的平均值。如果大于,则尝试Minor GC,如果GC失败,再进行Full GC

《深入理解JVM》 读书笔记第七章

  1. 类生命周期:加载-验证-准备-解析-初始化-使用-卸载,其中验证-准备-解析统称为解析,因此又可以称加载-连接-初始化

  2. 对于类的连接阶段,在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

    注意,虽然其他阶段,例如加载,虽然一定是按照顺序开始的,但是仅仅是开始,当加载开始后,可能加载还没完成就会执行其他操作

  3. JVM规定必须初始化的情况:

    • 遇到new , getstatic,invokestaic,putstatic这4条字节码指令的时候,如果类没有初始化,必须先触发初始化,这4条指令分别对应:
      • new一个对象
      • 读取或设置一个类的静态字段
      • 调用一个类的静态方法
    • 使用reflect包的方法对类进行反射调用的时候,如果类没有初始化,则先触发初始化
    • 当初始化一个类的时候,如果其父类还未初始化,则先触发其父类初始化
    • 当虚拟机启动的时候,应该先初始化执行main的类
    • 当使用JDK1.7 的动态语言支持的时候,如果最后解析结果包含REF_getStatic等。

    JVM规范规定有且仅有以上5类情况的时候,才会触发类初始化,类似下面的情况,不会触发类初始化:

    • 通过子类引用父类的静态字段,子类不会被初始化
    • 定义此类型的数组不会引起初始化
    • 常量能够通过编译期间直接替换的属性的引用不会初始化

    对于接口来说,也有接口初始化的过程,这过程大多数和类初始化差不多,不过唯一不同的是接口的类初始化不会导致其父类接口的初始化。

加载

加载是类加载的过程的一个阶段。加载主要需要完成3件事:

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

加载与连接部分内容是交叉进行的,加载尚未完成,连接阶段可能就已经开始了

连接

连接包括验证,准备,解析三个阶段。

验证主要是为了确保Class文件的字节流中包含信息符合当前虚拟机的要求,其主要包括:

  • 文件格式验证
    • 是否以魔数开头
    • 主次版本是否在当前处理范围之内
    • 常量池的常量中是否有不支持的类型
    • ...
  • 元数据验证
    • 这个类是否有父类
    • 这个类是否继承了不允许被继承的类
    • 这个类是不是抽象类
    • 。。。
  • 字节码验证
    • 保证任意时刻操作数栈的数据类型与指令代码都能配合工作
    • 保证跳转指令不会跳转到方法体意外的字节指令中
    • ...
  • 符号引用验证
    • 将符号引用转换为直接引用
    • 符号引用通过字符串描述能否找到对应的类
    • 符号引用的可访问性是否允许当前类被访问

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中分配。由于此阶段是类的加载阶段,和实例无关,因此这里分配内存和设置类变量都是针对的类,也就是static。主要进行以下两个工作:

  • 分配类变量的内存空间(在方法区中分配)
  • 设置类变量的默认值 (这里仅仅是默认值,而不是初始值)
  • 对于ConstantValue属性(final static),在准备阶段会被初始化为初始值。

解析

解析是虚拟机将常量池内的符号引用替换为直接引用的过程,其中符号引用指的是用符号来描述所引用的目标,符号可以是任意形式的字面量,

直接引用指的是直接指向目标的指针,相对偏移量或者一个能直接定位到目标的句柄。

虚拟机规范并没有规定解析阶段发生的具体时间。

初始化

在准备阶段中,分配了类变量的内存空间和初始值,在初始化阶段,会执行<clinit>()方法进行类初始化

  • <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作以及静态语句块中的语句合并产生的,其收集的顺序是由源文件中出现的顺序决定
  • <clinit>()方法与类的构造函数不同,它不需要显示的调用父类构造函数,虚拟机在执行<clinit>()方法之前,会保证其父类已经执行完毕
  • 虚拟机会保证<clinit>()方法在多线程环境中被正确的加锁,同步。如果多个线程同时初始化一个类,那么只有一个线程会执行<clinit>()方法,其他线程必须阻塞等待

类加载器

Java中,程序员可以自己定义实现类加载动作。因此对于任意一个类,都需要加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性

类和类加载器有着很大的关联性,回想回收方法区的时候,判断类是否能回收的条件之一便是加载这个类的类加载器已经被回收

如果两个类的类加载器不同,那么这个类对象equals(),isAssignableFrom()isInstance()方法的返回结果必定是false,其中也包括instanceof关键字

双亲委派模型

Java中类加载器分为以下3种:

  • 启动类加载器(Boostrap ClassLoader
  • 扩展类加载器(Extension ClassLoader
  • 应用程序类加载器(Application ClassLoader)

双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是将其委派给父类加载器去完成。

破坏双亲委派模型

有些时候,双亲委派模型可能需要被破坏

  • 对于一些框架来说,基础类可能需要调用用户的代码,而一些基础加载路径是固定的,用户代码只能由Application ClassLoader加载。

因此,Java设计者提供了Thread.getContextLoader()获取上下文类加载器,而这个类加载器默认便是Application ClassLoader

《深入理解JVM》 读书笔记第二章

JVM规范中规定,Java虚拟机所管理的内存包括以下几个运行时数据区域:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

程序计数器

程序计数器是一块比较小的内存空间,它可以看作是当前线程所执行字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。

程序计数器是唯一一个没有规定任何OutOfMemoryError的区域,并且它是线程私有的。

程序计数器的线程私有:为了在多线程情况下,线程切换后能恢复现场,每个线程都会有个自己的程序计数器

程序计数器只会记录虚拟机字节码执行的指令地址,对于执行本地方法,程序计数器只会显示Undefined

虚拟机栈

虚拟机栈 描述的是Java方法执行的内存模型:在每个方法执行的时候都会创建一个栈帧(Stack Frame),用来存放:局部变量,操作数,动态链接,方法出口等信息。

值得注意的是,局部变量表存放了编译器可知的各种基本数据类型和引用类型。其中64位长度的long和double类型会占用2个局部变量控件(Slot),其余的类型各占用一个。

其中一个Slot为“字宽”,对于32位虚拟机来说,一个”字宽“占4个字节,对于64位虚拟机来说,一个”字宽“占8个字节。 这与我们的认知:byte占用一个字节,short,char两个字节等是相违背的。具体原因见:Java基本类型的物理存储大小---R大的回答

当线程的栈的深度大于虚拟机所允许的深度的时候(比如无限递归)虚拟机栈会抛出 StackOverflowError异常。如果虚拟机栈可以动态扩展,而当扩展时无法申请到足够的内存的时候,虚拟机栈会抛出OutOfMemoryError异常

虚拟机栈也是线程私有的,每个线程都独享一个虚拟机栈以保证每个线程都正常执行。

本地方法栈

本地方法栈和虚拟机栈非常类似,不过本地方法栈是用来执行本地方法。由于非常相似,因此 HotSpot JVM 已经将这两个栈的实现合二为一。

本地方法栈和虚拟机栈的特性非常相似

Java 堆

Java虚拟机规范规定,所有实例对象以及数组都要在堆上分配,因此Java堆一般用来存储实例对象。

Java虚拟机规范规定,Java堆可以处于物理上不连续的物理空间,只要逻辑上连续即可,在实现的时候,可以将堆实现成固定大小,也可以实现成可扩展的。不过一般主流虚拟机都将Java堆实现为可扩展的。

Java堆是线程共享的,每个在对象的对象都可以被不同的线程访问并改变

如果在堆中没有足够的内存完成实例分配,并且堆也无法扩展,则会抛出OutOfMemoryError

方法区

方法区是用来存储已被虚拟机加载的Class的相关信息比如:类名,访问修饰符,字段描述,方法描述,常量池静态变量即时编译器编译后的代码等数据。虽然JVM规范将方法区描述为堆的一个逻辑部分,但是他却有一个别名:Non-Heap,目的是为了与Java堆区分开来。

在HotSpot JVM中,jdk 7以前是用永久代(Permanent Generation)来实现方法区,这样实现是为了便于使得方法区能像Java 堆一样收集垃圾。

但是由于永久代有-XX:MaxPermSize上限,而String intered对象同样是放在永久代中(方法区外),在使用大量类似String.inter()功能的方法后,容易引起永久代内存溢出异常进而影响到方法区。而其他方式实现的方法区却没有此问题。因此在JDK1.7 的HotSpot中,已经把原本放在永久代的字符串对象移至Java堆中。

JDK1.7改动汇总:
interned String => Java heap
Symbols => native memory
静态变量 => java.lang.Class对象的末尾(位于普通的Java heap中)

注意这些东西,按照JVM规范中,它们还是在方法区中,只是HotSpot将方法区的具体实现,修改到了各个不同的地方而已。

注意:“常量池”如果说的是SymbolTable / StringTable,这俩table自身原本就一直在native memory里,是它们所引用的东西在哪里更有意思。上面说了,7是把SymbolTable引用的Symbol移动到了native memory,而StringTable引用的java.lang.String实例则从PermGen移动到了普通Java heap。

相关连接:JDK7,HotSpot的String常量池放到了native memory,native memory是什么? - RednaxelaFX的回答 - 知乎

同时,方法区也是线程共享的内存,在运行时如果无法分配足够的内存,则会抛出OutOfMemoryError异常

扩展:jvm为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。

运行时常量池(Runtime Constant Pool)是方法区的一部分,它用于存放在编译期生成的各种字面量符号引用,运行时常量池是可动态扩展的。
这里需要总结下运行时常量池的特性:
运行时常量池是用于存放编译期间生成的各种字面量符号引用
字面量包括:

  • 文本字符串
  • 八种基本类型的值
  • 被声明为final的编译时常量

有些比较小的基本数据类型会直接嵌入指令中,比如int a=1;会直接编译为 iconst_1,这种值不会放入常量池,也就说常量池找不到1,但是如果是具有final修饰的final int a=1;这个时候1便会放入常量池中,而比较大的基本数据类型是会放在运行时常量池的(比如int a=1234567)。

符号(Symbol)引用包括:

  • 类和方法的全限定名称
  • 字段和名称的描述符
  • 方法的名称和描述符

还有一种常量池,叫做常量池技术。比如Integer,Short等,在[-128,127],(127可以通过配置扩大)之间能够缓存对象,这种常量池应该和运行时常量池区分开理解,基本类型包装类的常量池技术是通过包装类本身通过在static代码块中初始化256个对象实现的,而静态变量又是放在方法区中,所以包装类的常量池是放在方法区中的。

DoubleFloat并没有实现常量池,因为不确定[-128,127]中一共应该缓存多少个数

总结:

区域名称 作用 是否线程共享
程序计数器 作为当前线程所执行的字节码行号的指示器
虚拟机栈 存放局部标量表,操作数栈,...等,用来描述Java方法执行内存模型
本地方法栈 用来描述执行本地方法内存模型
Java 堆 存放Java对象实例以及数组
方法区 存放已加载的内存信息,静态变量,常量等

直接内存

值得一提的还有直接内存,直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,这块内存称为直接内存,也叫堆外内存。不过它并不搜GC的管理,也不受Java堆大小的限制,只受本机总内存大小以及处理器寻址空间的限制。

HotSpot 对象

  1. 对象的创建包含有内存划分等步骤,其中内存划分是通过CAS加上失败重试的方式保证线程安全。另外一种方式是TLAB,TLAB是为每个线程分配单独的内存空间。

  2. 在HotSpot虚拟机中,对象在内存中可以分为3块区域:对象头(Header)、实例数据(instance Data)和对齐填充

  • 对象头

    • 包含一个对象运行时的本身数据,比如哈希码。GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等

    • 类型指针(非必须),虚拟机通过类型指针来确定对象是那个类的实例

    • 如果对象是数组,还包括一个记录数组长度的数据

  • 对齐填充

    • HotSpot VM自动内存管理要求对象起始地址必须是8字节的整数倍,不足的使用字节填充对齐
  1. 对象访问定位:前面说的类型指针(非必须)是因为对象访问定位包含两种方式

    • 通过句柄池记录:在Java堆中划分一块内存作为句柄池,这样而所有的引用指向的不是对象的地址,而是句柄,每个句柄都相应的记录了对象的实例数据以及具体类型
    • 通过直接指针访问:引用直接指向对象,而对象存储具体的类型

    类信息即虚拟机已加载的类型信息,存储在方法区中

    上面两种方法各有优势,句柄访问的好处是对象在GC时被移动后,不用修改引用的值,而直接访问的优势是速度更快。HotSpot VM使用的是直接指针访问

《深入理解JVM》 读书笔记第十二章

Java 内存模型

Java内存模型规定所有变量都存储在主内存中,同时每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中变量。不同线程之间无法直接访问对象工作内存中的变量。线程间的变量值通过主内存来完成。

线程对变量的操作主要包括:

- lock     锁定
- unlock   解锁
- read     读取
- load     载入
- use      使用
- assign   复制
- store    存储
- write    写入

其中,Java内存模型规定了以上操作的必须执行的顺序。

返回为happends-before来看,主要如下:

Volatile

对于volatile变量,它具有两个特性:

  • 保证此变量对所有线程的可见性

    虽然保证了可见性,但是并不保证要原子性,注意Java大多数操作都不是原子的

  • 禁止指令重排序优化

  • 保证对long/double的操作是原子的

final

JVM保证对于final修饰的变量,在访问的时候一定初始化完毕了。

Happends-Before

  • 程序次序规则: 在一个线程内,按照程序代码的顺序,书写在前面的程序happends-before书写在后面的程序
  • 线程锁定规则: 同一个锁的unlock操作happends-before锁的lock操作
  • volatile变量规则: 对于一个volatile变量的写操作happends-before对这个变量的读操作
  • 线程启动规则: 一个线程的start()操作happends-before其他线程在start()这个线程之前的操作
  • 线程终止规则:一个线程的所有操作都happends-before这个线程结束后的操作,比如thead.join()
  • 线程中断规则:一个线程的所有操作都happends-before这个线程中断后的操作
  • 对象终结规则:一个对象初始化完成happends-before它的finalize()的开始
  • 传递性规则:happends-before是可传递的

上面所说的happends-before,指的是只要存在happends-before关系,那么前面的操作对后面的操作是具有可见性和有序性的。

《深入理解JVM》 读书笔记第六章

  1. JVM魔数: Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件能否成为一个能被虚拟机接受的Class文件,JVM魔数为0xCAFEBABE

  2. Class前4个字节存储为魔数,后4个字节存储为Class文件的版本号。其中第5和第6个字节是次版本号,第7和第8个字节为主版本号。主版本号从45开始

  3. Class文件前面依次为魔数,版本号,常量池。

    常量池可以理解为Class文件的资源仓库,主要存放两大类常量:

    • 字面量
      • 文本字符串
      • 声明为final的常量值
    • 符号引用
      • 类和接口的全限定名
      • 字段的名称和描述符
      • 方法的名称和描述符

    常量池的作用:Java在编译的时候,并不想C/C++一样有link这一步骤,而是在虚拟机加载Class文件的时候动态加载,也就是说在Class文件中不会保存各个方法,字段的最终内存布局信息,因此这些字段,方法的符号引用需要在运行期转换为对应的内存入口地址。

    当虚拟机运行的时候,需要从常量池获取对应的符号引用,再在类创建的时候或者运行时解析,翻译到具体的内存地址之中。

  4. 常量池之后,紧接着便是一个类访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:Class是类还是接口,是否定义为public类型,是否定义为abstract类型,是否被声明为final类型。

  5. 类索引,父类索引,接口索引用于确定这个Class的继承关系。

  6. 字段表集合用于描述接口或者类中声明的变量

    对于字段表,都使用简单的标识字符描述字段类型:

    - Bbyte
    - Cchar
    - D: double
    - F:  float
    - I : int
    - Jlong
    - S : short
    - Z:  boolean
    - V:  void
    - L: 引用,如Ljava/lang/Object
    数组:[ 如[B 标识byte数组

    这里值得记忆的就是冲突了的类型,如long 为 J,boolean 为Z

    其中,字段的字符符号,被保存在常量池中,Class文件通过描述信息只保存索引。

  7. 方法表用于描述接口或类中定义的方法

    • 在Java中,方法特征名包括参数,方法名,但是不包括返回值
    • JVM中,方法特征名包括参数,方法名以及返回值

    也就是说,在JVM中,返回值不同的方法是可以重载的

  8. JVMfinally字段的处理:在JDK1.5之前,finally是通过指令跳转实现,JDK1.5之后优化为了异常表

    JVMfinally的处理是通过将finally中代码拼接trycatch中,也就是说对于下面的代码:

    public static int test(){
        int x=0;
        try {
            x=1;
            return x;
        } catch (Exception e) {
            x=2;
            return x;
        } finally {
            x=3;
        }
    }

    但是JVM执行的效果是类似如下:

        public static int test(){
            int x=0;
            try {
                x=1;
                return x;
                x=3;
            } catch (Exception e) {
                x=2;
                return x;
                x=3;
            } 
        }

指令

  1. JVM没有单独对byte,short,char,boolean的指令,他们都是使用的int的指令类型,在使用的时候,byte,short会被带符号扩展为int类型,char,boolean会被零位扩展为int类型
  2. 将一个局部变量加载到操作栈:iload,iload_<n>,lload,lload_<n>,fload,fload_<n>,dload,dload_<n>,aload,aload_<n>
  3. 将一个数值从操作数栈存储到局部变量表:store
  4. 将一个常量加载到操作数栈:push,ldc,ldc_w,aconse_null,iconst_m1,
  5. 扩充局部变量表的访问索引的指令:wide
  6. 运算指令:
    • 加法:add
    • 减法: sub
    • 乘法:mul
    • 除法:div
    • 求余:rem
    • 取反:neg
  7. 方法调用指令:
    • invokevirtual: 调用对象的实例方法
    • invokeinterface: 调用接口方法
    • invokespecial:调用需要特殊处理的方法
    • invokestatic:调用静态方法
    • invokedynamic:调用动态方法

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.