# 深入理解 Java 虚拟机 (一)

深入理解 Java 虚拟机 (一)

文章目录

  • 深入理解 Java 虚拟机 (一)
    • Java内存区域与内存溢出异常
      • 运行时数据区域
        • 程序计数器
        • Java虚拟机栈
        • 本地方法栈
          • 堆内存划分
          • 垃圾回收简介
          • 垃圾回收过程
        • 方法区
        • 运行时常量池
        • 直接内存
        • Jvm参数选项
      • HotSpot 虚拟机对象探秘
        • 对象的创建
        • 对象的访问定位
          • 句柄访问
          • 指针访问
      • 实战:OutOfMemoryError 异常
        • 堆溢出
        • 虚拟机栈和本地方法栈溢出
    • 垃圾收集器与内存分配策略
      • 对象已死
        • 引用计数法
        • 可达性分析法
          • 可以做根节点的对象
        • Java 引用
      • 垃圾收集算法
        • 标记清除算法
          • 缺点
        • 标记复制算法
        • 标记整理算法
    • 虚拟机类加载机制
      • 类加载的时机
      • 类加载的过程
        • 加载
        • 验证
        • 准备
        • 解析
        • 初始化
        • 使用
        • 卸载
      • 类加载器
        • 双亲委派模型

Java内存区域与内存溢出异常

  • 虚拟机自动管理内存。

运行时数据区域

# 深入理解 Java 虚拟机 (一)_第1张图片

程序计数器

  • 当前线程所执行的字节码的行号指示器,存储指向一条指令的地址。
  • 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
  • 程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

Java虚拟机栈

  • 每个线程在创建时候会创建一个虚拟机栈,内部有栈帧。
  • 生命周期和线程一致,线程结束虚拟机栈销毁
  • 线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackoverflowError 异常。
  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

本地方法栈

  • 本地方法栈和虚拟机栈一样本地方法栈为虚拟机使用到的本地Native方法服务

  • 堆是一块大内存,是启动JVM执行Java程序时自动分配的
  • 线程共享
  • Java堆是垃圾收集管理器管理的内存区域
  • Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的
  • java堆的大小是可扩展的, 通过-Xmx-Xms控制。
堆内存划分

# 深入理解 Java 虚拟机 (一)_第2张图片

  • 堆大小=新生代+老年代
  • 堆的大小可通过参数–Xms(堆的初始容量)、-Xmx(堆的最大容量) 来指定。
  • Edem : from : to = 8 : 1 : 1
垃圾回收简介
  • java堆是GC垃圾回收的主要区域。 GC分为两种: Minor GCFull GC
  • Minor GC是发生在新生代中的垃圾收集动作, 所采用的是复制算法。
垃圾回收过程
  • 对象在Eden出生后,经过一次MinorGc后,如果对象还存活,并且能够被To区域所容纳。
  • 使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1
  • 之后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 ,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
  • 但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

方法区

# 深入理解 Java 虚拟机 (一)_第3张图片

  • 保存加载过的每一个类的信息、保存静态变量、字符串常量池
  • 方法区是线程共享的

运行时常量池

  • JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池
  • 运行时常量池是方法区的一部分
  • 运行时常量池的作用是存储java class文件常量池中的符号信息
  • 运行时常量池中保存着一些class文件中描述的符号引用,在类的解析阶段还会将这些符号引用翻译出直接引用(直接指向实例对象的指针,内存地址),翻译出来的直接引用也是存储在运行时常量池中。

直接内存

  • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现

Jvm参数选项

参数 含义 默认值 说明
-Xms 初始堆大小 物理内存的1/64
-Xmx 最大堆大小 物理内存的1/4
-Xss 每个线程栈的大小 在相同物理内存下,减小这个值能生成更多的线程,操作系统对一个进程内线程数有限制,等同于-XX:ThreadStackSize
-Xmn 新生代大小 -XX:NewSize设置新生代大小和 -XX:MaxNewSize 新生代最大值含义一致,推荐使用-Xmn``
-XX:MaxMetaspaceSize 设置元空间最大值 默认值是-1 JDK1.8之前是永久代的概念,参数是-XX:MaxPermSize
-XX:MetaspaceSize 设置元空间初始值 默认值是21M JDK1.8之前是永久代的概念,参数是 -XX:PermSize
-XX:MaxDirectMemorySize 最大直接内存大小 -1 JDK1.8开始支持的参数
-XX:NewRatio 老年代与新生代的比值 默认值2 新生代(包括Eden和两个Survivor区)
-XX:SurvivorRatio Eden区与Survivor区的大小比值 默认值8 设置为8,则两个Survivor区与一个Eden区的比值为2:8
-XX:PretenureSizeThreshold 对象超过多大是直接在老年代分配 默认值0(先在新生代创建) 新生代采用Parallel Scavenge GC时无效
-XX:MaxTenuringThreshold 垃圾最大年龄 默认值15 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代
-XX:PrintCommandLineFlags 启动的时候打印JVM参数 默认关闭
-XX:-UseBiasedLocking 不启动偏向锁 默认启用 JDK1.6开始支持偏向锁,默认启用
-XX:+AggressiveOpts 启动性能优化参数 默认关闭 优化功能参数是属于实验性功能,预计将在即将发布的版本中成为默认功能
-XX:+DisableExplicitGC 关闭System.gc() 默认启动
-Xnoclassgc 禁用类垃圾回收 默认关闭
-XX:ParallelGCThreads 并行收集器的线程数 此值最好配置与处理器数目相等
-XX:+UseAdaptiveSizePolicy 自动选择年轻代区大小和相应的Survivor区比例 UseParallelGC垃圾收集器默认启动了AdaptiveSizePolicy
-XX:MaxGCPauseMillis 垃圾回收的最长时间(最大暂停时间ms) 软目标,JVM尽力调整配置达到
-XX:GCTimeRatio 设置垃圾回收时间占程序运行时间的百分比 公式为1/(1+n)

