--- 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 Scala,Erlang,Clojure天生具备并行计算能力 ### 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)执行(构造方法),字段赋值 #### 2.2.2 对象的内存布局:对象头,实例数据,对齐填充 (1)**对象头**包括两部分信息: 第一部分:存储对象自身的运行时数据,如 hashcode,GC分代年龄,锁状态标识,线程持有的锁,偏向线程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 }}