一.内存模型
JVM的内存模型如下图所示,包括线程共享的(方法区、堆)、线程安全的(虚拟机栈、本地方法栈、程序计数器)
1)程序计数器
程序计数器(PC寄存器)可以看作是当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个JAVA方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是NATIVE方法,这个计数器值则为空UNDEFINE。 2)java虚拟机栈 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。它的生命周期与线程 相同。 局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。 3)本地方法栈 类似虚拟机栈,虚拟机栈为执行Java方法服务,而本地方法栈则为用到的Native方法服务。在HotSpot虚拟机中直接把二者合并。 4)java堆 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。 从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。 从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。TLAB实际上属于Eden的一个特殊部分 5)方法区 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。运行时常量池(Runtime Constant Pool)是方法区的一部分。 在java8中,永久代(方法区的实现) : PermGen----->替换为Metaspace(元空间 本地内存中)。常量池也移到了堆上,元空间与永久代之间最大的区别在于:方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”;元空间并不在虚拟机中,而是使用本地内存。 6)直接内存 在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。 显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。二.对象的内存使用
1)对象内存分配
①对象所需内存的大小在类加载完成后便可完全确定,分配内存常见的两种方式是指针碰撞和空闲列表。 假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。 如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。 在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。 ②内存分配的线程安全 解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定,默认是开启的。2)对象的内存布局
对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。第二部分是类型指针,由于hotspot使用的是直接指针,所以需要记录指向类型数据的地址。 对齐填充,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍,所以需要将对象的大小填充为8的整数倍。 3)对象访问定位 目前主流的访问方式有使用句柄和直接指针两种。 如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。 HotSpot使用的是直接指针。三.垃圾回收机制
1.对象存活判定算法
1)引用计数法 主要缺点在于不能解决对象的相互循环引用 2)可达性分析算法 当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。2.垃圾收集算法
1)标记-清除算法 Mark-Sweep 算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。 它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片。 2)复制算法 Copying 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 上述分配极大的浪费了内存,由于98%的对象都是朝生夕死,为了解决这个问题。是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保(Handle Promotion)机制进入老年代。 3)标记-整理算法 Mark-Compact 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。老年代中大部分对象都是存活的,不适用于复制收集算法。 标记-整理是指在标记之后,不清理,而是让所有存活对象向一端移动,避免内存碎片。 4)分代收集算法 Generational Collecti3.hotspot实现
hotspot使用一组称为OopMap的数据结构记录所有引用信息,从而避免扫描巨大的内存空间。但引用信息的变换十分频繁,不可能时刻记述,hotspot引入了安全点的概念,只有在安全点才会记录OopMap信息,即只有程序运行到安全点或安全区域才可以GC。安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。 在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。几乎所有虚拟机都使用的是主动式中断。四.内存分配与回收
1.大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
2.大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。 3.长期存活的对象将进入老年代。 4.空间担保 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。 5.其他概念 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。 6.分配顺序 ①编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2. ②尝试放在TLAB上,如果现有的TLAB不足以存放当前对象则3. ③重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4. ④在Eden区加锁(这个区是多线程共享的),尝试分配到Eden区,如果Eden区不足以存放,则5. ⑤执行一次Young GC(minor collection)。 ⑥经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。五.垃圾收集器
1.Serial收集器
它在新生代使用复制算法,在老年代使用标记-整理算法。 它是虚拟机运行在Client模式下的默认新生代收集器。它只会使用一个CPU或一个线程去进行垃圾收集,没有线程切换开销,在桌面应用场景和单线程模式下具有较大优势。 2.ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。 3.Parallel Scavenge收集器 Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器。它侧重于吞吐量优化,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),适合在后台运算,不需要太多交互的任务。 4.Serial Old收集器 Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。 5.Parallel Old收集器 Parallel Scavenge收集器的老年代版本。 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。 6.CMS收集器 Concurrent Mark Sweep CMS注重于响应时间,适合互联网站应用。CMS收集器是基于“标记—清除”算法实现的。 CMS是一款基于“标记—清除”算法实现的收集器,会产生大量的空间碎片。 CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 CMS回收线程本身也会占用资源,默认CMS回收线程数是(CPU数+3)/4,cpu较少时,性能可能反而不高。 7.G1收集器 Garbage-First G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。 G1除了追求低停顿外,还能建立可预测的停顿时间模型。 使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。六.字节码执行引擎
1.运行时栈帧结构
栈帧是虚拟机栈中的基本元素。栈帧存储了方法的局部变量表、操作数栈、动态连接、方法返回地址、异常处理表等信息。虚拟机执行时,当前执行的方法即位于栈顶的栈帧,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。 2.局部变量表 局部变量表以变量槽为最小单位,对于实例方法(非static)第0位表示的是对象实例的引用this,然后是方法参数,之后是方法体内定义的局部变量。 要注意的是,如果一个对象已不再使用,但是在局部变量表里仍然保存有对他的引用,则这个对象不能被GC。因此部分代码中,在大对象使用完成后,会加入XXX=null;的方式加快回收。 但赋null值是不可靠的,因为JIT优化后,可能会去除这一代码。 3.字节码解释执行 Java编译器输出的指令流,基本上是一种基于栈的指令集架构(Instruction SetArchitecture,ISA),指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。与之相对的另外一套常用的指令集架构是基于寄存器的指令集,最典型的就是x86的二地址指令集。 4.虚拟机加载机制 类在虚拟机中的生命周期包括:加载 链接 初始化 使用 卸载五个步骤 链接又分为 验证 准备 解析三步。 5.Class类文件结构 ①Class文件格式采用一种类似C语言结构体的伪数据结构,这种伪数据结构只有两种数据类型:无符号数和表。表是由多个无符号数或其他表作为数据项的复合数据类型,所有表都习惯以_info结尾。 ②整个Class文件本质上就是一张表。 ③Class文件格式,无论是顺序还是数量,都是被严格限定的,必须是下图顺序格式。 每个Class文件的头4个字节成为魔数(Magic Number),值为0xCAFEBABE。 紧邻魔数的4个字节是版本号,第5,6个字节是次版本号(Minor Version),7,8个字节是主版本号(Major Version)。 在版本号之后依次是 常量池入口 访问标志 access_flags 类索引 this_class 父类索引 super_class 索引表 字段表 方法表 属性表七.JVM多线程并发
1.并发的内存模型
Java内存模型规定了所有的“全局”变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存。虚拟机保证下列操作是原子的: lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。 unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。 load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。 use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。 assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。 write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。2.volatile实质
volatile变量在每次使用之前都要先刷新,从主内存读取最新值;但对读取后的变化无能为力,无法保证安全性。 volatile的意义: ①保证变量可见性。 ②禁止指令重排序优化。volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
volatile的应用场景
①状态标记量 变量的可见性使volatile适合修饰状态Flag ②防止指令重排序 单例模式中的double checkclass Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}instance = new Singleton()这句赋值语句、由于指令重排序的原因2、3的执行循序是不能保证的,可以通过禁止指令重排序防止先3在2,出现执行错误。1.给 instance 分配内存2.调用 Singleton 的构造函数来初始化成员变量3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。
3.线程调度
主要有两种,分别是协同式线程调度和抢占式线程调度。java采用的是抢占式线程调度。 协同式调度,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。抢占式调度,每个线程将由系统来分配执行时间,不由线程本身来决定。4.线程安全
1)不可变 不可变 的是安全的 如final定义的基本数据类型。String.subString()会产生新字符串 2)绝对线程安全 真正的线程安全 3)相对线程安全 如Vector的add和get,本身线程安全,但调用代码上不一定安全。 4)线程兼容 如HashMap可以加Synchronized来限制 5)线程对立 如Thread类的suspend()和resume()方法5.加锁策略
1)互斥同步 悲观并发策略 syntronized 非阻塞同步 乐观并发策略 2)乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施。可以通过CAS Compare-and-Swap 硬件指令集支持。6.锁优化
1)java使用的一些优化技术:适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。 2)自旋锁 共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。默认的自旋次数为10次。 2)锁粗化 虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。 3)锁粗化 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。增加锁定范围反而会取得性能提升。 4)轻量级锁 在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。 如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向着线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。 轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”。 5)偏向锁 偏向锁的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。八 JVM常用参数
- -Xms 初始堆大小 默认物理内存的1/64(<1GB) 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
- -Xmx 最大堆大小 默认物理内存的1/4(<1GB) 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
- -XX:NewSize 设置年轻代大小(for 1.3/1.4)
- -XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
- -XX:PermSize 设置持久代(perm gen)初始值 默认物理内存的1/64
- -XX:MaxPermSize 设置持久代最大值 默认物理内存的1/4
- -Xss 每个线程的堆栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程. 但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:" -Xss is translated in a VM flag named ThreadStackSize” 一般设置这个值就可以了。
- -XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) 默认1:2
- -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5.Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
- -XX:SurvivorRatio Eden区与Survivor区的大小比值 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
- -XX:TLABWasteTargetPercent TLAB占eden区的百分比 1%
- -XX:MetaspaceSize、-XX:MaxMetaspaceSize
- -XX:+DisableExplicitGC,这个参数作用是禁止代码中显示调用GC。
- -XX:+UseConcMarkSweepGC 使用cms收集器
- -XX:+CMSParallelRemarkEnabled 减少Remark阶段暂停的时间,启用并行Remark,如果Remark阶段暂停时间长,可以启用这个参数
- -XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
- -XX:LargePageSizeInBytes=128m 设置堆的内存页大小。
- -XX:+UseFastAccessorMethods 优化原始类型的getter方法性能(get/set:Primitive Type)
- -XX:CMSInitiatingOccupancyFraction=70 指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC);
- -XX:+UseCMSInitiatingOccupancyOnly 开启用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整. 只要在JVM中开启了JMX服务,JMX将会1小时执行一次Full GC以清除引用.