内存划分
java虚拟机会把它所管理的内存划分为若干个不同的数据区域
程序计数器
当前线程所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令
虚拟机栈
java方法执行的内存模型,它的生命周期与线程相同。方法执行时创建一个栈帧用于存储局部变量表(存放了各种基本数据类型boolean、byte、char、short、int、float、long、double,对象引用)、操作栈、动态链接、方法出口等信息。
本地方法栈
虚拟机使用到的nativie方法服务
Java堆
存放对象的实例,几乎所有对象都在这里分配内存。栈上分配、标量替换会导致其他情况
方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
运行时常量池,Class文件中除了类的版本、字段、方法、接口等之外还有常量池。用于存放编译期生成的各种字面量和符号引用
1.8之后叫元空间
运行时常量池
用于存放编译期生成的各种字面量和符号引用,String类的intern()
虚拟机对象
对象的创建
1.类加载检查
虚拟机遇到一条new指令,会首先进行类加载的检查:
--检查这个指令的参数是否在常量池中定位到一个符号引用
--检查该类是否被加载、解析和初始化,如果没有就进行类加载。
2.分配内存
对象所需内存的大小在类加载完成后便可完全确定,于是分配空间等同于把一块确定大小的内存从堆中划分出来,具体分配方式取决于堆内存是否规整。是否规整是垃圾收集器是否带有压缩整理功能决定的。
-- 指针碰撞
堆内存规整,移动指针
-- 空闲列表
堆内存不规整,虚拟机维护记录可用内存的列表,从列表中查找对应对象大小的内存区域分配给对象,并更新空闲列表。
为保证内存分配时并发情况下线程安全问题,有两种方案:
-- CAS+失败重试
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理
-- TLAB
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存
3.初始化
内存分配完成后,虚拟机需要将除对象头以外分配到的内存空间都初始化为0,如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,这些设置信息存放在对象的对象头Object Header之中。
对象头包括:markword、类型指针、数组长度
5.执行init方法
为对象属性复制,执行对象的构造方法
对象的内存布局
对象访问(对象定位)
1.使用句柄
句柄中包含了对象实例数据和类型数据各自的具体地址信息
2.直接指针
分配对象
Java 中主要使用 Unsafe 调用了 C 的 allocate 和 free 两个方法,分配方法有两种:
1.指针碰撞: 始终跟踪上一次分配对象时使用的空间末尾地址。当要为新对象分配空间时,会在当前所处的代的各个内存块中查找是否有适合大小的空间来分配给新对象,若有,则更新指针,初始化对象
2.空闲列表: 维护一个列表,记录上哪些内存可用。
垃圾对象
1.引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能在被使用
缺点就是相互引用
2.根搜索算法
通过“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路劲成为引用链,当一个对象到Gc Roots 没有任何引用链相连,则证明此对象是不可用的。
Gc Roots对象包括:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI的引用的对象
什么是引用?
强引用
只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
软引用
在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收
弱引用
只能生存到下一次来及收集发生之前,WeakReference类来实现弱引用
虚引用
两次标记过程才会真正死亡。
第一次标记:
判断是否存在引用链。然后筛选(判断是否有必要执行finalize方法),并放置F-Queue的队列中
第二次标记
GC将对F-Queue中的对象进行第二次小规模的标记
垃圾收集算法
1.标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象
缺点: 1.效率问题。2.空间问题
2.复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了。就将存货着的对象复制到另外一块上。然后将已使用过的内存空间一次清理掉。
3.标记-整理算法
让所有存货的对象都向一端移动,然后直接清理掉端边界以外的内存
3.分代收集算法
根据对象的存货周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代。这样可以根据各个年代的特点采用最适当的收集算法。
垃圾收集器
如果两个收集器之间存在连线,说明可以搭配使用。
Serial收集器
jdk1.3.1之前新生代,单线程的收集器。收集垃圾时,必须暂停其他所有的工作线程
优点:简单而高效,适合client端
ParNew收集器
Serial收集器的多线程版本
Parallel Scavenge收集器
新生代收集器,使用复制算法并行的多线程收集器。目标是达到一个可控制的吞吐量
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验﹔而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Serial Old收集器
老年代版本的单线程收集器。使用“标记-整理”算法
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS收集器
是一种以获取最短回收停顿时间为目标的收集器。“标记-清除”
优点:并发收集、低停顿
缺陷:对cpu资源非常敏感、无法处理浮动垃圾、产生大量空间碎片
G1收集器(精准控制停顿时间,避免垃圾碎片)
减少“Stop the world”G1垃圾收集器能同时回收年轻代和老年代的对象,它最大的一个特点是把java堆内存拆分为多个大小相等的region,使得每个小空间可以单独进行垃圾回收。在指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象
维护了一个优先列表,根据允许的收集时间,优先回收最大的region
G1 收集器两个最突出的改进是:
【1】基于标记-整理算法,不产生内存碎片。
【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。(增量回收,不是全量回收)
存放类的信息到元空间
JVM分代模型:年轻代和老年代
对象的分配机制
分配机制
正常情况下是在堆上分配
新生代回收之后,因为存活对象太多,导致大量对象直接进入老年代
特别大的超大对象直接不经过新生代就进入老年代
有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数。只要对象大于这个参数,那么会直接进入老年代。避免对象在survivor 对象间频繁复制长期存活的对象
虚拟机给每个对象定义了一个对象年龄计数器,经过一次minorGC后仍然存活,并且被Survivor容纳的话,就加一岁,当它的年龄增加到一定程度(默认为15岁),就将晋升到老年代。
年龄阈值可通过“-XX:MaxTenuringThreshold”设置动态对象年龄判断机制
年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代
- 空间担保机制
每一次minor GC 之前,jvm会检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,假如老年代的内存小于新生代的全部对象大小,会看“-XX:-HandlePromotionFailure”的参数是否设置了,如果设置了就判断老年代的内存是否大于之前每一次minor Gc后进入老年代的平均对象大小。如果小于或者没有这个参数,那么就会执行full GC。如果执行后还是没有足够的大小够放对象。那么就会报“OOM”
方法区内会不会进行垃圾回收?
首先该类的所有实例对象都已经从Java堆内存里被回收
其次加载这个类的ClassLoader已经被回收
最后,对该类的Class对象没有任何引用
老年代什么是否触发垃圾回收?
JVM 优化之常用参数
-Xms: 堆最小空间,建议设置为物理内存的1/4
-Xmx:堆最大空间,建议设置为物理内存的1/2
-XX:NewRatio: Old/New的比例
-Xmn: 年轻代大小,调整会影响老年代大小,官方建议堆大小的3/8
-XX:SurvivorRatio: 调整Survivor和Eden的比例大小
-XX:MetaspaceSize: 元空间初始化大小,
-XX:MaxMetaspaceSize: 元空间最大大小
-XX:PretenureSizeThreshold: 大对象直接进入老年代的阈值
-XX:MaxTenuringThreshold: 进入老年代的分代年龄阈值
JVM性能调优
常用命令:jps、jinfo、jstat、jstack、jmap
调测工具
Arthas