Java虚拟机
Oracle有两款实现了 Java SE
的产品:Java SE Development Kit(JDK)
和 Java SE Runtime Environment(JRE)
。
JDK
是 JRE
的超集,包含了 JRE
的所有内容,以及开发应用程序所需的编译器和调试器等工具。 JRE
提供了函数库、Java Virtual Machine(JVM)
和其它用来运行Java应用程序的组件。
Java内存区域
Java虚拟机运行时数据区程序计数器
程序计数器是一块较小的内存空间,他的作用可以看做是当前线程所执行的字节码的行号指示器。由于 JVM
的多线程是通过线程轮流切换并分配处理器执行时间来实现的,因此每个线程都有一个独立的程序计数器,用于线程切换后能恢复到正确的执行位置。如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native方法
,这个计数器的值为 undefined
。此区域是唯一一个在Java虚拟机规范中没有规定 OutOfMemoryError
情况的区域。
Java虚拟机栈
- 虚拟机栈描述的是Java方法执行的动态内存模型,调用方法即创建栈帧并入栈,方法执行完毕栈帧出栈。
- 栈帧:每个方法执行,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等。每一个方法从调用到执行完成的过程,就是一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表:存放编译器可知的各种基本数据类型,引用类型,returnAddress类型。对象是在堆内存中创建的,局部变量表存放的是对象的引用,其大小是不会改变的。因此局部变量表的内存空间在编译期完成分配后,方法需要在帧分配多少内存是固定的。
- 虚拟机栈异常:如果线程请求的栈深度超过虚拟机允许的深度,将抛出
StackOverFlowError
异常;如果虚拟机栈允许扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError
异常。
本地方法栈
Java虚拟机栈为虚拟机执行Java方法服务,本地方法栈则为虚拟机使用到的 Native方法
服务。在 Hotspot虚拟机
的实现中是把本地方法栈和虚拟机栈合二为一的。与虚拟机栈一样,本地方法栈也会抛出 StackOverFlowError
异常和 OutOfMemoryError
异常。
Java堆
堆是线程共享的数据运行时区域,几乎所有的对象实例以及数组都要在堆上分配内存。堆是垃圾收集器管理的主要区域。Java堆可以处于物理上不连续的内存空间中。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError
异常。
方法区
方法区存储虚拟机加载的类信息(类的版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码等数据。于 Hotspot虚拟机
来说,将方法区纳入GC管理范围,这样就不必单独管理方法区的内存,所以就有了相对于新生代和老年代的永久代一说。
运行时常量池
运行时常量池(JDK6在方法区,JDK7在Java堆)用来存放编译器生成的各种字面量以及符号引用(类加载之后进入运行时常量池)。运行期间也能将新的常量放入池中。当常量池无法再申请到内存时,将会抛出 OutOfMemoryError
异常。
直接内存
Java NIO
使用 Native函数库
直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer
对象作为这块内存的引用进行操作。通过避免在 Java堆
和 Native堆
中来回复制数据来提高性能。直接内存大小不受虚拟机参数控制,如果各个内存区域总和大于物理内存限制,就会出现 OutOfMemoryError
异常。
对象
对象的创建
对象的创建过程内存分配策略
根据 Java 堆
是否规整可以判断使用哪种内存分配策略。
- 指针碰撞:堆内存中的空闲空间十分的规整,使用与未使用的空间全部为连续,分配内存只需移动指针。
- 空闲列表:针对堆内存中的空间零散的存在,虚拟机维护着一个列表,记录那些内存未使用。
线程安全性
对象创建在虚拟机中是十分频繁的行为,在并发环境下需要考虑线程安全。
-
CAS失败重试
:通过乐观锁实现线程安全。 -
TLAB(Thread Local Allocation Buffer)
:本地线程分配缓冲,内存为每个线程分配一个TLAB区域
,每个线程要创建对象时先在这个区域中创建,当原来的空间不足时再通过线程同步获取一块新的区域。
对象设置
将对象的哈希吗、GC年龄信息等存放在对象头中,执行 <init>
方法。
对象的结构
对象头(Header)
- 自身运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 类型指针:对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,对象头中还必须有一块记录数组长度的数据。
实例数据(Instance Data)
实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。
对齐填充(Padding)
HotSpot虚拟机
要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。
对象的访问定位
Java程序通过栈上的 reference
数据来操作堆上的具体对象,由于 reference
类型在Java虚拟机规范中只规定了一个指向对象的引用,具体用何种方式去定位、引用堆中对象的具体位置,取决于虚拟机的实现,目前主要有 使用句柄
和 直接指针
两种方式。
- 使用句柄:引用类型指向堆中一块区域(句柄池),此区域保存了实例对象的地址(对象被移动时维护句柄中的指针数据,无需改变
reference
本身)。 - 直接指针:从引用类型直接指向内存区域(速度更快,节省了一次指针定位带来的开销)。
垃圾回收
在Java堆上分配一个内存给实例对象时,此时在虚拟机栈上引用型变量就会存放这个实例对象的起始地址。当线程销毁后,其在虚拟机栈上的内存自然会被回收,也就是说虚拟机栈上的这块内存不在虚拟机GC范围内。
垃圾对象判定算法
- 引用计数法:在对象中添加一个引用计数器,当有地方引用这个对象时,引用计数器的值+1,当引用失效时,计数器的值-1。垃圾回收器遇到计数器为0的对象时就会回收。但是当堆内存中的对象相互引用,而外部不存在对这些对象的引用时,计数器值不为0,无法判定回收。
- 可达性分析法:通过一系列名为
GC Roots
的对象(虚拟机栈、方法区类属性所引用的对象、方法区中常量所引用的对象、本地方法栈中引用的对象)作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)
,当一个对象到GC Roots没有任何引用链相连时,则此对象证明是不可用的,将被判定为可回收对象。
Java引用
- 强引用:类似
Obejct obj = new Object()
,只要强引用还在,垃圾收集器就永远不会收集被引用的对象。 - 软引用:还有用但并非必须的对象。在系统发生内存溢出之前,会将软引用关联的对象进行回收,如果回收后还没有足够的内存,才会抛出内存异常。
- 弱引用:垃圾收集器工作时,无论当前内存是否足够,都会回收被弱引用关联的对象。
- 虚引用:一个对象是否有虚引用的存在,完全不会对其生产时间构成影响。也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被垃圾收集时收到一个系统通知。
finalize()
将对象回收至少要经历两次标记过程,如果在可达性分析中发现对象没有与 GC Roots
的引用链,那它将会被第一次标记并被进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()
方法(当前对象没有覆盖此方法或者已经执行过此方法,则虚拟机认为“没有必要执行”),虚拟机不会承诺等待此方法执行结束。如果在 finalize()
方法中成功与引用链上的人一个对象建立关联,则对象不会被回收。
如何回收
回收策略
- 标记-清除算法:标记可达对象,在清除阶段回收并没有被标记为可达的对象所占用的内存空间,并将原来的可达标记删除。但是效率不高且会造成内存碎片化。
- 复制算法:标记待回收内存和不需回收的内存,将不需回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域是连续的。其缺点是需要损失部分系统内存用于复制,但是可以避免产生内存碎片。实例创建时通常发生在
Eden空间
,发生Minor GC
后,会将Eden和其中一个Survivor空间
不需回收的对象内存复制到另一个Survivor空间
中(HotSpot
虚拟机中Eden空间
和Survivor空间
的比例为8:1),如果反复几次复制有对象一直存活,则会将相应的对象内存移至老年代。
- 标记-整理算法:是老年代中的垃圾回收算法,标记过后,将不用回收的对象内存压缩到空间的一端,再对另一端的内存空间进行垃圾回收。这样既避免了复制算法带来的效率问题,也避免了内存碎片化的问题。
垃圾收集器
垃圾收集器是垃圾回收算法的具体实现。
Serial垃圾收集器
Serial垃圾收集器
是最基本、发展历史最悠久的收集器。
- 采用复制算法,针对新生代
- 单线程垃圾回收,执行时必须暂停所有工作线程,直到完成
ParNew垃圾收集器
ParNew垃圾收集器
是Serial收集器的多线程版本。
Parallel Scavenge垃圾收集器
Parallel Scavenge收集器
的目标是达到一个可控制的吞吐量(CPU用于运行用户代码的时间与CPU消耗的总时间的比值),即减少垃圾收集时间,让用户代码获得更长的运行时间。
- 采用复制算法,针对新生代
- 多线程垃圾回收
Parallel Scavenge收集器
提供两个参数用于精确控制吞吐量: - -XX:MaxGCPauseMillis:最大垃圾收集停顿时间(ms),设置稍小会缩短停顿时间,但也可能会造成频繁发生垃圾回收,使得吞吐量下降。
- -XX:GCTimeRatio:垃圾收集时间占总时间的比率,0 < n < 100的整数
CMS(Concurrent Mark Sweep)垃圾收集器
CMS收集器是一款真正意义上的并发收集器,实现了让垃圾收集线程与用户线程(基本上)同时工作。适用于与用户交互较多的场景。
- 针对老年代
- 基于标记-清除算法
- 以获取最短回收停顿时间为目标
G1垃圾收集器
- 充分利用多CPU、多核环境下的硬件优势,可以使垃圾收集和用户程序同时进行。
- 分代收集,能够独立管理整个GC堆,采用不同方式处理不同时期的对象。整个堆被划分为多个大小相等的
不连续独立区域(Region)
,新生代和老年代不再是物理隔离,它们都是一部分Region的集合。 - 从整体看基于标记-整理算法,从局部看(两个Region间)是基于复制算法,不会产生内存碎片,有利于长时间运行。
- 可预测的停顿,可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。
- 适用于服务端,针对大内存,多处理器的机器。
内存分配
HotSpot一般的年代内存划分对象的内存分配主要在堆上分配(JIT编译优化后可能在栈上分配),在新生代的Eden空间中分配,如果启用了本地线程分配缓冲,则线程优先在TLAB上分配。少数情况下对象会直接分配在老年代中。
内存分配策略
- 优先分配到Eden空间:大多数情况下,对象在新生代
Eden区域
中分配。当Eden区域
没有足够空间时将会发起一次Minor GC
。 - 大对象直接分配到老年代:所谓大对象是指需要大量连续空间的Java对象,直接在老年代分配空间就避免了大对象在新生代各个区域间反复复制。
- 长期存活的对象分配到老年代:虚拟机给每个对象定义了一个对象年龄计数器,如果对象在
Eden区域
出生且经过一次Minor GC
后仍然存活,并且能被Survivor空间
容纳,对象年龄就会被设为1,此后对象每熬过一次Minor GC
,其年龄就会增加1,当年龄增加到一定程度(默认为15岁),就会晋升到老年代中。 - 动态对象年龄判断:
Survivor空间
中相同年龄的所有对象大小的总和大于Survivor空间
的一半,则年龄大于等于该年龄的对象可以直接进入老年代。 - 空间分配担保:有可能在
Minor GC
执行后仍有大量对象在新生代存活,就需要老年代进行分配担保,将Survivor空间
无法容纳的对象晋升到老年代,老年代要进行这样的担保,前提是老年代中有容纳这些对象的足够空间。因此在进行Minor GC
前,虚拟机会检查老年代最大连续可用内存是否大于新生代所有对象总空间,如果条件成立,则Minor GC
可以确保是安全的,如果不成立虚拟机则查看HandlePromotionFailure
参数判断是否允许担保失败。如果允许,则会继续检查老年代的最大连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试一次Minor GC
,如果小于或HandlePromotionFailure
参数不允许则改为进行一次Full GC
,发生担保失败也会重新进行一次Full GC
。空间分配担保可以避免Full GC
过于频繁。
网友评论