Files
kaka111222333-kaka111222333…/_posts/2014-08-20-jvm.md
2019-11-17 01:12:14 +08:00

541 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
layout: post
title: Java虚拟机
tags: JVM Java
categories: Java
---
* TOC
{:toc}
# 一、走近JAVA
## 第一章、走近JAVA
java技术体系
![java][java]
### 1.1 jdk1.7的主要特性
G1收集器
JSR-292对非JAVA语言的调用支持
ARM指令集`?`
Sparc指令集`?`
**新语法**:原生二进制(0b开头)switch支持字符串"<>"操作符异常处理改进简化变长参数方法调用面向资源的try-catch-finally
**多核并行**java.util.concurrent.forkjoin
openJdk子项目Sumatra为java提供使用GPU、APU运算能力的工具
java并行框架hadoop MapReduce
ScalaErlangClojure天生具备并行计算能力
### 1.2 虚拟机历史
classic/exact vm → hotspot vm(sun jdk 与 open jdk 共同的虚拟机) → oracle 收购 bea与sun 拥有JRockit VM 与 Hotspot VM
jit编译器 → OSR 栈上替换(on-stack replace)
jsr : Java Specification Requests java规范请求
### 1.3 Open Service Gateway Initiative(OSGI)
一种java模块化标准《深入理解OSGI:Equinox原理,应用与最佳实践》
混合语言运行于java平台上的其他语言Clojure,JRuby/Rails,Groovy
# 二、自动内存管理机制
## 第二章、java内存区域与内存溢出异常
### 2.1 运行时数据区
![runtime][runtime]
#### 2.1.1 程序计数器
程序计数器是一块比较小的内存空间,可以看成是当前线程所执行的字节码的行号指示器,字节码解释器通过改变计数器的值来选取指令。
java多线程通过轮流切换实现每个线程都需要有一个独立的计数器这类内存称为"线程私有"的内存。这是java虚拟机唯一没有规定任何OutOfMemoryError情况的区域。`线程私有`
#### 2.1.2 java虚拟机栈
每个方法对应一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等,每个方法的执行对应着一个栈帧在虚拟机中的入栈与出栈。`线程私有`
局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference类型不等同于对象本身)、returnAddress类型
long,double占两个局部变量空间其余占一个局部变量表所需空间在编译时确定运行期不会改变。
#### 2.1.3本地方法栈
与java虚拟机栈作用类似区别是虚拟机栈执行java方法本地方法栈执行native方法。`线程私有`
>**以上三个线程私有的内存区域,随线程而生,随线程而灭,无需考虑垃圾回收**
#### 2.1.4 java堆
最大的空间,`所有线程共享`,用于存放对象实例(数组也是对象)GC管理的主要区域GC基本采用分代收集算法(新生代,老年代,永久代(方法区))。
java堆只需要逻辑连续,不要求物理连续
#### 2.1.5 方法区
`所有线程共享`存储类信息、常量、静态变量、字段描述方法描述即时编译器编译后的代码等。Hotspot中称为永久代。
#### 2.1.6 运行时常量池
`方法区`的一部分class文件中包含类的版本字段方法接口常量池等。运行时常量池具备动态性class常量池反之。
#### 2.1.7 直接内存
使用nativie函数分配堆外内存然后在堆中通过DirectoryByteBuffer对象作为这块内存的引用。
不会受到java堆大小的限制会受到本机总内存以及处理器寻址空间的限制。
### 2.2 HotSpot虚拟机对象探秘
#### 2.2.1 对象的创建
(1)检查能否在常量池定位到一个类的符号引用,
(2)检查这个类是否已被加载、解析和初始化过,如果没有,执行相应加载过程
(3)从java堆中分配确定大小的内存有两种分配方式指针碰撞与空闲列表取决于垃圾收集器是否带有压缩整理功能
(4)分配空间的线程安全同步或者本地线程分配缓冲是否使用TLAB-XX:+/-UseTLAB
(5)分配完成后,空间初始化为零值,设置对象头信息
(6)执行<init>(构造方法),字段赋值
#### 2.2.2 对象的内存布局:对象头,实例数据,对齐填充
(1)**对象头**包括两部分信息:
第一部分:存储对象自身的运行时数据,如 hashcodeGC分代年龄锁状态标识线程持有的锁偏向线程ID偏向时间戳等
第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机以此确定对象是哪个类的实例,数组长度(if)
(2)**实例数据**:类中字段的内容,默认分配策略总是将相同宽度的字段分配到一起,所以子类较窄的变量可能插入父类变量的空隙中
(3)**对齐填充**不是必然存在的仅仅起到占位符的作用因为对象大小以及对象头的大小必须是8字节的整数倍
#### 2.2.3 对象的访问定位
**句柄** (reference指向句柄池,句柄池指向实例数据和类型数据),稳定
![handle][handle]
**直接指针** (reference指向实例数据,实例数据中存放类型数据指针),速度快,少一次指针定位
![point][point]
HotSpot使用直接指针
确式内存管理虚拟机可以知道内存中某个位置的具体数据是什么类型。这样做可以摒弃句柄池而使用直接指针。HotSpot使用OopMap数据结构实现。
### 2.3 OutOfMemoryError异常
程序计数器不会发生,其他内存区域都有可能发生
>-Xms10M -Xmx10M 设置java堆的大小
>
>-Xmn2g 设置新生代大小java堆总大小=新生代+老年代
>
>-Xss 设置栈容量
>
>-verbose:gc 显示gc信息
>
>-XX:+HeapDumpOnOutOfMemoryError 出现内存溢出异常时Dump出当前的内存堆转储快照
>
>-XX:PermSize -XX:MaxPermSize 限制方法区大小
>
>-XX:MaxDirectMemorySize 指定直接内存容量默认为java堆最大值
#### 2.3.1 java堆溢出
java.lang.OutOfMemoryError: Java heap space 堆过小,或大量应该被清理的对象依旧被保持(内存泄露)
#### 2.3.2 虚拟机栈和本地方法栈溢出
如果栈深度大于允许最大深度—StackOverFlowError 定义大量局部变量(如递归),栈最少104k(jdk1.7.0_51)
如果扩展栈时无法申请到足够的空间—OutOfMemoryError:unable to create new native thread
因为每个线程都有独立的栈,所以在多线程环境下,每个线程的栈越大越容易发生内存溢出。
若不允许更换64位虚拟机(32位Win每个进程最多2GB内存),或者减少线程数量,那么减小最大堆或者减小最大栈反而能换取更多线程。
#### 2.3.3 方法区和运行时常量池溢出
java.lang.OutOfMemoryError: PermGen space 方法区过小、过多的动态代理或使用大量第三方jar文件造成class文件过多方法过多。
String.intern()是一个native方法如果字符串常量池中已经包含一个equal此String对象的字符串则返回常量池中的这个对象否则将此String对象包含的字符串添加到常量池中并返回此常量池中对象的引用。(jdk1.6中,intern会将首次出现的String复制一个新的实例至常量池;而jdk1.7中会在常量池中记录首次出现的实例引用,不会复制)
groovy等jvm平台的动态语言会持续创建类来实现语言的动态性极易发生该类型的溢出包含大量jsp的应用、基于OSGI的应用也容易发生该溢出。
#### 2.3.4 本机直接内存溢出
HeapDump中看不到明显的异常NIO可能引发这种异常
## 第三章、垃圾收集器与内存分配策略
### 3.1 对象已死吗?
#### 3.1.1 引用计数算法
给对象添加一个引用计数器,每产生一个对该对象的引用,计数器+1;引用失效时,计数器-1,计数器为0时,表示对象不可用.
但是主流的java虚拟机没有使用引用计数算法主要因为它很难解决对象之间相互循环引用的问题。
#### 3.1.2 可达性分析算法
通过一系列“GC Roots”作为起始点向下搜索搜索走过的路径称为引用链当一个对象到GC Roots没有任何引用链相连时证明此对象不可用.
java,C# ,List都是通过可达性分析来判断对象是否存活的.
在java语言中可作为GC Roots的对象包括以下几种
虚拟机栈(栈帧中的本地变量表)中引用的对象,
方法区中类静态属性引用的对象,
方法区中常量引用的对象,
本地方法栈中JNI(即native方法)引用的对象
#### 3.1.3 引用概念扩充,强度依次减弱:
* **强引用**(Strong Refrence)
Object obj = new Object(); 只要强引用还存在GC永远不会回收.
* **软引用**(Soft Reference)
用来描述一些有用但不必需的对象在系统将要发生内存溢出之前GC会对软引用对象进行回收若回收后仍没有足够内存才会抛出异常。
JDK1.2之后提供了SoftReference 类来实现软引用
* **弱引用**(Weak Reference)
也是用来描述非必需对象的但是强度比软引用更弱弱引用对象只能生存到下一次GC发生之前当GC工作时无论内存是否足够都会回收弱引用对象
JDK1.2之后提供了WeakReference 类来实现弱引用
* **虚引用**(Phantom Reference)
也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个实例。
为对象设置一个虚引用的唯一目的就是能在这个对象被GC回收时收到一个系统通知。
JDK1.2之后提供了PhantomReference 类来实现虚引用
#### 3.1.4 生存还是死亡
可达性分析算法中不可达的对象,并非是“非死不可”的,对象死亡需要一个过程:
(1)若不可达筛选对象若对象没有覆盖finalize()方法或者finalize()已经执行过,将判定为没有必要执行(将跳过第2步);若判定为有必要执行这个对象将放之在F-Queue队列中并在稍后由一个虚拟机自动建立、低优先级的Finalizeer线程去执行。
(2)执行finalize()方法只是触发不会等待方法执行结束目的是防止finalize执行时间过长或者发生死循环引起F-Queue中对象永久等待。
finalize方法是对象逃脱死亡的最后一次机会若在finalize()方法中该对象被重新引用,该对象将被移出“即将回收集合”。
任何一个对象的finalize()方法只会被系统自动调用一次。该方法不确定性太高,建议不使用。
(3)回收对象
#### 3.1.5 回收方法区
(1)废弃常量和无用的类可以被回收
**废弃常量**
系统中不存在对这个常量的引用
**无用的类**
①该类的所有实例都被回收也就是java堆中不存在该类的任何实例
②加载该类的ClassLoader已经被回收
③该类对应的java.lang.Class对象没有在任何地方被引用无法在任何地方通过反射访问该类的方法。
(2)回收设置
-Xnoclassgc 不对类进行回收
-verbase:class -XX:+TraceClassLoading -XX:+TraceClassUnLoading 查看类的加载和卸载信息
其中-verbase:class -XX:+TraceClassLoading可以再Product版虚拟机使用-XX:+TraceClassUnLoading需要FastDebug版支持
频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能以保证永久代不会溢出。
### 3.2 垃圾收集算法
#### 3.2.1 标记-清除算法
先标记处所有需要回收的对象,标记完成后统一回收,这是最基础的收集算法,后续的收集算法都是基于这个思路改进的。
不足:①标记和清除两个过程效率都不高 ②会产生大量不连续的内存碎片,可能导致在创建大对象时,因没有足够的连续内存而触发另一次垃圾收集动作。
#### 3.2.2 复制算法
将内存分为一块较大的Eden空间和两块较小的Survivor空间每次使用Eden和其中一块survivor当回收时将Eden和survivor中还存活的对象一次性复制到另一块survivor中最后清理掉Eden和刚才用过的survivor空间。
HotSpot默认Eden和survivor的大小比例为8:1(-XX:ServivorRatio=8设置比例)也就是每次新生代中可用内存为整个新生代容量的90%只有10%会被浪费。
当survivor不够用时需要依赖其他内存(老年代)进行分配担保。当survivor不够用时这些对象将直接通过分配担保机制进入老年代。
>为什么需要两块Survivor
为了确保每次复制后都必定有一块Survivor是空闲的。如果只有1个Survivor将Eden复制到Survivor中然后清理Eden之后还要将Survivor复制回Eden。
#### 3.2.3 标记-整理算法
标记过程与标记-清除算法相同,然后让所有对象向一端移动,最后清理掉端边界以外的内存。老年代适合这种算法。
#### 3.2.4 分代收集算法
根据对象存活周期不同将内存分为几块一般是把java堆分为新生代和老年代这样可以根据年代特点选用合适的收集算法。
**新生代**:对象存活率低,选用复制算法
**老年代**:对象存活率高,没有额外空间进行分配担保,选用标记-清除或者标记-整理算法
### 3.3 HotSpot算法实现
#### 3.3.1 枚举根节点:
GC Roots 主要在全局性引用(常量或类静态属性)与执行上下文(栈帧中的本地变量表)中若GC Roots过多(例如方法区数据过多),将消耗大量时间。
可达性分析时,整个系统的引用关系必须是不变的(一致性快照)因此必须停顿所有JAVA执行线程。
因为使用准确式GC并不需要检查所有执行上下文和全局引用虚拟机有办法知道哪些地方存放着对象引用。
缺陷--对执行时间敏感
#### 3.3.2 安全点(Safe Point)
只在安全点生成OopMap并"stop the world",因为要节省空间。
只在能让程序长时间执行的指令流(如方法调用、循环跳转、异常跳转)中选定安全点因为安全点过少则GC等待时间过长安全点过多则会增大运行负荷。
**抢先式中断**gc发生时先中断全部线程若有线程不在安全点上则恢复线程让其跑到安全点上。(几乎没有虚拟机使用)
**主动式中断**gc发生时不直接操作线程在安全点和创建对象分配内存的位置设立一个标志各个线程轮询这个标志当中断标志位真时自动中断挂起。
缺陷--在gc之前就已中断的线程无法进入安全点挂起需通过安全区域来解决。
>线程中断有先后顺序,如何保证在错开的时间内引用关系不会发生改变?
>
>JAVA多线程是通过轮流切换实现的一个线程执行时其他线程都是阻塞的不存在错开运行的情况而且安全点设在长指令流中不存在引用关系变化。
#### 3.3.3 安全区域(Safe Region)
在一段代码片段中引用关系不会发生变化在这个区域中的任意地方开始GC都是安全的这个区域称为安全区域可以看做扩展了的安全点。
当线程执行到 Safe Region 时首先标记自己进入SafeRegion当这段时间内要发起GC时可以不用关心SafeRegion状态的线程。
当线程要离开 Safe Region 时,它要检查系统是否完成了根节点枚举(或者是整个GC过程),如果完成了则线程继续运行,否则必须等待。
### 3.4 垃圾收集器
![gc][gc]
有连线表示可以搭配使用
#### 3.4.1 Serial收集器
复制算法
最基本、最古老、单线程工作时会停顿其他所有工作线程因为简单且高效所以作为Client模式下的默认新生代收集器。
#### 3.4.2 ParNew收集器
复制算法
本质上是Serial收集器的并行(多个垃圾处理线程并行工作,但是用户线程仍然等待)多线程版本指定CMS后的默认新生代收集器。
#### 3.4.3 Parallel Scavenge收集器
复制算法
并行多线程
关注于使CUP达到可控的吞吐量(运行用户代码的时间与JVM总运行时间的比值)高效利用CUP适合于在后台运算不需要交互的任务。俗称“吞吐量优先收集器”
`-XX:MaxGCPauseMillis` 控制最大垃圾收集停顿时间大于0的毫秒数这个值越小新生代越小吞吐量越小
`-XX:GCTimeRatio` 设置吞吐量大小大于0小于100的整数n最大允许垃圾收集时间比例为 1/(1+n)n默认为99即最大立即收集时间为1%
`-XX:UseAdaptiveSizePolicy` 开关参数,使用GC自适应调节策略
#### 3.4.4 Serial Old收集器
标记-整理算法
Serial收集器老年代版本单线程。
#### 3.4.5 Parallel Old收集器
标记-整理算法
Parallel Scavenge收集器的老年代版本多线程
在出现该收集器之前Parallel Scavenge只能与Serial Old搭配使用但是Serial Old会拖累Parallel Scavenge导致这种组合没有CMS+ParNew给力
在注重**吞吐量**以及**CPU资源**的场合应该优先考虑Parallel Scavenge + Parallel Old组合。
#### 3.4.6 CMS收集器
hotspot中的第一款并发收集器可以与用户线程同时工作
标记-清除算法分4个步骤
①初始标记 标记GC能关联的对象
②并发标记
③重新标记 修正并发标记期间因程序继续运作而导致标记变动的一部分对象的标记记录
④并发清除
①③仍需停顿,但耗时短②④耗时长,但可以与用户线程一起工作
以最短回收停顿时间为目标的收集器。也称“并发低停顿收集器”
>3个缺点
>
>* 对CPU资源敏感面向并发的程序都对CPU资源敏感在并发阶段(②④)会占用一部分线程(默认(cpu数量+3)/4)导致应用程序变慢,吞吐量变小。
>
>* 无法处理浮动垃圾在④阶段用户线程还活着那么在标记之后可能产生新的垃圾这部分垃圾只能留给下一次GC回收称为“浮动垃圾”。
因为CMS收集同时用户线程也需要空间来运行所以要预留足够空间给用户线程使用所以老年代使用68%(1.6中为92%)空间后就会激活CMS。
-XX:CMSInitiatingOccupancyFraction 设置激活CMS的内存百分比
设高了可以减少GC次数提高性能但是更容易出现因预留空间无法满足用户程序的情况而临时启用Serial Old反而降低性能的情况。
>* 基于标记-清除算法,会产生大量空间碎片。
>
-XX:UseCMSCompactAtFullCollection 当CMS触发Full GC时对内存碎片进行合并整理无法并发停顿会变长。
>
>-XX:CMSFullGCsBeforeCompaction 设置执行多少次不整理的Full GC后执行一次整理的Full GC默认值为0表示每次Full GC都会整理。
#### 3.4.7 G1收集器
标记-整理算法
面向服务端应用,特点:
(1)并行与并发利用多个CPU减少停顿时间总体上可以与用户线程并发执行。
(2)分代收集,不需要与其他收集器配合,可以采用不同的方式处理垃圾。
(3)空间整合,虽然整体上采用标记-整理算法,但是局部(两个Region之间)上采用复制算法,不会产生内存空间碎片。
(4)可预测的停顿能够让使用者明确指定一个长度为M毫秒的时间片段内消耗在垃圾收集的上的时间不得超过N毫秒。
使用G1时java堆被分为多个大小相等的独立区域(Region)新生代和老年代不再是物理隔离而是一部分Region的集合。
能够预测停顿时间的原因有计划地避免在整个java堆中进行全区域的垃圾收集G1跟踪各个Region里垃圾的价值大小在后台维护一个优先列表
每次根据允许的收集时间优先收集价值最大的Region。
每个Region对应一个Rememberd Set当程序对Reference类型进行写操作时将会检测Reference引用的对象是否处于不同的Region中
如果是就会将相关引用信息写入对象所属Region的Remeberd Set中当内存回收时GC根节点的枚举范围加入Remeberd Set就不必对全堆扫描。
G1运作步骤初始标记, 并发标记, 最终标记(修正变动,需停顿,可并行), 筛选回收。
#### 3.4.8 GC日志
`-XX:+PrintGCDetails` 开启垃圾回收日志
>33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K-152K(11904K),0.0031680 secs]
`33.125`:虚拟机启动以来经过的秒数
"`[GC`"或者"`[Full GC`"停顿类型后者表示发生了Stop The World如果是调用System.gc()方法触发的收集,将会显示为:"[Full GC (System)"
`DefNew`GC发生的区域该名称与垃圾收集器相关DefNew为Serial收集器新生代区域
`3324K->152K(3712K)`GC前该**区域**已使用容量->GC后该区域已使用容量(该区域总容量)
`0.0025925 secs`该区域GC占用的时间
`3324K->152K(11904K)`GC前java**堆**已使用的容量->GC后java堆已使用的容量(java堆总容量)
#### 3.4.9 垃圾收集器参数
![arg0][arg0]
![arg1][arg1]
### 3.5 内存分配与回收策略
#### 3.5.1 对象优先在Eden分配
Minor GC新生代GC对象大多朝生夕灭GC非常频繁回收速度较快。
Full GC/Major GC老年代GC经常会伴随至少一次Minor GC速度一般比Minor GC慢10倍以上。
#### 3.5.2 大对象直接进入老年代
典型:很长的字符串以及数组
`-XX:PretenureSizeThreshold=3145728`(不能写成3MB只对Serial和ParNew有效)令大于这个设置值的对象直接在老年代分配目的是避免在Eden以及两个Servivor区发生大量的内存复制。
创建大对象时容易触发GC即时此时还有大量的空间 应尽量避免创建短命的大对象。
#### 3.5.3 长期存活的对象进入老年代
jvm给每个对象定义了一个年龄计数器。
如果对象在Eden出生并经过第一次Minor GC后仍然存活并且能被Survivor容纳的话将被移动到Survivor中并将年龄设置为1。
对象在Survivor中每"熬过"一次Minor GC年龄就+1当年龄增加到15(默认)时将在下一次GC被晋升到老年代中。
`-XX:MaxTenuringThreshold=15` 设置对象晋升老年代的阈值。
`-XX:+PrintTenuringDistribution` 显示更详细的GC信息
#### 3.5.4 动态对象年龄判定
如果在Survivor中相同年龄对象大小的总和大于Survivor大小的一半年龄大于或等于该大小的对象就可以直接进入老年代无需增长MaxTenuringThreshold。
#### 3.5.5 空间担保分配
发生Minor GC前JVM会检查老年代最大可用连续内存是否大于新生代所有对象总大小
如果这个条件成立Minor GC可以确保是安全的。
如果不成立,虚拟机会查看`HandlePromotionFailure`设置是否允许担保失败。
如果不允许将进行一次Full GC。
如果允许,那么会继续检查老年代最大可以连续空间是否大于历次晋升到老年代对象的平均大小.
如果小于将进行一次Full GC。
如果大于将尝试进行一次Minor GC尽管这次GC是有风险的。
如果尝试失败将进行一次Full GC。
所谓风险Minor GC时Survivor中无法容纳的对象将担保分配至老年代如果本次GC时新生代对象超过平均大小因为具体超过多少是未知的老年代将可能无法容纳这些对象。
[java]: {{"/java-system.png" | prepend: site.imgrepo }}
[runtime]: {{"/jvm-runtime.png" | prepend: site.imgrepo }}
[handle]: {{"/jvm-handle.png" | prepend: site.imgrepo }}
[point]: {{"/jvm-point.png" | prepend: site.imgrepo }}
[gc]: {{"/jvm-gc.png" | prepend: site.imgrepo }}
[arg0]: {{"/jvm-arg0.png" | prepend: site.imgrepo }}
[arg1]: {{"/jvm-arg1.png" | prepend: site.imgrepo }}