jvm 订阅
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 展开全文
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
信息
软件语言
Java
简    称
JVM
应用学科
计算机软件
中文名
Java虚拟机
外文名
Java Virtual Machine
JVM概述
Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。Java虚拟机不仅是一种跨平台的软件,而且是一种新的网络计算平台。该平台包括许多相关的技术,如符合开放接口标准的各种API、优化技术等。Java技术使同一种应用可以运行在不同的平台上。Java平台可分为两部分,即Java虚拟机(Java virtual machine,JVM)和Java API类库。 [1] 
收起全文
精华内容
参与话题
问答
  • Java虚拟机(JVM)你只要看这一篇就够了!

    万次阅读 多人点赞 2018-08-14 12:55:02
    1. Java 内存区域与内存溢出异常 1.1 运行时数据区域 根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。 ...字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行...

    本文是学习了《深入理解Java虚拟机》之后的总结,主要内容都来自于书中,也有作者的一些理解。一是为了梳理知识点,归纳总结,二是为了分享交流,如有错误之处还望指出。

    用XMind画了一张导图(源文件对部分节点有详细备注和参考资料,需要的朋友可以关注我的微信公众号:Java团长,然后回复“JVM”获取):

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

    1.1 运行时数据区域

    根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。

     

    1.1.1 程序计数器

    内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

    如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

    1.1.2 Java 虚拟机栈

    线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

    局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

    StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
    OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

    1.1.3 本地方法栈

    区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

    1.1.4 Java 堆

    对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

    OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

    1.1.5 方法区

    属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    现在用一张图来介绍每个区域存储的内容。

     

    1.1.6 运行时常量池

    属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

    1.1.7 直接内存

    非虚拟机运行时数据区的部分

    在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
    OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

    1.2 HotSpot 虚拟机对象探秘

    主要介绍数据是如何创建、如何布局以及如何访问的。

    1.2.1 对象的创建

    创建过程比较复杂,建议看书了解,这里提供个人的总结。

    遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。

    类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。

    前面讲的每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。

    内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。

    执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。

    1.2.2 对象的内存布局

    在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)

    对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

    实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

    对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

    1.2.3 对象的访问定位

    使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。

    通过句柄访问

    Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。

     

    使用直接指针访问

    reference 中直接存储对象地址

     

    比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。

    1.3 实战

    // 待填

    2. 垃圾回收器与内存分配策略

    2.1 概述

    程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

    2.2 对象已死吗?

    在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

    2.2.1 引用计数法

    给对象添加一个引用计数器。但是难以解决循环引用问题。

     

    从图中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。

    2.2.2 可达性分析法

    通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

     

    可作为 GC Roots 的对象:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

    2.2.3 再谈引用

    前面的两种方式判断存活时都与‘引用’有关。但是 JDK 1.2 之后,引用概念进行了扩充,下面具体介绍。

    下面四种引用强度一次逐渐减弱

    强引用

    类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。

    软引用

    SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。

    弱引用

    WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

    虚引用

    PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

    2.2.4 生存还是死亡

    即使在可达性分析算法中不可达的对象,也并非是“facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

    如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。

    finalize() 方法只会被系统自动调用一次。

    2.2.5 回收方法区

    在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。

    永久代垃圾回收主要两部分内容:废弃的常量和无用的类。

    判断废弃常量:一般是判断没有该常量的引用。

    判断无用的类:要以下三个条件都满足

    • 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
    • 加载该类的 ClassLoader 已经被回收
    • 该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法

    2.3 垃圾回收算法

    仅提供思路

    2.3.1 标记 —— 清除算法

    直接标记清除就可。

    两个不足:

    • 效率不高
    • 空间会产生大量碎片

    2.3.2 复制算法

    把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。

    解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。

    2.3.3 标记-整理算法

    不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。

    2.3.4 分代回收

    根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。

    新生代

    每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。

    老年代

    老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。

    2.4 HotSpot 的算法实现

    // 待填

    2.5 垃圾回收器

    收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。

     

    说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。

    2.5.1 Serial 收集器

    这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。

     

    2.5.2 ParNew 收集器

    可以认为是 Serial 收集器的多线程版本。

     

    并行:Parallel

    指多条垃圾收集线程并行工作,此时用户线程处于等待状态

    并发:Concurrent

    指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。

    2.5.3 Parallel Scavenge 收集器

    这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。

    CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。

    作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。

    2.5.4 Serial Old 收集器

    收集器的老年代版本,单线程,使用 标记 —— 整理

     

    2.5.5 Parallel Old 收集器

    Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理

     

    2.5.6 CMS 收集器

    CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。

    运作步骤:

    1. 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
    2. 并发标记(CMS concurrent mark):进行 GC Roots Tracing
    3. 重新标记(CMS remark):修正并发标记期间的变动部分
    4. 并发清除(CMS concurrent sweep)

     

    缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片

    2.5.7 G1 收集器

    面向服务端的垃圾回收器。

    优点:并行与并发、分代收集、空间整合、可预测停顿。

    运作步骤:

    1. 初始标记(Initial Marking)
    2. 并发标记(Concurrent Marking)
    3. 最终标记(Final Marking)
    4. 筛选回收(Live Data Counting and Evacuation)

     

    2.6 内存分配与回收策略

    2.6.1 对象优先在 Eden 分配

    对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。

    一般来说 Java 堆的内存模型如下图所示:
     

     

    新生代 GC (Minor GC)

    发生在新生代的垃圾回收动作,频繁,速度快。

    老年代 GC (Major GC / Full GC)

    发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

    2.6.2 大对象直接进入老年代

    2.6.3 长期存活的对象将进入老年代

    2.6.4 动态对象年龄判定

    2.6.5 空间分配担保

    3. Java 内存模型与线程

     

    3.1 Java 内存模型

    屏蔽掉各种硬件和操作系统的内存访问差异。

     

    3.1.1 主内存和工作内存之间的交互

    操作 作用对象 解释
    lock 主内存 把一个变量标识为一条线程独占的状态
    unlock 主内存 把一个处于锁定状态的变量释放出来,释放后才可被其他线程锁定
    read 主内存 把一个变量的值从主内存传输到线程工作内存中,以便 load 操作使用
    load 工作内存 把 read 操作从主内存中得到的变量值放入工作内存中
    use 工作内存 把工作内存中一个变量的值传递给执行引擎,
    每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作
    assign 工作内存 把一个从执行引擎接收到的值赋接收到的值赋给工作内存的变量,
    每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
    store 工作内存 把工作内存中的一个变量的值传送到主内存中,以便 write 操作
    write 工作内存 把 store 操作从工作内存中得到的变量的值放入主内存的变量中

    3.1.2 对于 volatile 型变量的特殊规则

    关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。

    一个变量被定义为 volatile 的特性:

    1. 保证此变量对所有线程的可见性。但是操作并非原子操作,并发情况下不安全。

    如果不符合 运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值变量不需要与其他的状态变量共同参与不变约束 就要通过加锁(使用 synchronize 或 java.util.concurrent 中的原子类)来保证原子性。

    1. 禁止指令重排序优化。

    通过插入内存屏障保证一致性。

    3.1.3 对于 long 和 double 型变量的特殊规则

    Java 要求对于主内存和工作内存之间的八个操作都是原子性的,但是对于 64 位的数据类型,有一条宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即允许虚拟机实现选择可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。这就是 long 和 double 的非原子性协定。

    3.1.4 原子性、可见性与有序性

    回顾下并发下应该注意操作的那些特性是什么,同时加深理解。

    • 原子性(Atomicity)

    由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。

    • 可见性(Visibility)

    是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。主要操作细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 可以保证可见性。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步会主内存中( store、write 操作)”这条规则获得。而 final 可见性是指:被 final 修饰的字段在构造器中一旦完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。

    • 有序性(Ordering)

    如果在被线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句指“线程内表现为串行的语义”,后半句是指“指令重排”现象和“工作内存与主内存同步延迟”现象。Java 语言通过 volatile 和 synchronize 两个关键字来保证线程之间操作的有序性。volatile 自身就禁止指令重排,而 synchronize 则是由“一个变量在同一时刻指允许一条线程对其进行 lock 操作”这条规则获得,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

    3.1.5 先行发生原则

    也就是 happens-before 原则。这个原则是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是 Java 内存模型中定义的两项操作之间的偏序关系。

    天然的先行发生关系

    规则 解释
    程序次序规则 在一个线程内,代码按照书写的控制流顺序执行
    管程锁定规则 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
    volatile 变量规则 volatile 变量的写操作先行发生于后面对这个变量的读操作
    线程启动规则 Thread 对象的 start() 方法先行发生于此线程的每一个动作
    线程终止规则 线程中所有的操作都先行发生于对此线程的终止检测
    (通过 Thread.join() 方法结束、 Thread.isAlive() 的返回值检测)
    线程中断规则 对线程 interrupt() 方法调用优先发生于被中断线程的代码检测到中断事件的发生
    (通过 Thread.interrupted() 方法检测)
    对象终结规则 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始
    传递性 如果操作 A 先于 操作 B 发生,操作 B 先于 操作 C 发生,那么操作 A 先于 操作 C

    3.2 Java 与线程

    3.2.1 线程的实现

    使用内核线程实现

    直接由操作系统内核支持的线程,这种线程由内核完成切换。程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口 —— 轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程,每个轻量级进程都有一个内核级线程支持。

     

    使用用户线程实现

    广义上来说,只要不是内核线程就可以认为是用户线程,因此可以认为轻量级进程也属于用户线程。狭义上说是完全建立在用户空间的线程库上的并且内核系统不可感知的。

     

    使用用户线程夹加轻量级进程混合实现

    直接看图

     

    Java 线程实现

    平台不同实现方式不同,可以认为是一条 Java 线程映射到一条轻量级进程。

    3.2.2 Java 线程调度

    协同式线程调度

    线程执行时间由线程自身控制,实现简单,切换线程自己可知,所以基本没有线程同步问题。坏处是执行时间不可控,容易阻塞。

    抢占式线程调度

    每个线程由系统来分配执行时间。

    3.2.3 状态转换

    五种状态:

    • 新建(new)

    创建后尚未启动的线程。

    • 运行(Runable)

    Runable 包括了操作系统线程状态中的 Running 和 Ready,也就是出于此状态的线程有可能正在执行,也有可能正在等待 CPU 为他分配时间。

    • 无限期等待(Waiting)

    出于这种状态的线程不会被 CPU 分配时间,它们要等其他线程显示的唤醒。

    以下方法会然线程进入无限期等待状态:
    1.没有设置 Timeout 参数的 Object.wait() 方法。
    2.没有设置 Timeout 参数的 Thread.join() 方法。
    3.LookSupport.park() 方法。

    • 限期等待(Timed Waiting)

    处于这种状态的线程也不会分配时间,不过无需等待配其他线程显示地唤醒,在一定时间后他们会由系统自动唤醒。

    以下方法会让线程进入限期等待状态:
    1.Thread.sleep() 方法。
    2.设置了 Timeout 参数的 Object.wait() 方法。
    3.设置了 Timeout 参数的 Thread.join() 方法。
    4.LockSupport.parkNanos() 方法。
    5.LockSupport.parkUntil() 方法。

    • 阻塞(Blocked)

    线程被阻塞了,“阻塞状态”和“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个时间将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。

    • 结束(Terminated)

    已终止线程的线程状态。

     

    4. 线程安全与锁优化

    // 待填

    5. 类文件结构

    // 待填

    有点懒了。。。先贴几个网址吧。

    1. Official:The class File Format
    2.亦山: 《Java虚拟机原理图解》 1.1、class文件基本组织结构

    6. 虚拟机类加载机制

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

    在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。

    6.1 类加载时机

    类的生命周期( 7 个阶段)

     

    其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。

    以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):

    1. 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
    2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
    3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
    4. 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
    5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。

    前面的五种方式是对一个类的主动引用,除此之外,所有引用类的方法都不会触发初始化,佳作被动引用。举几个例子~

    public class SuperClass {
        static {
            System.out.println("SuperClass init!");
        }
        public static int value = 1127;
    }
    
    public class SubClass extends SuperClass {
        static {
            System.out.println("SubClass init!");
        }
    }
    
    public class ConstClass {
        static {
            System.out.println("ConstClass init!");
        }
        public static final String HELLOWORLD = "hello world!"
    }
    
    public class NotInitialization {
        public static void main(String[] args) {
            System.out.println(SubClass.value);
            /**
             *  output : SuperClass init!
             * 
             * 通过子类引用父类的静态对象不会导致子类的初始化
             * 只有直接定义这个字段的类才会被初始化
             */
    
            SuperClass[] sca = new SuperClass[10];
            /**
             *  output : 
             * 
             * 通过数组定义来引用类不会触发此类的初始化
             * 虚拟机在运行时动态创建了一个数组类
             */
    
            System.out.println(ConstClass.HELLOWORLD);
            /**
             *  output : 
             * 
             * 常量在编译阶段会存入调用类的常量池当中,本质上并没有直接引用到定义类常量的类,
             * 因此不会触发定义常量的类的初始化。
             * “hello world” 在编译期常量传播优化时已经存储到 NotInitialization 常量池中了。
             */
        }
    }

    6.2 类的加载过程

    6.2.1 加载

    1. 通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。

    数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:

    1. 如果数组的组件类型是引用类型,那就递归采用类加载加载。
    2. 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
    3. 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。

    内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。
    加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。

    6.2.2 验证

    是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。

    文件格式验证

    1. 是否以魔数 0xCAFEBABE 开头
    2. 主、次版本号是否在当前虚拟机处理范围之内
    3. 常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
    4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    5. CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
    6. Class 文件中各个部分集文件本身是否有被删除的附加的其他信息
    7. ……

    只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。

    元数据验证

    1. 这个类是否有父类(除 java.lang.Object 之外)
    2. 这个类的父类是否继承了不允许被继承的类(final 修饰的类)
    3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    4. 类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)

    这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。

    字节码验证

    1. 保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
    2. 保证跳转指令不会跳转到方法体以外的字节码指令上
    3. 保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
    4. ……

    这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。

    符号引用验证

    1. 符号引用中通过字符创描述的全限定名是否能找到对应的类
    2. 在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
    3. 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
    4. ……

    最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。
    符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

    6.2.3 准备

    这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

    public static int value = 1127;
    这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 1127 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。

    基本数据类型的零值

    数据类型 零值 数据类型 零值
    int 0 boolean false
    long 0L float 0.0f
    short (short) 0 double 0.0d
    char '\u0000' reference null
    byte (byte) 0  

    特殊情况:如果类字段的字段属性表中存在 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 1127。

    6.2.4 解析

    这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

    1. 符号引用
      符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量。
    2. 直接引用
      直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和迅疾的内存布局实现有关

    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。

    6.2.5 初始化

    前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

    6.3 类加载器

    通过一个类的全限定名来获取描述此类的二进制字节流。

    6.3.1 双亲委派模型

    从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)

    1. 启动类加载器
      加载 lib 下或被 -Xbootclasspath 路径下的类

    2. 扩展类加载器
      加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类

    3. 引用程序类加载器
      ClassLoader负责,加载用户路径上所指定的类库。

     

    除顶层启动类加载器之外,其他都有自己的父类加载器。
    工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。

    6.3.2 破坏双亲委派模型

    keyword:线程上下文加载器(Thread Context ClassLoader)

    最后

    前面两次粗略的阅读,能理解内容,但是很难记住细节。每每碰到不会的知识点就上网查,所以知识点太碎片脑子里没有体系不仅更不容易记住,而且更加容易混乱。但是通过这种方式记录发现自己清晰了很多,就算以后忘记,知识再次捡起的成本也低了很多。

    这次还有一些章节虽然阅读了,但是还未完成记录。等自己理解深刻有空闲了就再次记录下来,这里的内容均出自周志明老师的《深入理解 Java 虚拟机》,有兴趣的可以入手纸质版。

    展开全文
  • JVM架构和GC垃圾回收机制(JVM面试不用愁)

    万次阅读 多人点赞 2017-06-10 16:49:15
    JVM架构和GC垃圾回收机制详解 JVM架构图分析 下图:参考网络+书籍,如有侵权请见谅 (想了解Hadoop内存溢出请看: Hadoop内存溢出(OOM)分类、参数调优化) JVM被分为三个主要的子系统 (1)类加载器子系统(2...

    JVM架构和GC垃圾回收机制详解

    JVM架构图分析

    下图:参考网络+书籍,如有侵权请见谅 (想了解Hadoop内存溢出请看: Hadoop内存溢出(OOM)分类、参数调优化

    JVM被分为三个主要的子系统

    (1)类加载器子系统(2)运行时数据区(3)执行引擎

    1. 类加载器子系统

    Java的动态类加载功能是由类加载器子系统处理。当它在运行时(不是编译时)首次引用一个类时,它加载、链接并初始化该类文件。

    1.1 加载

    类由此组件加载。启动类加载器 (BootStrap class Loader)、扩展类加载器(Extension class Loader)和应用程序类加载器(Application class Loader) 这三种类加载器帮助完成类的加载。

    1.  启动类加载器 – 负责从启动类路径中加载类,无非就是rt.jar。这个加载器会被赋予最高优先级。

    2.  扩展类加载器 – 负责加载ext 目录(jre\lib)内的类.

    3.  应用程序类加载器 – 负责加载应用程序级别类路径,涉及到路径的环境变量等etc.

    上述的类加载器会遵循委托层次算法(Delegation Hierarchy Algorithm)加载类文件

    1.2 链接

    1.  校验 – 字节码校验器会校验生成的字节码是否正确,如果校验失败,我们会得到校验错误

    2.  准备 – 分配内存并初始化默认值给所有的静态变量。

    3.  解析 – 所有符号内存引用方法区(Method Area)原始引用所替代。

    1.3 初始化

    这是类加载的最后阶段,这里所有的静态变量会被赋初始值, 并且静态块将被执行。

    2. 运行时数据区(Runtime Data Area)

    The 运行时数据区域被划分为5个主要组件:

    2.1 方法区(Method Area)

    所有类级别数据将被存储在这里,包括静态变量。每个JVM只有一个方法区,它是一个共享的资源。

    2.2 堆区(Heap Area)

    所有的对象和它们相应的实例变量以及数组将被存储在这里。每个JVM同样只有一个堆区。由于方法区堆区的内存由多个线程共享,所以存储的数据不是线程安全的

    2.3 栈区(Stack Area)

    对每个线程会单独创建一个运行时栈。对每个函数呼叫会在栈内存生成一个栈帧(Stack Frame)。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧被分为三个子实体:

    a 局部变量数组 – 包含多少个与方法相关的局部变量并且相应的值将被存储在这里。

    b 操作数栈 – 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令。

    c 帧数据 – 方法的所有符号都保存在这里。在任意异常的情况下,catch块的信息将会被保存在帧数据里面。

    如上是JVM三大核心区域

    2.4 PC寄存器

    每个线程都有一个单独的PC寄存器来保存当前执行指令的地址,一旦该指令被执行,pc寄存器会被更新至下条指令的地址。

    2.5 本地方法栈

    本地方法栈保存本地方法信息。对每一个线程,将创建一个单独的本地方法栈。

    3. 执行引擎

    分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。

    3.1  解释器:

     解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。

    编译器

    JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用JIT编译器将全部字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提高了系统的性能。

    a. 中间代码生成器 – 生成中间代码

    b. 代码优化器 – 负责优化上面生成的中间代码

    c. 目标代码生成器 – 负责生成机器代码或本机代码

    d.  探测器(Profiler) – 一个特殊的组件,负责寻找被多次调用的方法。

    3.3  垃圾回收器:

    收集并删除未引用的对象。可以通过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。

    Java本地接口 (JNI)JNI 会与本地方法库进行交互并提供执行引擎所需的本地库。

    本地方法库:它是一个执行引擎所需的本地库的集合。

    通过一个小程序认识JVM

    package com.spark.jvm;
    /**
     * 从JVM调用的角度分析java程序堆内存空间的使用:
     * 当JVM进程启动的时候,会从类加载路径中找到包含main方法的入口类HelloJVM
     * 找到HelloJVM会直接读取该文件中的二进制数据,并且把该类的信息放到运行时的Method内存区域中。
     * 然后会定位到HelloJVM中的main方法的字节码中,并开始执行Main方法中的指令
     * 此时会创建Student实例对象,并且使用student来引用该对象(或者说给该对象命名),其内幕如下:
     * 第一步:JVM会直接到Method区域中去查找Student类的信息,此时发现没有Student类,就通过类加载器加载该Student类文件;
     * 第二步:在JVM的Method区域中加载并找到了Student类之后会在Heap区域中为Student实例对象分配内存,
     * 并且在Student的实例对象中持有指向方法区域中的Student类的引用(内存地址);
     * 第三步:JVM实例化完成后会在当前线程中为Stack中的reference建立实际的应用关系,此时会赋值给student
     * 接下来就是调用方法
     * 在JVM中方法的调用一定是属于线程的行为,也就是说方法调用本身会发生在线程的方法调用栈:
     * 线程的方法调用栈(Method Stack Frames),每一个方法的调用就是方法调用栈中的一个Frame,
     * 该Frame包含了方法的参数,局部变量,临时数据等 student.sayHello();
     */
    public class HelloJVM {
    	//在JVM运行的时候会通过反射的方式到Method区域找到入口方法main
    	public static void main(String[] args) {//main方法也是放在Method方法区域中的
    		/**
    		 * student(小写的)是放在主线程中的Stack区域中的
    		 * Student对象实例是放在所有线程共享的Heap区域中的
    		 */
    		Student student = new Student("spark");
    		/**
    		 * 首先会通过student指针(或句柄)(指针就直接指向堆中的对象,句柄表明有一个中间的,student指向句柄,句柄指向对象)
    		 * 找Student对象,当找到该对象后会通过对象内部指向方法区域中的指针来调用具体的方法去执行任务
    		 */
    		student.sayHello();
    	}
    }
    
    class Student {
    	// name本身作为成员是放在stack区域的但是name指向的String对象是放在Heap中
    	private String name;
    	public Student(String name) {
    		this.name = name;
    	}
    	//sayHello这个方法是放在方法区中的
    	public void sayHello() {
    	System.out.println("Hello, this is " + this.name);
    	}
    }

    JVM三大性能调优参数:-Xms –Xmx –Xss

    -Xms –Xmx是对堆的性能调优参数,一般两个设置是一样的,如果不一样,当Heap不够用,会发生内存抖动。一般都调大这两个参数,并且两个大小一样。

    -Xss是对每一个线程栈的性能调优参数,影响堆栈调用的深度

    实战演示从OOM推导出JVM GC时候基于的内存结构:Young Generation(Eden、From、To)、OldGeneration、Permanent Generation

    JVMHeap区域(年轻代、老年代)和方法区(永久代)结构图:

    从Java GC的角度解读代码:程序20行new的Person对象会首先会进入年轻代Eden中(如果对象太大可能直接进入年老代)。在GC之前对象是存在Eden和from中的,进行GC的时候Eden中的对象被拷贝到To这样一个survive空间(survive幸存)空间:包括from和to,他们的空间大小是一样的,又叫s1和s2)中(有一个拷贝算法),From中的对象(算法会考虑经过GC幸存的次数)到一定次数(阈值(如果说每次GC之后这个对象依旧在Survive中存在,GC一次他的Age就会加1,默认15就会放到OldGeneration。但是实际情况比较复杂,有可能没有到阈值就从Survive区域直接到Old Generation区域。在进行GC的时候会对Survive中的对象进行判断,Survive空间中有一些对象Age是一样的,也就是经过的GC次数一样,年龄相同的这样一批对象的总和大于等于Survive空间一半的话,这组对象就会进入old Generation中,(是一种动态的调整))),会被复制到OldGeneration,如果没到次数From中的对象会被复制到To中,复制完成后To中保存的是有效的对象,Eden和From中剩下的都是无效的对象,这个时候就把Eden和From中所有的对象清空。在复制的时候Eden中的对象进入To中,To可能已经满了,这个时候Eden中的对象就会被直接复制到Old Generation中,From中的对象也会直接进入Old Generation中。就是存在这样一种情况,To比较小,第一次复制的时候空间就满了,直接进入old Generation中。复制完成后,To和From的名字会对调一下,因为Eden和From都是空的,对调后Eden和To都是空的,下次分配就会分配到Eden。一直循环这个流程。好处:使用对象最多和效率最高的就是在Young Generation中,通过From to就避免过于频繁的产生FullGC(Old Generation满了一般都会产生FullGC)

    虚拟机在进行MinorGC(新生代的GC)的时候,会判断要进入OldGeneration区域对象的大小,是否大于Old Generation剩余空间大小,如果大于就会发生Full GC

    刚分配对象在Eden中,如果空间不足尝试进行GC,回收空间,如果进行了MinorGC空间依旧不够就放入Old Generation,如果OldGeneration空间还不够就OOM了。

    比较大的对象,数组等,大于某值(可配置)就直接分配到老年代,(避免频繁内存拷贝)

    年轻代和年老代属于Heap空间的

    Permanent Generation(永久代)可以理解成方法区,(它属于方法区)也有可能发生GC,例如类的实例对象全部被GC了,同时它的类加载器也被GC掉了,这个时候就会触发永久代中对象的GC。

    如果OldGeneration满了就会产生FullGC

    满原因:1,from survive中对象的生命周期到一定阈值

    2,分配的对象直接是大对象

    3、由于To 空间不够,进行GC直接把对象拷贝到年老代(年老代GC时候采用不同的算法)

    如果Young Generation大小分配不合理或空间比较小,这个时候导致对象很容易进入Old Generation中,而Old Generation中回收具体对象的时候速度是远远低于Young Generation回收速度。

    因此实际分配要考虑年老代和新生代的比例,考虑Eden和survives的比例

    Permanent Generation中发生GC的时候也对性能影响非常大,也是Full GC

    JVM GC时候核心参数:

    -XX:NewRatio –XX:SurvivorRatio –XX:NewSize –XX:MaxNewSize

    –XX:NewSize–XX:MaxNewSize指定新生代初始大小和最大大小。

    1,-XX:NewRatio    是年老代 新生代相对的比例,比如NewRatio=2,表明年老代是新生代的2倍。老年代占了heap的2/3,新生代占了1/3

    2,-XX:SurvivorRatio 配置的是在新生代里面Eden和一个Servive的比例

    如果指定NewRatio还可以指定NewSizeMaxNewSize,如果同时指定了会如何???

    NewRatio=2,这个时候新生代会尝试分配整个Heap大小的1/3的大小,但是分配的空间不会小于-XX:NewSize也不会大于 –XX:MaxNewSize

    3,-XX:NewSize –XX:MaxNewSize

    实际设置比例还是设置固定大小,固定大小理论上速度更高。

    -XX:NewSize –XX:MaxNewSize理论越大越好,但是整个Heap大小是有限的,一般年轻代的设置大小不要超过年老代。

    -XX:SurvivorRatio新生代里面Eden和一个Servive的比例,如果SurvivorRatio是5的话,也就是Eden区域是SurviveTo区域的5倍。Survive由From和To构成。结果就是整个Eden占用了新生代5/7,From和To分别占用了1/7,如果分配不合理,Eden太大,这样产生对象很顺利,但是进行GC有一部分对象幸存下来,拷贝到To,空间小,就没有足够的空间,对象会被放在old Generation中。如果Survive空间大,会有足够的空间容纳GC后存活的对象,但是Eden区域小,会被很快消耗完,这就增加了GC的次数。

    JVM的GC日志解读:

    一、 JVM YoungGeneration下MinorGC日志详解

    [GC (Allocation Failure) [PSYoungGen:2336K->288K(2560K)] 8274K->6418K(9728K), 0.0112926 secs] [Times:user=0.06 sys=0.00, real=0.01 secs]

    PSYoungGen(是新生代类型,新生代日志收集器),2336K表示使用新生代GC前,占用的内存,->288K表示GC后占用的内存,(2560K)代表整个新生代总共大小

    8274K(GC前整个JVM Heap对内存的占用)->6418K(MinorGC后内存占用总量)(9728K)(整个堆的大小)0.0112926 secs(Minor GC消耗的时间)] [Times: user=0.06 sys=0.00, real=0.01 secs] 用户空间,内核空间时间的消耗,real整个的消耗

    二、 JVM的GC日志Full GC日志每个字段彻底详解

    [Full GC (Ergonomics) [PSYoungGen: 984K->425K(2048K)] [ParOldGen:7129K->7129K(7168K)] 8114K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1022588 secs] [Times: user=0.56 sys=0.02,real=0.10 secs]

    [Full GC (Allocation Failure) [PSYoungGen: 425K->425K(2048K)][ParOldGen: 7129K->7129K(7168K)] 7555K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1003696 secs] [Times: user=0.64 sys=0.03,real=0.10 secs]

    [Full GC(表明是Full GC) (Ergonomics) [PSYoungGen:FullGC会导致新生代Minor GC产生]984K->425K(2048K)][ParOldGen:(老年代GC)7129K(GC前多大)->7129K(GC后,并没有降低内存占用,因为写的程序不断循环一直有引用)(7168K) (老年代总容量)] 8114K(GC前占用整个Heap空间大小)->7555K (GC后占用整个Heap空间大小) (9216K) (整个Heap大小,JVM堆的大小), [Metaspace: (java6 7是permanentspace,java8改成Metaspace,类相关的一些信息) 2613K->2613K(1056768K) (GC前后基本没变,空间很大)], 0.1022588 secs(GC的耗时,秒为单位)] [Times: user=0.56 sys=0.02, real=0.10 secs](用户空间耗时,内核空间耗时,真正的耗时时间)

    三、 Java8中的JVM的MetaSpace

    Metaspace的使用C语言实现的,使用的是OS的空间,Native Memory Space可动态的伸缩,可以根据类加载的信息的情况,在进行GC的时候进行调整自身的大小,来延缓下一次GC的到来。

    可以设置Metaspace的大小,如果超过最大大小就会OOM,不设置如果把整个操作系统的内存耗尽了出现OOM,一般会设置一个足够大的初始值,安全其间会设置最大值。

    永久代发生GC有两种情况,类的所有的实例被GC掉,且class load不存。

    对于元数据空间 简化了GC, class load不存在了就需要进行GC。

    三种基本的GC算法基石

    一、 标记/清除算法

    内存中的对象构成一棵树,当有效的内存被耗尽的时候,程序就会停止,做两件事,第一:标记,标记从树根可达的对象(途中水红色),第二:清除(清楚不可达的对象)。标记清除的时候有停止程序运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。

    缺点:递归效率低性能低;释放空间不连续容易导致内存碎片;会停止整个程序运行;

    二、 复制算法

    把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象1,4清除掉,变成空闲区。

    速度快但耗费空间,假定活动区域全部是活动对象,这个时候进行交换的时候就相当于多占用了一倍空间,但是没啥用。

    三、 标记整理算法

    平衡点

    标记谁是活跃对象,整理,会把内存对象整理成一课树一个连续的空间,

    JVM垃圾回收分代收集算法

    综合了上述算法优略

    1, 分代GC在新生代的算法:采用了GC的复制算法,速度快,因为新生代一般是新对象,都是瞬态的用了可能很快被释放的对象。

    2, 分代GC在年老代的算法 标记/整理算法,GC后会执行压缩,整理到一个连续的空间,这样就维护着下一次分配对象的指针,下一次对象分配就可以采用碰撞指针技术,将新对象分配在第一个空闲的区域。

    JVM垃圾回收器串行、并行、并发垃圾回收器概述

    1, JVM中不同的垃圾回收器

    2, 串行,并行,并发垃圾回收器(和JVM历史有关系,刚开始串行)

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

    JVM中Serial收集器、ParNew收集器、Parallel收集器解析

    Serial收集器 单线程方式(没有线程切换开销,如果受限物理机器单线程可采用)串行且采用stop the world在工作的时候程序会停止

    Serial和serial old

    ParNew收集器:多线程(多CPU和多Core的环境中高效),生产环境对低延时要求高的话,就采用ParNew和CMS组合来进行server端的垃圾回收

    Parallel 收集器:多线程,并行, 它可以控制JVM吞吐量的大小,吞吐量优先的收集器,一般设置1%,可设置程序暂停的时间,会通过把新生代空间变小,来完成回收,频繁的小规模垃圾回收,会影响程序吞吐量大小

    JVM中CMS收集器解密

    低延迟进行垃圾回收,在线服务和处理速度要求高的情况下很重要

    配置:XX:UseConcMarkSweepGC

    concurrence(并发) Mark(标记)Sweep(清理)

    低延时

    把垃圾回收分成四个阶段

    CMS-initial-mark初始标记阶段会stop the world,短暂的暂停程序根据跟对象标记的对象所连接的对象是否可达来标记出哪些可到达

    CMS-concurrent-mark并发标记,根据上一次标记的结果确定哪些不可到达,线程并发或交替之行,基本不会出现程序暂停。

    CMS-remark再次标记,会出现程序暂停,所有内存那一时刻静止,确保被全部标记,有可能第二阶段之前有可能被标记为垃圾的对象有可能被引用,在此标记确认。

    CMS-concurrent-sweep并发清理垃圾,把标记的垃圾清理掉了,没有压缩,有可能产生内存碎片,不连续的内存块,这时候就不能更好的使用内存,可以通过一个参数配置,根据内存的情况执行压缩。

    JVM中G1收集器

    可以像CMS收集器一样,GC操作与应用的现场一起并发执行

    紧凑的空闲内存区域且没有很长的GC停顿时间

    需要可预测的GC暂停耗时

    不想牺牲太多吞吐量性能

    启动后不需要请求更大的Java堆

    通过案例瞬间理解JVM中PSYoungGen、ParOldGen、MetaSpace

    Heap
     PSYoungGen      total 2560K, used 321K[0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
      eden space 2048K, 15% used[0x00000007bfd00000,0x00000007bfd50568,0x00000007bff00000)
      from space 512K, 0% used[0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
      to   space 512K, 0% used[0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
     ParOldGen       total 7168K, used 7097K[0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
     object space 7168K, 99%used [0x00000007bf600000,0x00000007bfcee7b8,0x00000007bfd00000)
     Metaspace       used 2647K, capacity 4486K, committed4864K, reserved 1056768K
     class space    used 289K, capacity 386K, committed 512K,reserved 1048576K

    PSYoungGen是eden + from

    使用MAT对Dump文件进行分析实战

    导出Dump文件

     

    MapReduce过程详解及其性能优化

    展开全文
  • 一次性精通JVM JAVA虚拟机

    千人学习 2019-12-13 20:27:37
    为什么要学JVM JVM是JAVA的老祖宗,一切JAVA代码都运行在JVM之上,只有深入理解和掌握虚拟机才能写出更强大的代码。 JVM是高级程序员的必备技能,是程序员进阶,拿高薪的必备技能。 ...
  • JVM从入门到入魔

    2020-10-16 15:50:42
    本课程主要是分享当前互联网Java架构及高级热门技术,由业内技术大牛,行业及实战经验丰富的讲师进行技术分享。其中涵盖redis/mongodb/dubbo/zookeeper/kafka 高并发、高可用、分布式、微服务技术。
  • 深入详细讲解JVM原理

    万次阅读 多人点赞 2018-06-04 22:55:47
    一、JVM体系结构: 类装载器ClassLoader:用来装载.class文件 执行引擎:执行字节码,或者执行本地方法 运行时数据区:方法区、堆、Java栈、程序计数器、本地方法栈JVM把描述类数据的字节码.Class文件加载到内存...

    一、JVM体系结构:

         类装载器ClassLoader:用来装载.class文件

         执行引擎:执行字节码,或者执行本地方法

         运行时数据区方法区、堆、Java栈、程序计数器、本地方法栈


    JVM把描述类数据的字节码.Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制


    二、JVM原理:

    JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行


    三、JVM执行程序的过程:

    1、加载.class文件

    2、管理并分配内存

    3、执行垃圾收集

    四步完成JVM环境:

    1、创建JVM装载环境和配置

    2、装载JVM.dll

    3、初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例

    4、调用JNIEnv实例装载并处理class类。



    四、JVM的生命周期:

    JVM实例和JVM执行引擎实例:

    (1)JVM实例对应了一个独立运行的java程序——进程级别

             一个运行时的Java虚拟机(JVM)负责运行一个Java程序。

             当启动一个Java程序时,一个虚拟机实例诞生;当程序关闭退出,这个虚拟机实例也就随之消亡。

             如果在同一台计算机上同时运行多个Java程序,将得到多个Java虚拟机实例,每个Java程序都运行于它自己的Java虚拟机实例中。

    (2)JVM执行引擎实例则对应了属于运行程序的线程——线程级别

    JVM的生命周期:

    (1)JVM实例的诞生

    当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。

    (2)JVM实例的运行

    main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。

    (3)JVM实例的消亡

    当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。



    五、类装载器:

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接



    1.加载:(重点)
    加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:
    1.通过“类全名”来获取定义此类的二进制字节流

    2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构

    3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

    相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

    加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。

    2.验证:(了解)

    验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
    验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

    3.准备:(了解)

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:

    public static int value  = 12;

    那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。

    上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建设上面类变量value定义为:

    public static final int value = 123;

    编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

    4.解析:(了解)
    解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
    符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。

    直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

    解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

    1.类、接口的解析

    2.字段解析

    3.类方法解析

    4.接口方法解析

    5.初始化:(了解)

    类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。


    类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了4种类加载器,启动(Bootstrap ClassLoader)类加载器、扩展(Extension ClassLoader)类加载器、应用程序(Application ClassLoader)类加载器、自定义(User ClassLoader)类加载器

    1.启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。

    2.扩展类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。

    3.应用程序类加载器:这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。

    4.自定义加载器:用户自己定义的类加载器。



    六、执行引擎:

    执行引擎负责具体的代码调用及执行过程。就目前而言,所有的执行引擎的基本一致:

    1. 输入:字节码文件
    2. 处理:字节码解析
    3. 输出:执行结果。

    物理机的执行引擎是由硬件实现的,和物理机的执行过程不同的是虚拟机的执行引擎由于自己实现的。


    类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),然后执行引擎执行会执行这些字节码。通过类装载器装载的,被分配到JVM的运行时数据区的字节码会被执行引擎执行。执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。不过Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被JVM执行的语言。字节码可以通过以下两种方式转换成合适的语言。

    • 解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。
    • 即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。




    七、运行时数据区:

    包括:方法区——线程共享

              ——线程共享

              Java栈(虚拟机栈)——非线程共享

              程序计数器——非线程共享

              本地方法栈——非线程共享


    JVM运行时会分配好方法区和堆,而JVM每遇到一个线程,就为其分配一个程序计数器、Java栈、本地方法栈,当线程终止时,三者(程序计数器、Java栈、本地方法栈)所占用的内存空间也会释放掉。

    程序计数器、Java栈、本地方法栈的生命周期与所属线程相同,而方法区和堆的生命周期与JAVA程序运行生命周期相同,所以gc只发生在线程共享的区域(大部分发生在Heap上)。


    7.1、方法区:

    有时候也称为永久代(Permanent Generation),在方法区中,存储了每个类的信息(包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,在这里进行的GC主要是方法区里的常量池和类型的卸载。当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

    在方法区中有一个非常重要的部分就是运行时常量池用于存放静态编译产生的字面量和符号引用。运行时生成的常量也会存在这个常量池中,比如String的intern方法它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。

    7.2、堆:

    Java中的堆是用来存储对象实例以及数组(当然,数组引用是存放在Java栈中的)。堆是被所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。在JVM中只有一个堆。堆是Java垃圾收集器管理的主要区域,Java的垃圾回收机制会自动进行处理。

    Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配

    堆空间分为老年代年轻代刚创建的对象存放在年轻代,而老年代中存放生命周期长久的实例对象。年轻代中又被分为Eden区两个Survivor区(From Space和To Space)。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次GC仍然存活的,就会被转移到老年代。 当一个对象大于eden区而小于old区(老年代)的时候会直接扔到old区。 而当对象大于old区时,会直接抛出OutOfMemoryError(OOM)

    7.3、Java栈:

    Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈。JVM栈是线程私有的,每个线程创建的同时都会创建自己的JVM栈,互不干扰。

    Java栈是Java方法执行的内存模型。Java栈中存放的是一个个的栈帧每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。

    局部变量表:用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译期就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的

    操作数栈:栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

    指向运行时常量池的引用:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

    方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

    7.4、程序计数器:

    程序计数器(Program Counter Register),也有称作为PC寄存器。

    由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的

    在JVM规范中规定,如果线程执行的是非native(本地)方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。

    由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

    7.5、本地方法栈:

    JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。



    八、JVM各区域潜在异常

    8.1、程序计数器

    此区域是JVM规范中唯一一个不存在OOM(OutOfMemory)的区域。


    8.2、Java栈(局部变量空间)

    (1)StackOverflowError :栈深度大于虚拟机所允许的深度。

    (2)OOM :如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可以动态扩展,只不过Java虚拟机规范中的也允许固定长度的虚拟机栈),如果扩展是无法申请到足够的内存。


    8.3、本地方法栈

    (1)StackOverflowError :栈深度大于虚拟机所允许的深度。

    (2)OOM


    8.4、堆

    OOM: 堆无法扩展时。


    8.5、方法区

    OOM



    九、类加载机制

    9.1、什么是类加载机制

    Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。

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


    9.2、类加载流程图


    类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过以下步骤:

    加载:查找和导入Class文件

    在装载阶段,虚拟机需要完成以下3件事情

            (1) 通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流

            (2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

            (3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

    链接:把类的二进制数据合并到JRE中

    1.验证:检查载入Class文件数据的正确性。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统奔溃。

    为static变量在方法区中分配空间,设置变量的初始值

    准备阶段是正式为类变量分配并设置类变量初始值的阶段,这些内存都将在方法区中进行分配,需要说明的是:

    这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;这里所说的初始值“通常情况”是数据类型的零值,假如:

    public static int value = 123;

    value在准备阶段过后的初始值为0而不是123,而把value赋值的putstatic指令将在初始化阶段才会被执行

    虚拟机将常量池的符号引用转变成直接引用。例如"aaa"为常量池的一个值,直接把"aaa"替换成存在于内存中的地址。

    引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即                   可。符号引用与虚拟机实现的内存布局无关,引用 的目标并不一定已经加载到内存中。

    引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的                内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

    初始化:对类的静态变量,静态代码块执行初始化操作

    初始化阶段是执行类构造器<clinit>()方法。在类构造器方法中,它将由编译器自动收集类中的所有类变量的赋值动作(准备阶段的a正是被赋值a)和静态变量与静态语句块static{}合并。

    使用:

    正常使用。

    卸载:

    GC把无用对象从内存中卸载。


    9.3、类加载与初始化时机

    (1)类加载时机

    当应用程序启动的时候,所有的类会被一次性加载吗?估计你早已知道答案,当然不能,因为如果一次性加载,内存资源有限,可能会影响应用程序的正常运行。那类什么时候被加载呢?例如,A a=new A(),一个类真正被加载的时机是在创建对象的时候,才会去执行以上过程,加载类。当我们测试的时候,最先加载拥有main方法的主线程所在类。

    (2)类初始化时机

    主动引用的五种情况(发生类初始化过程)

    1. 使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。
    2. 通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。
    3. 当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。
    4. 当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。
    5. 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

    被动引用的情况(不会发生类的初始化

        1.当访问一个静态变量时,只有真正声明这个变量的类才会初始化。(子类调用父类的静态变量,只有父类初始化,子类不初始化)。

    [java] view plain copy
    1. //父类    
    2. public class SuperClass {    
    3.     //静态变量value    
    4.     public static int value = 666;    
    5.     //静态块,父类初始化时会调用    
    6.     static{    
    7.         System.out.println("父类初始化!");    
    8.     }    
    9. }    
    10.     
    11. //子类    
    12. public class SubClass extends SuperClass{    
    13.     //静态块,子类初始化时会调用    
    14.     static{    
    15.         System.out.println("子类初始化!");    
    16.     }    
    17. }    
    18.     
    19. //主类、测试类    
    20. public class NotInit {    
    21.     public static void main(String[] args){    
    22.         System.out.println(SubClass.value);    
    23.     }    
    24. }    

    输出结果:


       2.通过数组定义类引用,不会触发此类的初始化

    [java] view plain copy
    1. //父类    
    2. public class SuperClass {    
    3.     //静态变量value    
    4.     public static int value = 666;    
    5.     //静态块,父类初始化时会调用    
    6.     static{    
    7.         System.out.println("父类初始化!");    
    8.     }    
    9. }    
    10.     
    11. //主类、测试类    
    12. public class NotInit {    
    13.     public static void main(String[] args){    
    14.         SuperClass[] test = new SuperClass[10];    
    15.     }    
    16. }    
    没有任何结果输出!

       3.静态常量不会触发此类的初始化,因为在编译阶段就存储在常量池中,不会引用到定义常量的类

    [java] view plain copy
    1. //常量类    
    2. public class ConstClass {    
    3.     static{    
    4.         System.out.println("常量类初始化!");    
    5.     }    
    6.         
    7.     public static final String HELLOWORLD = "hello world!";    
    8. }    
    9.     
    10. //主类、测试类    
    11. public class NotInit {    
    12.     public static void main(String[] args){    
    13.         System.out.println(ConstClass.HELLOWORLD);    
    14.     }    
    15. }    

    输出结果:




    十、JVM垃圾回收机制

    GC的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停


    10.1、哪些内存需要回收

    JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分


    10.1.1、引用计数法

    引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

    优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

    缺点无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。


    10.1.2、可达性分析算法

    程序把所有的引用关系看作一张图,从一个节点GC ROOTS开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

    在Java语言中,可作为GC Roots的对象包括下面几种:

      a) 虚拟机栈中引用的对象(栈帧中的本地变量表);

      b) 方法区中类静态属性引用的对象;

      c) 方法区中常量引用的对象;

      d) 本地方法栈中JNI(Native方法)引用的对象。


    不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

    (1)强引用:

    在程序代码中普遍存在的,类似Object obj=new Object()这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

    (2)软引用:

    用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

    (3)弱引用:

    也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

    (4)虚引用:

    也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知(用来得知对象是否被GC)。


    无论引用计数算法还是可达性分析算法都是基于强引用而言的

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

    第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;

    第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

    第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。


    10.2、方法区如何判断是否需要回收

    方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

    (1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例

    (2)加载该类的ClassLoader已经被回收

    (3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。


    10.3、常用的垃圾回收算法

    10.3.1、标记-清除(Mark-Sweep)算法

    标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。


    主要缺点:

    • 一个是效率问题,标记和清除过程的效率都不高。
    • 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致:当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。


    10.3.2、复制Copying)算法

    为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。


    这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

    很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。


    10.3.3、标记-整理(Mark-Compact)算法

    为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。



    10.3.4、分代收集(Generational Collection)算法

    分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)年轻代(Young Generation)在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

    老年代的特点是每次垃圾收集时只有少量对象需要被回收,而年轻代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。


    年轻代(Young Generation)的回收算法:

    在年轻代中jvm使用的是Mark-copy(标记-复制)算法

    a)所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

    b)年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当另外一个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。

    c)当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

    d)新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。


    老年代(Old Generation)的回收算法:

    老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact(标记-整理)算法

    a)在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象

    b)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC或Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。


    永久代(Permanent Generation)的回收算法:

    永久代(permanent generation)也称为“方法区(method area)”,他存储class对象和字符串常量。所以这块内存区域绝对不是永久的存放从老年代存活下来的对象的。在这块内存中有可能发生垃圾回收。发生在这里垃圾回收也被称为major GC



    十一、常见的垃圾收集器

    11.1、Serial收集器(复制算法)

    新生代单线程收集器,标记和清理都是单线程,优点是简单高效单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。


    说明:1. 需要STW(Stop The World),停顿时间长。2. 简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。


    11.2、Serial Old收集器(标记-整理算法)

    老年代单线程收集器,Serial收集器的老年代版本。它的优点是实现简单高效,但是缺点是会给用户带来停顿


    11.3、ParNew收集器(复制算法)

    新生代收集器,可以认为是Serial收集器的多线程版本,使用多个线程进行垃圾收集,在多核CPU环境下有着比Serial更好的表现。是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作它默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。



    11.4、Parallel Scavenge收集器(复制算法)

    Parallel Scavenge收集器是一个新生代多线程收集器并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法。追求高吞吐量,高效利用CPU,主要是为了达到一个可控的吞吐量。Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器Parallel Scavenge收集器也被称为“吞吐量优先收集器”

    适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。


    11.5、Parallel Old收集器(标记-整理算法)

    Parallel Scavenge收集器的老年代版本,并行收集器吞吐量优先使用多线程标记-整理(Mark-Compact算法。



    11.6、CMS(Concurrent Mark Sweep)收集器(标记-清除算法

    CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。

    收集过程分为如下四步:

    (1). 初始标记,标记GCRoots能直接关联到的对象,时间很短。

    (2). 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。

    (3). 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。

    (4). 并发清除,回收内存空间,时间很长。

    其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。运行过程如下图所示:

    说明:1. 对CPU资源非常敏感,可能会导致应用程序变慢,吞吐率下降。2. 无法处理浮动垃圾,因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。3. 由于采用的标记 - 清除算法,会产生大量的内存碎片,不利于大对象的分配,可能会提前触发一次Full GC。虚拟机提供了-XX:+UseCMSCompactAtFullCollection参数来进行碎片的合并整理过程,这样会使得停顿时间变长,虚拟机还提供了一个参数配置,-XX:+CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,接着来一次带压缩的GC。


    11.7、G1

    G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

    G1收集器有以下特点:

    (1). 并行和并发。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。

    (2). 分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。

    (3). 空间整合。基于标记 - 整理算法,无内存碎片产生。

    (4). 可预测的停顿。能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

    使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region的集合。


    十二、Minor GC、Full GC触发条件

    12.1、Minor GC触发条件

    • Eden区域满了,或者新创建的对象大小 > Eden所剩空间
    • CMS设置了CMSScavengeBeforeRemark参数,这样在CMS的Remark之前会先做一次Minor GC来清理新生代,加速之后的Remark的速度。这样整体的stop-the-world时间反而短
    • Full GC的时候会先触发Minor GC


    12.2、Full GC触发条件

    • Minor GC后存活的对象晋升到老年代时由于悲观策略的原因,有两种情况会触发Full GC, 一种是之前每次晋升的对象的平均大小 > 老年代剩余空间;一种是Minor GC后存活的对象超过了老年代剩余空间。这两种情况都是因为老年代会为新生代对象的晋升提供担保,而每次晋升的对象的大小是无法预测的,所以只能基于统计,一个是基于历史平均水平,一个是基于下一次可能要晋升的最大水平。这两种情况都是属于promotion failure
    • CMS失败,发生concurrent mode failure会引起Full GC,这种情况下会使用Serial Old收集器,是单线程的,对GC的影响很大。concurrent mode failure产生的原因是老年代剩余的空间不够导致了和gc线程并发执行的用户线程创建的大对象(由PretenureSizeThreshold控制新生代直接晋升老年代的对象size阀值)不能进入到老年代,只要stop the world来暂停用户线程,执行GC清理。可以通过设置CMSInitiatingOccupancyFraction预留合适的CMS执行时剩余的空间
    • 新生代直接晋升到老年代的大对象超过了老年代的剩余空间,引发Full GC。注意于promotion failure的区别,promotion failure指的是Minor GC后发生的担保失败
    • Perm永久代空间不足会触发Full GC,可以让CMS清理永久代的空间。设置CMSClassUnloadingEnabled即可
    • System.gc()引起的Full GC,可以设置DisableExplicitGC来禁止调用System.gc引发Full GC

    结论:

    1. Full GC == Major GC指的是对老年代/永久代的stop the world的GC

    2. Full GC的次数 = 老年代GC时 stop the world的次数

    3. Full GC的时间 = 老年代GC时 stop the world的总时间

    4. CMS 不等于Full GC,我们可以看到CMS分为多个阶段,只有stop the world的阶段被计算到了Full GC的次数和时间,而和业务线程并发的GC的次数和时间则不被认为是Full GC

    5. Full GC本身不会先进行Minor GC,我们可以配置,让Full GC之前先进行一次Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS时,设置CMSScavengeBeforeRemark优化,让CMS remark之前先进行一次Minor GC。


    12.3、Heap什么时候会发生OOM

    • 花在GC的时间超过了GCTimeLimit,这个值默认是98%
    • GC后的容量小于GCHeapFreeLimit,这个值默认是2%

    12.4、stop-the-world

    不管选择哪种GC算法,stop-the-world都是不可避免的。Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作中断了的线程直到GC任务结束才继续它们的任务。GC调优通常就是为了改善stop-the-world的时间。



    十三、降低GC的调优的策略

    13.1、代大小优化

    最关键参数:-Xms、 -Xmx 、-Xmn 、-XX:SurvivorRatio、-XX:MaxTenuringThreshold、-XX:PermSize、-XX:MaxPermSize

    -Xms、 -Xmx 通常设置为相同的值,避免运行时要不断扩展JVM内存,这个值决定了JVM heap所能使用的最大内存。

    -Xmn 决定了新生代空间的大小,新生代Eden、S0、S1三个区域的比率可以通过-XX:SurvivorRatio来控制(假如值为 4  表示:Eden:S0:S1 = 4:3:3 )

    -XX:MaxTenuringThreshold 控制对象在经过多少次minor GC之后进入老年代,此参数只有在Serial 串行GC时有效。

    -XX:PermSize、-XX:MaxPermSize 用来控制方法区的大小,通常设置为相同的值。

    1.避免新生代大小设置过小

    当新生代设置过小时,会产生两种比较明显的现象,一是minor GC次数频繁,二是可能导致 minor GC对象直接进入老年代。当老年代内存不足时,会触发Full GC。

    2.避免新生代大小设置过大

    新生代设置过大,会带来两个问题:一是老年代变小,可能导致Full  GC频繁执行;二是 minor GC 执行回收的时间大幅度增加

    3.避免Survivor区过大或过小

    -XX:SurvivorRatio参数的值越大,就意味着Eden区域变大,minor GC次数会降低,但两块Survivor区域变小,如果超过Survivor区域内存大小的对象在minor GC后仍没被回收,则会直接进入老年代,

    -XX:SurvivorRatio参数值设置过小,就意味着Eden区域变小,minor GC触发次数会增加,Survivor区域变大,意味着可以存储更多在minor GC后任存活的对象,避免其进入老年代。

    4.合理设置对象在新生代存活的周期

    新生代存活周期的值决定了新生代对象在经过多少次Minor GC后进入老年代。因此这个值要根据自己的应用来调优,Jvm参数上这个值对应的为-XX:MaxTenuringThreshold,默认值为15次。


    减少GC开销的措施

        1)不要显式调用System.gc()此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。

         2)尽量减少临时对象的使用临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

        3)对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

        4)尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

        5)能用基本类型如Int,Long,就不用Integer,Long对象基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

        6)尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

        7)分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。


    --------------------------------------------------------------------------------------------------------------------------------------

    内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。 

    内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。 

    二者的关系

    1. 内存泄漏的堆积最终会导致内存溢出
    2. 内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
    3. 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
    4. 内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错,

    内存泄漏的分类(按发生方式来分类)

    1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
    2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
    3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
    4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

    内存溢出的原因及解决方法:

    1. 内存溢出原因: 
      1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据; 
      2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收; 
      3.代码中存在死循环或循环产生过多重复的对象实体; 
      4.使用的第三方软件中的BUG; 
      5.启动参数内存值设定的过小
    2. 内存溢出的解决方案: 
      第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)

      第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。

      第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。



    内存分配:


    对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的Eden Space和From Space,少数情况下会直接分配在老年代。如果新生代的Eden Space和From Space的空间不足,则会发起一次GC,如果进行了GC之后,Eden Space和From Space能够容纳该对象就放在Eden Space和From Space。在GC的过程中,会将Eden Space和From  Space中的存活对象移动到To Space,然后将Eden Space和From Space进行清理。如果在清理的过程中,To Space无法足够来存储某个对象,就会将该对象移动到老年代中。在进行了GC之后,使用的便是Eden space和To Space了,下次GC时会将存活对象复制到From Space,如此反复循环。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。

      一般来说,大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组,比如:

      byte[] data = new byte[4*1024*1024]

      这种一般会直接在老年代分配存储空间。

      当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。

    展开全文
  • JVM原理详解

    千人学习 2018-12-20 18:34:03
    1,通俗讲解JVM运行原理,各组成部分的作用,包括栈、堆等 2,使用visio视图,直观介绍JVM组成 3,介绍线程安全产生的原因 4,面试题中存在的些许JVM题目讲解
  • 可能是最全面的 Java G1学习笔记

    千次阅读 2018-12-26 09:22:42
    引子 最近遇到很多朋友过来咨询G1调优的问题,我自己去年有专门学过一次G1,但是当时只是看了个皮毛,因此自己也有不少问题。总体来讲,对于G1我有几个疑惑,希望能够在这篇文章中得到解决。 ...

    转自:https://mp.weixin.qq.com/s/Shw0jtVse1QqNbFCyYmfZA

    引子

    最近遇到很多朋友过来咨询G1调优的问题,我自己去年有专门学过一次G1,但是当时只是看了个皮毛,因此自己也有不少问题。总体来讲,对于G1我有几个疑惑,希望能够在这篇文章中得到解决。

    1. G1出现的初衷是什么?

    2. G1适合在什么场景下使用?

    3. G1的trade-off是什么?

    4. G1的详细过程?

    5. 如何理解G1的gc日志?

    6. G1的调优思路?

    7. G1和CMS的对比和选择?

    一、基础知识

    1. 初衷

    在G1提出之前,经典的垃圾收集器主要有三种类型:串行收集器、并行收集器和并发标记清除收集器,这三种收集器分别可以是满足Java应用三种不同的需求:内存占用及并发开销最小化、应用吞吐量最大化和应用GC暂停时间最小化,但是,上述三种垃圾收集器都有几个共同的问题:(1)所有针对老年代的操作必须扫描整个老年代空间;(2)新生代和老年代是独立的连续的内存块,必须先决定年轻代和老年代在虚拟地址空间的位置。

    2. 设计目标

    G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。

    3. 使用场景

    G1适用于以下几种应用:

    • 可以像CMS收集器一样,允许垃圾收集线程和应用线程并行执行,即需要额外的CPU资源;

    • 压缩空闲空间不会延长GC的暂停时间;

    • 需要更易预测的GC暂停时间;

    • 不需要实现很高的吞吐量

    二、G1的重要概念

    1. 分区(Region)

    G1采取了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控制等问题——G1将整个堆分成相同大小的分区(Region),如下图所示。

    G1的堆模型

    每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。
    年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。

    新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。

    G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。每个分区的大小从1M到32M不等,但是都是2的冥次方。

    2. 收集集合(CSet)

    一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。CSet会占用不到整个堆空间的1%大小。

    3. 已记忆集合(RSet)

    RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。

    如下图所示,Region1和Region3中的对象都引用了Region2中的对象,因此在Region2的RSet中记录了这两个引用。

    RSet的示意图

    摘一段R大的解释:G1 GC则是在points-out的card table之上再加了一层结构来构成points-into RSet:每个region会记录下到底哪些别的region有指向自己的指针,而这些指针分别在哪些card的范围内。 这个RSet其实是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。 举例来说,如果region A的RSet里有一项的key是region B,value里有index为1234的card,它的意思就是region B的一个card里有引用指向region A。所以对region A来说,该RSet记录的是points-into的关系;而card table仍然记录了points-out的关系。

    4. Snapshot-At-The-Beginning(SATB)

    SATB是维持并发GC的正确性的一个手段,G1GC的并发理论基础就是SATB,SATB是由Taiichi Yuasa为增量式标记清除垃圾收集器设计的一个标记算法。Yuasa的SATAB的标记优化主要针对标记-清除垃圾收集器的并发标记阶段。按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。

    SATB算法创建了一个对象图,它是堆的一个逻辑“快照”。标记数据结构包括了两个位图:previous位图和next位图。previous位图保存了最近一次完成的标记信息,并发标记周期会创建并更新next位图,随着时间的推移,previous位图会越来越过时,最终在并发标记周期结束的时候,next位图会将previous位图覆盖掉。
    下面我们以几个图例来描述SATB算法的过程:

    在并发周期开始之前,NTAMS字段被设置到每个分区当前的顶部,并发周期启动后分配的对象会被放在TAMS之前(图里下边的部分),同时被明确定义为隐式存活对象,而TAMS之后(图里上边的部分)的对象则需要被明确地标记。

    初始标记过程中的一个堆分区

    并发标记过程中的堆分区

    并发标记过程中的对分区

    位于堆分区的Bottom和PTAMS之间的对象都会被标记并记录在previous位图中;

    位于Bottom和PTAMS之间的对象都会被标记在previous位图中

    位于堆分区的Top和PATMS之间的对象均为隐式存活对象,同时也记录在previous位图中;

    隐式存活标记,是一种增量标记

    在重新标记阶段的最后,所有NTAMS之前的对象都会被标记

    重新标记

    在并发标记阶段分配的对象会被分配到NTAMS之后的空间,它们会作为隐式存活对象被记录在next位图中。一次并发标记周期完成后,这个next位图会覆盖previous位图,然后将next位图清空。

    开始并发标记后的对象会被识别为隐式存活对象,放在next位图中

    SATB是一个快照标记算法,在并发标记进行的过程中,垃圾收集器(Collecotr)和应用程序(Mutator)都在活动,如果一个对象还没被mark到,这时候Mutator就修改了它的引用,那么这时候拿到的快照就是不完整的了,如何解决这个问题呢?

    G1 GC使用了SATB write barrier来解决这个问题——在并发标记过程中,将该对象的旧的引用记录在一个SATB日志对列或缓冲区中。去翻G1的代码,却发现实际代码如下——只该对象入队列,并没有将整个修改过程放在写屏障之间完成。

      // hotspot/src/share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.hpp
      // This notes that we don't need to access any BarrierSet data
      // structures, so this can be called from a static context.
      template <class T> static void write_ref_field_pre_static(T* field, oop newVal) {
        T heap_oop = oopDesc::load_heap_oop(field);
        if (!oopDesc::is_null(heap_oop)) {
          enqueue(oopDesc::decode_heap_oop(heap_oop));
        }
      }
    

    enqueue的真正代码在hotspot/src/share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.cpp中,这里使用JavaThread::satb_mark_queue_set().is_active()判断是否处于并发标记周期。

    void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
      // Nulls should have been already filtered.
      assert(pre_val->is_oop(true), "Error");
    
      if (!JavaThread::satb_mark_queue_set().is_active()) return;
      Thread* thr = Thread::current();
      if (thr->is_Java_thread()) {
        JavaThread* jt = (JavaThread*)thr;
        //将旧值入队
        jt->satb_mark_queue().enqueue(pre_val);
      } else {
        MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag);
        JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
      }
    }
    

    stab_mark_queue.enqueue方法首先尝试将以前的值记录在一个缓冲区中,如果这个缓冲区已经满了,就会将当期这个SATB缓冲区“退休”并放入全局列表中,然后再给线程分配一个新的SATB缓冲区。并发标记线程会定期检查和处理那些“被填满”的缓冲区。

    三、G1的过程

    1. 四个操作

    G1收集器的收集活动主要有四种操作:

    • 新生代垃圾收集

    • 后台收集、并发周期

    • 混合式垃圾收集

    • 必要时候的Full GC

    第一、新生代垃圾收集的图例如下:

    image.png

    • Eden区耗尽的时候就会触发新生代收集,新生代垃圾收集会对整个新生代进行回收

    • 新生代垃圾收集期间,整个应用STW

    • 新生代垃圾收集是由多线程并发执行的

    • 新生代收集结束后依然存活的对象,会被拷贝到一个新的Survivor分区,或者是老年代。

    G1设计了一个标记阈值,它描述的是总体Java堆大小的百分比,默认值是45,这个值可以通过命令-XX:InitiatingHeapOccupancyPercent(IHOP)来调整,一旦达到这个阈值就回触发一次并发收集周期。注意:这里的百分比是针对整个堆大小的百分比,而CMS中的CMSInitiatingOccupancyFraction命令选型是针对老年代的百分比。并发收集周期的图例如下:

    image.png

    在上图中有几个情况需要注意:

    1、新生代的空间占用情况发生了变化——在并发收集周期中,至少有一次(很可能是多次)新生代垃圾收集;

    2、注意到一些分区被标记为X,这些分区属于老年代,它们就是标记周期找出的包含最多垃圾的分区(注意:它们内部仍然保留着数据);

    3、老年代的空间占用在标记周期结束后变得更多,这是因为在标记周期期间,新生代的垃圾收集会晋升对象到老年代,而且标记周期中并不会是否老年代的任何对象。

    第二、G1的并发标记周期包括多个阶段:
    并发标记周期采用的算法是我们前文提到的SATB标记算法,产出是找出一些垃圾对象最多的老年代分区。

    1、初始标记(initial-mark),在这个阶段,应用会经历STW,通常初始标记阶段会跟一次新生代收集一起进行,换句话说——既然这两个阶段都需要暂停应用,G1 GC就重用了新生代收集来完成初始标记的工作。

     

    在新生代垃圾收集中进行初始标记的工作,会让停顿时间稍微长一点,并且会增加CPU的开销。初始标记做的工作是设置两个TAMS变量(NTAMS和PTAMS)的值,所有在TAMS之上的对象在这个并发周期内会被识别为隐式存活对象;

     

    2、根分区扫描(root-region-scan),这个过程不需要暂停应用,在初始标记或新生代收集中被拷贝到survivor分区的对象,都需要被看做是根,这个阶段G1开始扫描survivor分区,所有被survivor分区所引用的对象都会被扫描到并将被标记。

     

    survivor分区就是根分区,正因为这个,该阶段不能发生新生代收集,如果扫描根分区时,新生代的空间恰好用尽,新生代垃圾收集必须等待根分区扫描结束才能完成。如果在日志中发现根分区扫描和新生代收集的日志交替出现,就说明当前应用需要调优。

     

    3、并发标记阶段(concurrent-mark),并发标记阶段是多线程的,我们可以通过-XX:ConcGCThreads来设置并发线程数,默认情况下,G1垃圾收集器会将这个线程总数设置为并行垃圾线程数(-XX:ParallelGCThreads)的四分之一;并发标记会利用trace算法找到所有活着的对象,并记录在一个bitmap中,因为在TAMS之上的对象都被视为隐式存活,因此我们只需要遍历那些在TAMS之下的;

     

    记录在标记的时候发生的引用改变,SATB的思路是在开始的时候设置一个快照,然后假定这个快照不改变,根据这个快照去进行trace,这时候如果某个对象的引用发生变化,就需要通过pre-write barrier logs将该对象的旧的值记录在一个SATB缓冲区中,如果这个缓冲区满了,就把它加到一个全局的列表中——G1会有并发标记的线程定期去处理这个全局列表。

     

    4、重新标记阶段(remarking),重新标记阶段是最后一个标记阶段,需要暂停整个应用,G1垃圾收集器会处理掉剩下的SATB日志缓冲区和所有更新的引用,同时G1垃圾收集器还会找出所有未被标记的存活对象。这个阶段还会负责引用处理等工作。

     

    5、清理阶段(cleanup),清理阶段真正回收的内存很小,截止到这个阶段,G1垃圾收集器主要是标记处哪些老年代分区可以回收,将老年代按照它们的存活度(liveness)从小到大排列。

     

    这个过程还会做几个事情:识别出所有空闲的分区、RSet梳理、将不用的类从metaspace中卸载、回收巨型对象等等。识别出每个分区里存活的对象有个好处是在遇到一个完全空闲的分区时,它的RSet可以立即被清理,同时这个分区可以立刻被回收并释放到空闲队列中,而不需要再放入CSet等待混合收集阶段回收;梳理RSet有助于发现无用的引用。

    第三、混合收集只会回收一部分老年代分区,下图是第一次混合收集前后的堆情况对比。

    image.png

    混合收集会执行多次,一直运行到(几乎)所有标记点老年代分区都被回收,在这之后就会恢复到常规的新生代垃圾收集周期。当整个堆的使用率超过指定的百分比时,G1 GC会启动新一轮的并发标记周期。在混合收集周期中,对于要回收的分区,会将该分区中存活的数据拷贝到另一个分区,这也是为什么G1收集器最终出现碎片化的频率比CMS收集器小得多的原因——以这种方式回收对象,实际上伴随着针对当前分区的压缩。

    2. 两个模式

    G1收集器的模式主要有两种:

    • Young GC(新生代垃圾收集)

    • Mixed GC(混合垃圾收集)

    在R大的帖子中,给出了一个假象的G1垃圾收集运行过程,如下图所示,在结合上一小节的细节,就可以将G1 GC的正常过程理解清楚了。

    image.png

    3. 巨型对象的管理

    巨型对象:在G1中,如果一个对象的大小超过分区大小的一半,该对象就被定义为巨型对象(Humongous Object)。巨型对象时直接分配到老年代分区,如果一个对象的大小超过一个分区的大小,那么会直接在老年代分配两个连续的分区来存放该巨型对象。巨型分区一定是连续的,分配之后也不会被移动——没啥益处。

    由于巨型对象的存在,G1的堆中的分区就分成了三种类型:新生代分区、老年代分区和巨型分区,如下图所示:

    image.png

    如果一个巨型对象跨越两个分区,开始的那个分区被称为“开始巨型”,后面的分区被称为“连续巨型”,这样最后一个分区的一部分空间是被浪费掉的,如果有很多巨型对象都刚好比分区大小多一点,就会造成很多空间的浪费,从而导致堆的碎片化。如果你发现有很多由于巨型对象分配引起的连续的并发周期,并且堆已经碎片化(明明空间够,但是触发了FULL GC),可以考虑调整-XX:G1HeapRegionSize参数,减少或消除巨型对象的分配。

    关于巨型对象的回收:在JDK8u40之前,巨型对象的回收只能在并发收集周期的清除阶段或FULL GC过程中过程中被回收,在JDK8u40(包括这个版本)之后,一旦没有任何其他对象引用巨型对象,那么巨型对象也可以在年轻代收集中被回收。

    4. G1执行过程中的异常情况

    并发标记周期开始后的FULL GC

    G1启动了标记周期,但是在并发标记完成之前,就发生了Full GC,日志常常如下所示:

    51.408: [GC concurrent-mark-start]
    65.473: [Full GC 4095M->1395M(4096M), 6.1963770 secs]
     [Times: user=7.87 sys=0.00, real=6.20 secs]
    71.669: [GC concurrent-mark-abort]
    

    GC concurrent-mark-start开始之后就发生了FULL GC,这说明针对老年代分区的回收速度比较慢,或者说对象过快得从新生代晋升到老年代,或者说是有很多大对象直接在老年代分配。针对上述原因,我们可能需要做的调整有:调大整个堆的大小、更快得触发并发回收周期、让更多的回收线程参与到垃圾收集的动作中。

    混合收集模式中的FULL GC

    在GC日志中观察到,在一次混合收集之后跟着一条FULL GC,这意味着混合收集的速度太慢,在老年代释放出足够多的分区之前,应用程序就来请求比当前剩余可分配空间大的内存。针对这种情况我们可以做的调整:增加每次混合收集收集掉的老年代分区个数;增加并发标记的线程数;提高混合收集发生的频率。

    疏散失败(转移失败)

    在新生代垃圾收集快结束时,找不到可用的分区接收存活下来的对象,常见如下的日志:

    60.238: [GC pause (young) (to-space overflow), 0.41546900 secs]
    

    这意味着整个堆的碎片化已经非常严重了,我们可以从以下几个方面调整:(1)增加整个堆的大小——通过增加-XX:G1ReservePercent选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量;(2)通过减少 -XX:InitiatingHeapOccupancyPercent提前启动标记周期;(3)
    你也可以通过增加-XX:ConcGCThreads选项的值来增加并发标记线程的数目;

    巨型对象分配失败

    如果在GC日志中看到莫名其妙的FULL GC日志,又对应不到上述讲过的几种情况,那么就可以怀疑是巨型对象分配导致的,这里我们可以考虑使用jmap命令进行堆dump,然后通过MAT对堆转储文件进行分析。关于堆转储文件的分析技巧,后续会有专门的文章介绍。

    四、G1的调优

    G1的调优目标主要是在避免FULL GC和疏散失败的前提下,尽量实现较短的停顿时间和较高的吞吐量。关于G1 GC的调优,需要记住以下几点:

    1、不要自己显式设置新生代的大小(用Xmn-XX:NewRatio参数),如果显式设置新生代的大小,会导致目标时间这个参数失效。

     

    2、由于G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,即调整-XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单,这里有个取舍:如果减小这个参数的值,就意味着会调小新生代的大小,也会导致新生代GC发生得更频繁,同时,还会导致混合收集周期中回收的老年代分区减少,从而增加FULL GC的风险。这个时间设置得越短,应用的吞吐量也会受到影响。

     

    3、针对混合垃圾收集的调优。如果调整这期望的最大暂停时间这个参数还是无法解决问题,即在日志中仍然可以看到FULL GC的现象,那么就需要自己手动做一些调整,可以做的调整包括:

     

    1)调整G1垃圾收集的后台线程数,通过设置-XX:ConcGCThreads=n这个参数,可以增加后台标记线程的数量,帮G1赢得这场你追我赶的游戏;

     

    2)调整G1垃圾收集器并发周期的频率,如果让G1更早得启动垃圾收集,也可以帮助G1赢得这场比赛,那么可以通过设置-XX:InitiatingHeapOccupancyPercent这个参数来实现这个目标,如果将这个参数调小,G1就会更早得触发并发垃圾收集周期。

     

    这个值需要谨慎设置:如果这个参数设置得太高,会导致FULL GC出现得频繁;如果这个值设置得过小,又会导致G1频繁得进行并发收集,白白浪费CPU资源。通过GC日志可以通过一个点来判断GC是否正常——在一轮并发周期结束后,需要确保堆剩下的空间小于InitiatingHeapOccupancyPercent的值。

     

    3)调整G1垃圾收集器的混合收集的工作量,即在一次混合垃圾收集中尽量多处理一些分区,可以从另外一方面提高混合垃圾收集的频率。在一次混合收集中可以回收多少分区,取决于三个因素:

     

    (1)有多少个分区被认定为垃圾分区,-XX:G1MixedGCLiveThresholdPercent=n这个参数表示如果一个分区中的存活对象比例超过n,就不会被挑选为垃圾分区,因此可以通过这个参数控制每次混合收集的分区个数,这个参数的值越大,某个分区越容易被当做是垃圾分区;

     

    (2)G1在一个并发周期中,最多经历几次混合收集周期,这个可以通过-XX:G1MixedGCCountTarget=n设置,默认是8,如果减小这个值,可以增加每次混合收集收集的分区数,但是可能会导致停顿时间过长;

     

    (3)期望的GC停顿的最大值,由MaxGCPauseMillis参数确定,默认值是200ms,在混合收集周期内的停顿时间是向上规整的,如果实际运行时间比这个参数小,那么G1就能收集更多的分区。

     

    五、G1的最佳实践

    1. 关键参数项

    • -XX:+UseG1GC,告诉JVM使用G1垃圾收集器

    • -XX:MaxGCPauseMillis=200,设置GC暂停时间的目标最大值,这是个柔性的目标,JVM会尽力达到这个目标

    • -XX:INitiatingHeapOccupancyPercent=45,如果整个堆的使用率超过这个值,G1会触发一次并发周期。记住这里针对的是整个堆空间的比例,而不是某个分代的比例。

    2. 最佳实践

    不要设置年轻代的大小

    通过-Xmn显式设置年轻代的大小,会干扰G1收集器的默认行为:

    • G1不再以设定的暂停时间为目标,换句话说,如果设置了年轻代的大小,就无法实现自适应的调整来达到指定的暂停时间这个目标

    • G1不能按需扩大或缩小年轻代的大小

     

    响应时间度量

    不要根据平均响应时间(ART)来设置-XX:MaxGCPauseMillis=n这个参数,应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间,记住,这个值是一个目标,但是G1并不保证100%的GC暂停时间都可以达到这个目标

    3. G1 GC的参数选项

    参数名 含义 默认值
    -XX:+UseG1GC 使用G1收集器 JDK1.8中还需要显式指定
    -XX:MaxGCPauseMillis=n 设置一个期望的最大GC暂停时间,这是一个柔性的目标,JVM会尽力去达到这个目标 200
    -XX:InitiatingHeapOccupancyPercent=n 当整个堆的空间使用百分比超过这个值时,就会触发一次并发收集周期,记住是整个堆 45
    -XX:NewRatio=n 新生代和老年代的比例 2
    -XX:SurvivorRatio=n Eden空间和Survivor空间的比例 8
    -XX:MaxTenuringThreshold=n 对象在新生代中经历的最多的新生代收集,或者说最大的岁数 G1中是15
    -XX:ParallelGCThreads=n 设置垃圾收集器的并行阶段的垃圾收集线程数 不同的平台有不同的值
    -XX:ConcGCThreads=n 设置垃圾收集器并发执行GC的线程数 n一般是ParallelGCThreads的四分之一
    -XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间溢出(疏散失败)的风险。默认值是 10%。增加或减少这个值,请确保对总的 Java 堆调整相同的量 10
    -XX:G1HeapRegionSize=n 分区的大小 堆内存大小的1/2000,单位是MB,值是2的幂,范围是1MB到32MB之间
    -XX:G1HeapWastePercent=n 设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,JavaHotSpotVM不会启动混合垃圾回收周期(注意,这个参数可以用于调整混合收集的频率)。 JDK1.8是5
    -XX:G1MixedGCCountTarget=8 设置并发周期后需要执行多少次混合收集,如果混合收集中STW的时间过长,可以考虑增大这个参数。(注意:这个可以用来调整每次混合收集中回收掉老年代分区的多少,即调节混合收集的停顿时间) 8
    -XX:G1MixedGCLiveThresholdPercent=n 一个分区是否会被放入mix GC的CSet的阈值。对于一个分区来说,它的存活对象率如果超过这个比例,则改分区不会被列入mixed gc的CSet中 JDK1.6和1.7是65,JDK1.8是85

    常见问题

    1、Young GC、Mixed GC和Full GC的区别?
    答:Young GC的CSet中只包括年轻代的分区,Mixed GC的CSet中除了包括年轻代分区,还包括老年代分区;Full GC会暂停整个引用,同时对新生代和老年代进行收集和压缩。

    2、ParallelGCThreads和ConcGCThreads的区别?
    答:ParallelGCThreads指得是在STW阶段,并行执行垃圾收集动作的线程数,ParallelGCThreads的值一般等于逻辑CPU核数,如果CPU核数大于8,则设置为5/8 * cpus,在SPARC等大型机上这个系数是5/16。;ConcGCThreads指的是在并发标记阶段,并发执行标记的线程数,一般设置为ParallelGCThreads的四分之一。

    3、write barrier在GC中的作用?如何理解G1 GC中write barrier的作用?
    写屏障是一种内存管理机制,用在这样的场景——当代码尝试修改一个对象的引用时,在前面放上写屏障就意味着将这个对象放在了写屏障后面。

    write barrier在GC中的作用有点复杂,我们这里以trace GC算法为例讲下:trace GC有些算法是并发的,例如CMS和G1,即用户线程和垃圾收集线程可以同时运行,即mutator一边跑,collector一边收集。这里有一个限制是:黑色的对象不应该指向任何白色的对象。如果mutator视图让一个黑色的对象指向一个白色的对象,这个限制就会被打破,然后GC就会失败。

    针对这个问题有两种解决思路:

    (1)通过添加read barriers阻止mutator看到白色的对象;

    (2)通过write barrier阻止mutator修改一个黑色的对象,让它指向一个白色的对象。write barrier的解决方法就是讲黑色的对象放到写write barrier后面。如果真得发生了white-on-black这种写需求,一般也有多种修正方法:增量得将白色的对象变灰,将黑色的对象重新置灰等等。

    我理解,增量的变灰就是CMS和G1里并发标记的过程,将黑色的对象重新变灰就是利用卡表或SATB的缓冲区将黑色的对象重新置灰的过程,当然会在重新标记中将所有灰色的对象处理掉。关于G1中write barrier的作用,可以参考R大的这个帖子里提到的:

    image.png

    4、G1里在并发标记的时候,如果有对象的引用修改,要将旧的值写到一个缓冲区中,这个动作前后会有一个write barrier,这段可否细说下?

    答:这块涉及到SATB标记算法的原理,SATB是指start at the beginning,即在并发收集周期的第一个阶段(初始标记)是STW的,会给所有的分区做个快照,后面的扫描都是按照这个快照进行;在并发标记周期的第二个阶段,并发标记,这是收集线程和应用线程同时进行的,这时候应用线程就可能修改了某些引用的值,导致上面那个快照不是完整的,因此G1就想了个办法,我把在这个期间对对象引用的修改都记录动作都记录下来,有点像mysql的操作日志。

    5、GC算法中的三色标记算法怎么理解?
    trace GC将对象分为三类:白色(垃圾收集器未探测到的对象)、灰色(活着的对象,但是依然没有被垃圾收集器扫描过)、黑色(活着的对象,并且已经被垃圾收集器扫描过)。垃圾收集器的工作过程,就是通过灰色对象的指针扫描它指向的白色对象,如果找到一个白色对象,就将它设置为灰色,如果某个灰色对象的可达对象已经全部找完,就将它设置为黑色对象。当在当前集合中找不到灰色的对象时,就说明该集合的回收动作完成,然后所有白色的对象的都会被回收。

    PS:这个问题来自参考资料17,我将原文也贴在下面:

    For a tracing collector (marking or copying), one conceptually colours the data white (not yet seen by the collector), black (alive and scanned by the collector) and grey (alive but not yet scanned by the collector). The collector proceeds by scanning grey objects for pointers to white objects. The white objects found are turned grey, and the grey objects scanned are turned black. When there are no more grey objects, the collection is complete and all the white objects can be recycled.

    参考资料

    1. Understanding G1 GC Logs

    2. Garbage First Garbage Collector Tuning

    3. 垃圾优先型回收器调优

    4. Oracle的GC调优文档——G1

    5. The Garbage-First Garbage Collector

    6. 《Java性能权威指南》

    7. 《Java性能调优指南》

    8. G1入门,O记官网的PPT

    9. Java Hotspot G1 GC的一些关键技术

    10. G1 GC的论文

    11. R大关于G1 GC的帖子

    12. Tips for Tuning the Garbage First Garbage Collector

    13. Java性能调优指南

    14. Java性能权威指南

    15. G1: What are the differences between mixed gc and full gc?

    16. Part 1: Introduction to the G1 Garbage Collector

    17. Collecting and reading G1 garbage collector logs - part 2

    18. GC FAQ -- algorithms

     

    -END-

     

    展开全文
  • JVM学习 1、类加载器 类加载器子系统:从文件系统或网络中加载class文件,class文件在文件开头有特定的文件标识 CAFEBABE 类加载器加载的类信息,会放在方法区的内存空间。 1.1、类加载的过程 加载阶段: 1、通过...
  • 文章目录JVM面试常见1、JVM的位置2、JVM体系结构3、类加载器4、双亲委派机制双亲委派机制 : 安全双亲委派机制 : 作用5、沙箱安全机制组成沙箱的基本组件6、NativeNative Method StackNative Interface本地接口7、PC...
  • 最全面的JVM G1学习笔记

    千次阅读 2018-12-18 20:59:42
    引子 最近遇到很多朋友过来咨询G1调优的问题,我自己去年有专门学过一次G1,但是当时只是看了个皮毛,因此自己也有不少问题。总体来讲,对于G1我有几个疑惑,希望能够在这篇文章中得到解决。 ...
  • 什么是JVM

    万次阅读 多人点赞 2018-04-20 16:05:05
    说明:做java开发的几乎都知道jvm这个名词,但是由于jvm对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的及专门做性能优化的什么的),很少有人能很好的去学习及理解什么是jvm,以及弄...
  • jvm到底是什么?有什么作用?工作机制如何?

    万次阅读 多人点赞 2018-05-04 11:24:23
    1、jvm定义 JVM就是java虚拟机,它是一个虚构出来的计算机,可在实际的计算机上模拟各种计算机的功能。JVM有自己完善的硬件结构,例如处理器、堆栈和寄存器等,还具有相应的指令系统。 2、jvm作用 JVM是java字节码...
  • JVM内存模型总结

    万次阅读 2018-05-21 22:59:36
    JVM内存模型: 从这张图中很直观的看到,程序计数器,虚拟机栈,native栈是线程私有的,堆是线程共有的,现在详细介绍JVM各个区块。 1. 堆(Heap) 是java虚拟机所管理的内存中最大的一块内存区域,也是被各个...
  • 深入浅出JVM调优,看完你就懂

    万次阅读 多人点赞 2019-08-12 13:50:52
    深入浅出JVM调优 基本概念: JVM把内存区分为堆区(heap)、栈区(stack)和方法区(method)。由于本文主要讲解JVM调优,因此我们可以简单的理解为,JVM中的堆区中存放的是实际的对象,是需要被GC的。其他的都无需GC。 ...
  • JVM调优

    万次阅读 2019-04-03 12:17:05
    一、JDK,JRE,JVM区别与联系 二、JVM虚拟机详解 程序计数器(线程专属) 虚拟机栈(FILO)(线程专属)->栈->数据结构->存储数据 本地方法栈(线程专属) 方法区(线程共享)(永久代) java堆(heap...
  • JVM调优经验分享

    万次阅读 多人点赞 2016-03-30 12:06:47
    JVM调优经验分享 前言 一、JVM调优知识背景简介 二、JVM调优参数简介 三、JVM调优目标 四、JVM调优经验 结束语
  • jvm调优思路2. 订单的秒杀模块jvm调优案例 1. jvm调优思路         jvm调优其实更多的是对GC的优化,尤其是尽量减少full GC。       &...
  • jvm调优案例

    千次阅读 2019-09-05 11:34:15
    JVM调优 JVM 收集器 默认使用串行收集器, 单个cpu时适用吞吐收集器(throughput collector):命令行参数:-XX:+UseParallelGC。在新生代使用并行清除收集策略,在旧生代和默认收集器相同。 适用:a、拥有2个以上...
  • jvm系列(七):jvm调优-工具篇

    万次阅读 2017-02-24 16:46:42
    16年的时候花了一些时间整理了一些关于jvm的介绍文章,到现在回顾起来还是一些还没有补充全面,其中就包括如何利用工具来监控调优前后的性能变化。工具做为图形化界面来展示更能直观的发现问题,另一方面一些耗费性能...
  • 一、JVM调优参数简介1、 JVM参数简介-XX 参数被称为不稳定参数,之所以这么叫是因为此类参数的设置很容易引起JVM 性能上的差异,使JVM 存在极大的不稳定性。如果此类参数设置合理将大大提高JVM 的性能及稳定性。不...

空空如也

1 2 3 4 5 ... 20
收藏数 765,795
精华内容 306,318
关键字:

jvm