Java-深入理解JVM虚拟机学习笔记

摘要

《深入理解JVM虚拟机》学习笔记

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

内存分区

名称 线程 功能 异常
程序计数器 私有 字节码的行号指示器 ;如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。 /
虚拟机栈 私有 生命周期与线程相同。 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、 操作数栈、 动态链接、 方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程 StackOverflow OutOfMemory
本地方法栈 / 与虚拟机栈类似,为Native方法服务 同上
共享 存放对象实例,分配内存。可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可 OutOfMemory
方法区 共享 储存被加载的类信息,常量,静态变量,即时编译器编译后的代码等。 OutOfMemory
运行时常量池 方法区的一部分, JDK7开始常量池被移到了堆中 OutOfMemory
直接内存 它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。 OutOfMemory

一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),每个java程序都需要运行在自己的JVM上,然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。那么程序开始运行后,都是如何涉及到各内存区域的呢?

概括地说来,JVM初始运行的时候都会分配好 Method Area(方法区)Heap(堆) ,而JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , **VM Stack(虚拟机栈)和Native Method Stack (本地方法栈), **当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)的原因。


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

对象已死吗

名称 实现 利弊 使用
引用计数法 给对象添加一个引用计数器,引用一次计数器加1,引用失效计数器减1。任何计数器为0的对象不可能再被使用 很难解决对象之间相互循环引用的问题,java虚拟机不使用 Python,FlashPlayer等(Netty4引入)
可达性分析法 一个对象到GC Roots没有任何引用链相连(即GC Roots到这个对象不可达),则对象不可用。(虚拟机栈、方法区中类静态属性引用的和常量引用的对象、本地方法栈中JNI引用的对象均可作为GC Roots) / 主流商用程序语言

垃圾回收算法

名称 实现 利弊 适用
标记-清除 先暂停整个程序的全部运行线程,让回收线程以单线程进行扫描标记,并进行直接清除回收,然后回收完成,再恢复运行线程。 效率低;产生内存碎片 老年代
标记-整理 和“标记-清除”相似,不同的是,该算法在回收期间会同时将保留下来的对象移动聚集到连续的内存空间,从而避免内存空间碎片。 对象的移动是需要时间成本的。 老年代
复制 内存空间分成两个部分。每次只使用其中一块,清理时把存活的放到另一块,这块整块清理。 避免了内存碎片;但是内存浪费;复制操作 新生代
分代收集 把java堆分为老年代和新生代,根据各年代特点采用不同算法

传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。如下图所示:

ss


并行和并发

垃圾收集器

总结:新生代基本采用复制,老年代基本采用标记-整理,CMS采用标记-清除。

收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景 Jdk 其他
Serial 串行 新生代 复制算法 速度优先 单CPU环境下的Client模式 1.3
ParNew 并行 新生代 复制算法 速度优先 多CPU环境时在Server模式下与CMS配合 1.4 Serial的多线程版本
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务 1.4
Serial Old 串行 老年代 标记-整理 速度优先 单CPU环境下的Client模式、CMS的后备预案 1.5 Serial的老年代版本
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务 1.6 Parallel Scavenge的老年代版本
CMS 并发 老年代 标记-清除 速度优先 集中在互联网站或B/S系统服务端上的Java应用 1.5 只能和Serial和parNew合作
G1 并发 both 标记-整理+复制算法 速度优先 面向服务端应用,将来替换CMS 1.7

内存分配与回收策略

  1. 新生代:所有对象创建在新生代的Eden区,当Eden区满后触发新生代的Minor GC,将Eden区和非空闲Survivor区存活的对象复制到另外一个空闲的Survivor区中。保证一个Survivor区是空的,新生代Minor GC就是在两个Survivor区之间相互复制存活对象,直到Survivor区满为止。

  2. 老年代:当Survivor区也满了之后就通过Minor GC将对象复制到老年代。老年代也满了的话,就将触发Full GC,针对整个堆(包括新生代、老年代、持久代)进行垃圾回收。

  3. 持久代:持久代如果满了,将触发Full GC。

元空间

元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

内存申请过程

  1. JVM会试图为相关Java对象在Eden中初始化一块内存区域;
  2. 当Eden空间足够时,内存申请结束。否则到下一步;
  3. JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
  4. Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
  5. 当old区空间不够时,JVM会在old区进行major collection;
  6. 完全垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”Out of memory错误”。

虚拟机类加载机制

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性

类加载的过程

xxx

  1. 加载:类加载器执行,根据类名查找.class文件,查找字节码,并从这些字节码中创建一个Class对象

  2. 链接:验证类中的字节码,确保没有被破坏且不包含不良Java代码。为静态域分配存储空间

  3. 初始化:如果该类具有超类,则对其初始化。

类加载器

类与类加载器

JDK中提供了三个ClassLoader,根据层级从高到低为:

  1. Bootstrap ClassLoader,主要加载JVM自身工作需要的类。
  2. Extension ClassLoader,主要加载%JAVA_HOME%\lib\ext目录下的库类。
  3. Application ClassLoader,主要加载Classpath指定的库类,一般情况下这是程序中的默认类加载器,也是ClassLoader.getSystemClassLoader() 的返回值。(这里的Classpath默认指的是环境变量中配置的Classpath,但是可以在执行Java命令的时候使用-cp 参数来修改当前程序使用的Classpath)

双亲委派模型

​如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委托给自己的父加载器,每一层的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的Bootstrap ClassLoader中,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己加载。

双亲委托模型的重要用途是为了解决类载入过程中的安全性问题。

假设有一个开发者自己编写了一个名为Java.lang.Object的类,想借此欺骗JVM。现在他要使用自定义ClassLoader来加载自己编写的java.lang.Object类。然而幸运的是,双亲委托模型不会让他成功。因为JVM会优先在Bootstrap ClassLoader的路径下找到java.lang.Object类,并载入它