HotSpot 虚拟机对象探秘

对象的创建

  • 但遇到new指令时,先去检查指令的参数是否能在常量池中定位到一个类的符号引用。
  • 检查符号引用代表的类是否已经被加载、解析和初始化过,如果没有先执行响应的类加载过程
  • 类加载检查通过后,为新生对象分配内存
  • 如果内存是规整的那么只需要进行指针碰撞,如果内存不规整虚拟机需要维护一个列表,分配的时候找到一块足够大的空间划分给对象实例

对象的访问定位

句柄访问
  • 在堆内存中划分一个区域存放句柄池,java 栈中存储的refrence 指向的就是句柄池的地址,句柄池里存放的是java 对象实例的地址,和java类型数据的具体信息。
指针访问
  • refrence 里存储的是堆中java 具体实例对象的地址,而堆中java 具体的实例对象自关联到方法区中的类型数据具体信息。

实战:OutOfMemoryError 异常

堆溢出

  • 不断的创建对象

虚拟机栈和本地方法栈溢出

  • 线程请求的栈深度大于虚拟机所允许的最大深度
  • 当扩展栈容量无法申请到足够的内存时候

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

对象已死

引用计数法

  • 在对象中添加一个计数器,如果引用一次该对象则计数器加1,引用失效后减1,任何时刻计数器为0的对象就是不能在被使用的

可达性分析法

  • 根据引用关系向下搜索,搜索过程中走过的路径称为引用链,如果某个节点到根节点没有任何引用链,则证明此对象是不可能再被使用的。
可以做根节点的对象
  • 在虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

Java 引用

  • 强引用:如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为nullJVM在合适的时间就会回收该对象
  • 弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference
  • 软引用:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
  • 虚引用:虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期

垃圾收集算法

标记清除算法

  • 标记出所有需要回收的对象,标记完成后统一回收掉所有被标记的对象
缺点
  • 执行效率不稳定,如果又大量的需要回收,需要大量标记和清除的动作
  • 内存空间碎片化问题,标记清除后产生大量不连续的内存碎片,导致触发另一次垃圾收集动作

标记复制算法

  • 为了解决标记清除算法面对大量可回收对象执行效率低的问题
  • 将存活的对象复制到另一块内存上,然后一次性清除掉已使用过的内存空间
  • 这种复制算法的代价是将可用内存缩小为原来的一半空间浪费严重

标记整理算法

  • 根据老年代的特点,对标记清除进行改进,提出了标记整理算法。标记整理算法的标记过程与标记清除算法相同,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

虚拟机类加载机制

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

类加载的时机

  • 类的生命周期
    # 深入理解 Java 虚拟机 (一)_第4张图片

类加载的过程

加载

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的Class对象

验证

  • 确保Class文件的字节流中包含的信息符合虚拟机规范的约束要求,保证代码运行时不会危害虚拟机自身

  • 文件格式验证

  • 字节码验证

  • 元数据验证

  • 符号引用验证

准备

  • 为类变量分配内存并且设置类变量的初始值

解析

  • Java虚拟机将常量池内的符号引用替代为直接引用的过程

初始化

  • 用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。
  • 遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

使用

  • JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

卸载

  • 当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

类加载器

双亲委派模型

  • 如果一个类加载器收到加载类的请求,它首先不回自己去尝试加载某个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,只有当父加载器无法完成加载时子加载器才会尝试自己完成加载。
    # 深入理解 Java 虚拟机 (一)_第5张图片

  • 启动类加载器:负责加载JavaHome\lib目录

  • 扩展加载器:负责加载JavaHome\lib\ext目录,负责加载扩展类

  • 应用程序加载器:负责加载用户类路径上所有的类库,一般情况下这个就是程序中默认的类加载器
    尝试加载某个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,只有当父加载器无法完成加载时子加载器才会尝试自己完成加载。

你可能感兴趣的:(读书笔记,java,jvm,开发语言)