JVM详细解释 整理自深入理解JAVA虚拟机
JVM结构
详解介绍
- 程序计数器
- 字节码解释器通过控制程序计数器的值来选取下一条要执行的指令例如分支 循环 跳转 异常处理 线程终端 等基础功能都需要依赖这个计数器来完成 为了线程切换后能恢复到正确位置 每条线程需要有一个独立的程序计数器 我们称这个内存为县城私有内存 这个内存区域 是jvm 规范中唯一没有规定oom 情况的区域
- Java 虚拟机栈
- 包括 栈帧 局部变量表 操作数栈 方法的出口 动态链接 等信息 long double 会占用 连个局部变量表的空间 其余的占用一个 线程请求的栈深度 大于虚拟机允许的深度 会报出 stakoverflaw 虚拟机栈可以动态扩展 扩展时无法申请到足够的内存会报出 oom
- 本地方法栈
- 本地方法栈 为虚拟机使用到的Native方法服务
- Java 堆 线程共享区域
-
字符串常量池
-
此内存区域唯一的目的就是存放对象实例
-
包括伊甸园区 新生区 老年区
-
它是可扩展的 通过 -Xmx -Xms 如果堆中没有完成实例分配 并且堆 无法发在扩展时 会报出 oom
4.元空间(方法区)
调节参数
* -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
* -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
* -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
* -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
-
使用直接内存
-
用于存储虚拟机加载的类信息 常量 静态变量 即时编译器编译后的代码 等信息
-
运行时常量池
5.直接内存
- 元数据就在直接内存中
- NIO 基于 通道 和缓冲区的 IO 它可以直接使用Native 函数库 直接分配堆外内存
对象的创建
-
内存的分配方式 指针碰撞 空闲列表
-
在使用Serial(串行收集器) ParNew(并行收集器) 等 带 Compact 过程收集器时 采用的的分配算法是 指针碰撞
-
使用 CMS(Concurrent Mark Sweep) 最短回收停顿 这种基于 Mark-Sweep 算法收集器是通常采用的是空间列表
-
分配内存的时候 也涉及到 线程安全 解决这两个问题有两种方式
1 cas 配上失败重试 保证 更新操作的原子性
2 本地线程 分配缓冲 那个线程要分配内存 就在 那个线程的 本地线程缓冲区(Thread Local Allocation Buffer , TLAB ) 是否使用TLAB 通过 -XX:+/-UseTLAB
对象的内存
1. 对象的访问定位
- 对象的主流的访问方式有句柄和直接指针两种方式
2. java 堆溢出
-
一般将 -Xms -Xmx 参数设置为一样大 避免自动扩展
-
通过参数-XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析
-
可以通过工具对快照进行分析
3. 虚拟机栈和本地方法栈溢出
- -Xoss参数设置本地方法栈大小 实际是无效的 栈容量只由 -Xss参数设定
- 多线程编程时 32 位 的 window 时2GB 2GB减去 Xmx(最大堆容量 ) 再减去MaxPermSize(最大方法去容量) 程序计数器消耗的内存很小可以忽略不记 剩下的 由 虚拟机栈和本地方法栈瓜分 每个线程分配的栈越大 可以创建的线程就越小 因为 他们都是私有内存 所以当发生StackOverFlow 时 可以通过减少堆内存和栈容量 来换取更多线程
4. 元空间和运行时常量溢出
-
可以通过 XX:MetaspaceSize XX:MaxMetaspaceSize限制方法区大小 从而间接限制常量池
-
元空间用于存放Class的相关信息 如类名 ,访问修饰符 ,常量池,字段描述, 方法描述等
5. 本机内存直接溢出
DirectMemory容量可以通过-XX:MaxDirectMemorySize指定 不指定 默认与最大堆值(-Xmx)一样
垃圾收集器和内存分配策略
引用计数算法
- 每当有一个对象引用 计数器就+1 引用失效 计数器-1 计数器为零 就是不可能被使用的对象
可达性分析算法
-
java 语言中作为GCroot对象 包括下面几种
- 虚拟机栈(栈帧中本地变量表) 中的引用对象
- 方法区中类静态属性引用对象
- 方法区中常量的引用对象
- 本地方法栈中JNI(即一般说的Native方法)引用对象
-
在对象被回收之前 至少要被标记两次 执行两次finalize()
再谈引用
- 强引用 类似 Object obj =new Object 只要强引用还在 垃圾收集器永远不会回收 被引用的对象
- 软引用 有用 但非必需的对象 在系统要发生内存溢出之前 会把这些对象列进二次回收的范围 回收之后内存还不够 才会报出内存溢出异常 例如 SoftReference sr = new SoftReference(new String(“hello”));
- 弱引用 也是描述非必需对象的 但 它的强度比软引用要弱 垃圾收集器工作时 无论内存是否足够 都会回收这类对象 例如 WeakReference sr = new WeakReference(new String(“hello”));
- 虚引用 为一个对象设置需引用的唯一目的就是在它被回收时 收到一个系统通知
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(new String(“hello”), queue);
垃圾收集算法
-
标记—清除算法(CMS 收集器)
它主要分为两个阶段 标记 整理 首先 标记 所有要清楚的对象 标记完成后 统一回收 被标记对象
缺点 : 在标记阶段 和 清除阶段 这两个 过程 效率 都不高 同时 标记清除 后 会产生 大量的不连续的空间 碎片 这可能 会导致 再后来 想要存储 一个大对象 时 找不到 足够的 连续 内存 而不得不 提前出发一次垃圾收集动作 -
复制算法
复制算法 将内存 按容量 划为容量大小相等的两块 每次只使用其中一块 这块满了 就将这块内存上存活的对象复制到另外一块上面 但是由于这种机制 在 对象 存活率较高的区域上 进行复制 效率会非常低 同时 会浪费 我们的 内存空间 .
因为 伊甸园区 对象 的 存活率 很低 所以 复制算法 主要应用 在 这里 这个包括 两块 Survivor 空间 每次 伊甸园区 只使用其中一块 Hotspot 虚拟机 默认 Eden 和 Sruvivor 的 比例是 8:1 这意味 Eden 区 最多能够使用的容量 之能占 分配 内存的 90% 当 如果存活对象 超过了 10% Suivivor 对象 存储不小 这些 对象 会 通过 分配担保机制 直接 进入 老年代 -
标记----整理算法(Parallel Old 收集器 )
标记阶段 与 标记清除 算法一样 但后续 的步骤 不是进行删除 而是让 所有 存活的对象 都向一端 移动 然后直接清理掉边界以外 的内存 -
分代收集算法
新生代 复制算法 因为 每次gc 都会有 大量的对象死去 只有少量存活
老年代 标记-整理 或 标记-清除 因为老年代 因为 对象的存活率 较高 没有 额外的空间进行担保
垃圾收集器
- Serial 历史 最悠久的单线程 收集器 会触发 stop the word 默认的 新生代收集器
- ParNew 收集器 多线程 回收策略 和 Serial 一样
- Parallel Scavenge 收集器 关注于 可控制 吞吐量
- Serial old 收集器 老年代 收集器 单线程 使用标记 整理算法
- CMS收集器 (标记-清除 收集器 )
- 初始标记 2 并发标记 3 重新标记 4 并发标记
*
类文件结构
- class文件结构
魔数 : class 文件的头4个字节 他的唯一作用确定这个文件是否为一个能虚拟机接受的class文件
第五个字节 和第六个字节 是此版本号
第七个字节和第八个字节是 主版本号
虚拟机类加载机制
- 类加载时机
-
有且只有五种情况必须对类进行初始化
- 遇到new , getstatic ,putstatic , 或 invokestatic 这四个字节码指令时 ,如果类没有进行初始化 , 则需要先触发其初始化 , 生成这4个指令最常见的场景 :使用new 关键字实例化对象的时候 , 读取或 设置 一个类的 静态字段(被final 修饰 , 已在编译期 把结果放入常量池的静态字段除外)的时候 ,以及调用一个类静态方法的时候 .
- 使用 java.lang.reflect 包下 的方法对类进行反射调用的时候 , 如果类没有进行初始化 则先需要触发初始化.
- 当初始化一个类的时候 ,如果发现其父类还没有进行初始化 , 则需要先触发父类初始化
- 当虚拟机启动的时,用户指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
- 当使用jdk1.7的动态语言支持时 如果一个 java.lang.invoke.MethodHandle 实例的最后的解析结果 REF_getstatic REF_putstatic REF_invokeStatic 的方法句柄并且这个方法句柄所对应的类没有进行过初始化 则先需要触发初始化
类加载过程
- 加载
1.通过一个类的全限定名来获取定义此类的二级制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问的入口 - 验证
-
文件格式验证
1.是否以魔数开头
2.主次版本号是否在当前虚拟机处理范围内
3.常量池的常量中是否有不被支持的类型 -
元数据验证
1.这个类是否有父类
2.这个类是否继承了不被允许的类(被final修饰的类)
3.如果这个类不是抽象类是否实现了其父类或接口中的要求实现的所有方法
4.类中的字段方法是否与父类产生矛盾(例如覆盖了父类的final字段或者不符合规则的方法重载) -
字节码验证
1 保证任意时刻操作数栈的数据类型与指令代码都能配合工作 例如 在操作数栈中 放了一个int 类型 取出来按long类型 加载进本地 -
符号引用验证
-
准备
- 为类变量(静态变量)在方法区分配内存,并设置零值。注意:这里是类变量,不是实例变量,实例变量是对象分配到堆内存时根据运行时动态生成的。
解析- 把常量池中的符号引用解析为直接引用:根据符号引用所作的描述,在内存中找到符合描述的目标并把目标指针指针返回。
初始化 - 真正开始执行Java程序代码,该步执行方法根据代码赋值语句,对 类变量和其他资源 进行初始化赋值。
- clinit方法:编译器自动收集类中所有 类变量的赋值语句和静态语句合并而成,收集的顺序是在程序代码出现的顺序。所以,静态语句中只能访问到定义在静态语句块之前的变量,在其之后的变量可以赋值(相当于新建并赋值了)但不可以访问(因为还没出现)。
- 注:由此步我们就可以得知,我们在分析向上转型的例子时的程序代码的运行顺序了:父类静态内容——子类静态内容——父类构造——子类构造——子类方法 。
- 把常量池中的符号引用解析为直接引用:根据符号引用所作的描述,在内存中找到符合描述的目标并把目标指针指针返回。