1. 内存区域
下图和下面谈论的是jdk1.6的内存区域
1.1 线程私有
1.1.1 程序计数器
当前线程所执行的字节码的行号指示器
1.1.2 Java虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用
等信息。随着方法的调用和结束调用而执行出栈入栈。
-Xss 512M 栈大小
1.1.3本地方法栈
为本地方法服务。
1.2 线程公有
1.2.1 Java堆
所有对象分配内存的区域。
-Xms 1M 初始参数
-Xmx 2M 最大参数
分为新生代和老年代。
1.2.2 方法区
存放已被加载的类信息,常量,静态变量等数据。
1.2.3 常量区
全局字符串常量池:类准备阶段在堆中产生的字符串对象的引用(jdk1.7
开始)
Class文件常量池:编译后生成的字面量和符号引用
运行时常量区:某个类加载后把文件常量池中的符号引用和字面量存放在运行时常量区中
1.2.4 直接内存
NIO 类可以使用 Native 函数库直接分配堆外内存(Native 堆),通过一个存储在 Java 堆里的DirectByteBuffer
对象作为这块内存的引用进行操作。
1.3 不同版本的内存区域
jdk1.6及以前:永久代在JVM内存中,永久代包括方法区、字符串常量池(interned strings),运行时常量池在方法区;
jdk1.7:存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
jdk1.8:移除永久代,将方法区移至本地内存的元数据区。
jdk1.6:字符串常量池在永久代中,直接在其中创建字符串;jdk1.7:字符串常量池在堆中,存放的是字符串的引用。
String的intern
方法:jdk1.6:字符串常量池中存在相同内容则返回字符串常量池中的字符串引用,不存在的话则在其创建一个字符串对象并返回该引用;jdk1.7:字符串常量池中存在相同内容则直接返回该引用,不存在的话则将该字符串的引用存入字符串常量池中并返回。
1.4 内存泄漏与内存溢出
1.4.1 内存泄漏
含义:因为GC的存在,我们无法手动释放内存,所以会存在不再会被使用的对象的内存不能被回收。
场景及解决方案:
1)长生命周期的对象持有短生命周期的引用
public class Simple { Object object; public void method(){ object = new Object(); } }
object长周期拥有method中的短周期引用,由于method调用完就退出不再使用,即其中的引用不会再使用,但是由于object仍然存在引用链,所以不会被回收,导致内存泄漏。
解决方法:在方法内退出前添加object = null
来跟GC说该引用无用可以回收;尽量减小对象的作用域。
2)链表中失效的结点
public E pop(){ if(size == 0) return null; else return (E) elementData[--size]; }
elementData[size-1]依然持有E类型对象的引用,并且暂时不能被GC回收。
解决方法:将失效的结点设置为null,告诉gc它已经失效可以被回收。
排查方法:
使用内存映像工具获取堆转储快照,查看其中的GC Roots的引用链,可以找到泄露对象是怎样的路径与Roots关联并导致垃圾收集器无法自动回收,找到泄漏对象的信息和引用链的信息,就可以定位到泄漏的位置。
1.4.2 内存溢出
含义:内存溢出包括:栈溢出、堆溢出、方法区和运行时常量池溢出、直接内存溢出。
栈溢出:
栈的默认大小为一个线程1M。
线程请求栈深度大于虚拟机所允许的最大深度,引发stackoverflow。(一个线程的栈帧过大,如创建的对象过大,过长,本地变量表的长度过大)
虚拟机扩展栈时候无法申请到足够的内存空间,引发outofmemory。(线程过多,栈设置过小)
堆溢出:
堆默认初始值大小为物理内存的1/64,最大值为1/4。
只要不断的创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制之后,引发outofmemory。
方法区和运行时常量池溢出:
方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述。若运行时产生大量的类去填满方法区,直达溢出。比如Spring 对类进行增强时(创建代理类),都会使用到cglib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载到内存。
但字符串常量过大,过长则会引发运行时常量池溢出
直接内存溢出:
在使用NIO方式使用Native函数库直接分配堆外内存,存储在Java堆中的DirectByteBuffer对象在进行分配内存是时候会抛出内存溢出异常。
排查方法:
使用内存映像工具获取内存快照,查看内存中的各个对象的详细信息和使用情况。
1.4.3 JVM命令行工具
jmap:内存映像工具,生成堆转储快照,可以根据堆快照查看堆的使用情况,包括对象的详细信息,空间使用率,GC器
jstack:生成当前时刻的线程快照,该快照是当前虚拟机的每一条线程正在执行的方法堆栈的集合,可以查看线程运行时间,拥有资源,用来排查定位长时间停顿的原因,如死循环,线程间死锁,请求外部资源时间过长等。
2. 垃圾收集
2.1 对象存活依据
2.1.1 引用计数
给对象添加一个引用计数器,但对象增加一个引用则加一,引用失效则减一,引用为0时则可回收,但是这种问题会造成死锁
(相互循环引用)。
2.1.2 可达性分析
将GC Roots作为根节点向下搜索,对象到GC Roots没有任何引用链时则可回收。
GC Roots对象:
- 栈帧中的局部变量表中引用的对象
- 本地方法栈中JNI引用的对象
- 方法区静态和常量引用的对象
2.1.3 死亡的两次标记
第一次标记:确定可回收,筛选finalize,如果调用过或没有覆盖,则直接死亡,否则第二个标记
第二次标记:执行finalize,如果重新引用则不死亡,否则死亡
2.2 对象引用类型
- 强引用:即new一类的,被关联的对象不会回收
- 软引用:如果内存快要耗尽则会回收这类引用的对象
- 弱引用:弱引用引用的对象在下一次GC时就会被回收
- 虚引用:不占内存,只是在关联的对象死亡时发送一个系统通知
2.3 垃圾收集算法
2.3.1 标记-清除
标记要回收的对象,最后清除。
缺点:产生大量的碎片,对于需要连续空间的对象时而不得不Minor GC;效率不高。
2.3.2 标记-整理
让存活的对象向一端移动,然后清理端边界以外的内存。
缺点:效率不高
2.3.3 复制
将内存划分成等分两块,每次只使用其中一块,在清理的时候将一块的可用对象复制到另外一块上,然后把原来的那块清理。
缺点:浪费内存
新生代的复制算法:
划分Eden空间和两块Survivor空间,默认比例为8:2,只拿一块Survivor空间来用来清理使用,如果不够则需要老年代分配担保。
为什么需要两块Survivor空间:
总的来说是为了解决内存碎片的问题。
如果只有一块Survivor空间,那么新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。
2.3.4 分代收集
根据对象存活周期划分内存区域,根据不同年代来采用以上三种收集算法。新生代:复制算法;老年代:标记清除法和标记整理法。
2.4 垃圾收集器
前三个是新生代收集器(复制算法),后三个是老年代收集器(前两个使用标记整理算法,后一个标记清除算法),最后一个是具备二者功能的收集器(局部复制算法+整体标记整理算法)。
2.4.1 GC线程
单线程与多线程:单线程指GC线程只有一个,多线程指多个GC线程
并行与并发:并行(Parallel)指多个GC线程共同工作,用户线程仍然等待;并发(Concurrent)指用户线程与GC线程共同工作(交替);串行(Serial)指GC单线程,用户线程等待。
2.4.2 Serial收集器
单线程收集器。
应用场景:Client模式
2.4.3 ParNew收集器
Serial的多线程版本
应用场景:Server模式。只有它能与CMS配合使用。单线程性能不如Serial,多线程可用 -XX:ParallelGCThreads
参数来设置线程数
2.4.4 Parallel Scavenge收集器
多线程收集器。其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
应用场景:
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,但是缩短停顿时间是以牺牲吞吐量和新生代空间为代价的,新生代空间变小,GC频繁,吞吐量降低;
而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
参数:
通过-XX:+UseAdaptiveSizePolicy
打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurivivorRatio)、晋升老年代对象年龄等细节参数了。
2.4.5 Serial Old收集器
单线程收集器。
应用场景:Client模式。CMS的后备预案,在并发收集失败时使用
2.4.6 Parallel Old收集器
Parallel的老年代版本。
应用场景:在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
2.4.7 CMS收集器
并发收集器,用户线程和GC线程共同工作。
工作步骤:
- 初始标记:标记GC Roots能关联到的对象,时间极短,用户线程需要停顿
- 并发标记:进行GC Roots Tracing,时间最长,不需要停顿
- 重新标记:修正并发标记过程中部分对象的引用记录变化,需要停顿
- 并发清除:并发清除标记的部分。不需要停顿
缺点:
- 总吞吐量低:低停顿时间是以牺牲吞吐量为代价
- 大量空间碎片:往往由于空间足够但不连续,对于大对象来讲就不得不触发FullGC
- 无法处理浮动垃圾:浮动垃圾指并发清除过程中由用户线程继续运行产生的垃圾,需要等待下一次GC清除。由于浮动垃圾的存在所以需要预留空间,通过
-XX:CMSInitiatingOccupancyFraction
设置超过老年代的空间百分比触发FullGC;如果失败则采用后备SerialOld收集器。
2.4.8 G1收集器
并行并发收集器。
内存区域划分成 取消新生代和老年代的物理隔离,将heap划分成等大小的独立Region,对每个Region回收;通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
工作步骤:
- 初始标记
- 并发标记
- 最终标记:修正在并发标记中因用户线程而标记记录的变化,将变化数据合并在
Remembered Set
中,停顿用户线程,GC线程并行执行 - 筛选回收:对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,从而实现可预测的停顿。此阶段可以和用户并发工作,但是由于GC停顿时间可控,所以停顿用户线程可大幅度提高GC效率
优点:
- 并行与并发
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 避免全堆扫描:每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过GCRoots加 入Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
深入理解JVM(3)——7种垃圾收集器
3. 内存分配和回收策略
3.1 MinorGC和FullGC
Minor GC:新生代GC,执行速度快,触发频繁
Full GC:老年代GC,执行速度慢,触发不频繁
3.2 内存分配策略
3.2.1 对象优先在Eden分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
3.2.2大对象进入老年代
XX:PretenureSizeThreshold
,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
3.2.3 长期存活对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。-XX:MaxTenuringThreshold
用来定义年龄的阈值。
3.2.4 动态对象年龄判定进入老年代
如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
3.2.5 分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立则判断是否允许担保失败,不允许则FullGC;如果允许则判断连续空间是否大于上次进入老年代的对象的平均大小,小则FullGC,大于则MinorGC
3.3 内存回收策略(FullGC触发条件)
- 调用System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。
- 老年代空间不足:老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
- 分配担保失败:使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
- Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
4. 类文件结构
4.1 类文件存储
Class文件是一组以8字节为基础单位的二进制流,当遇到8字节以上的空间则会按照Big-Endian顺序分割成若干各8字节存储。没有分隔符,严格限制。
4.2 类文件结构
4.2.1 魔数与版本
确定是否是Class文件与Class文件的版本号
4.2.2 常量池
- 字面量:文本字符串、各种数据类型的值、final变量值
- 符号引用
- 类和接口的全限名
- 字段的名称和描述符
- 方法的名称和描述符
4.2.3 访问标志
标识一些类或接口层次的访问信息
4.2.4 类、父类索引与接口索引集合
索引指向常量池中的CONSTANT_CLASS_INFO类型的常量,这个常量又会指向UTF-8类型常量指出全限定名
4.2.5 字段表集合
- 字段类型:类变量和实例级变量,不包括局部变量。
- 字段表结构
- 字段修饰符:字段的作用域、实例还是类变量(static)、可变性(final)、并发可见性、可被序列
- 字段描述符:字段的数据类型
- 字段简单名称
- 字段属性表:指向属性表,存字段的值
4.2.6 方法表集合
- 方法表结构
- 方法修饰符
- 方法描述符:方法的参数列表(数量、类型、顺序)、返回值
- 方法简单名称
- 方法属性表:指向属性表类型为Code的,存方法体内的代码
4.2.7 属性表集合
承接上文的字段表、方法表中的属性表元素。
5. 类加载机制
5.1 类初始化时机
5.1.1 主动引用
- 使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
- 对类进行反射访问时
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
- 继承结构中父类(接口只有在引用的时候加载)尚未初始化时
- 虚拟机刚启动时用户需要调用主类
5.1.2 被动引用
- 通过子类调用父类的静态字段
- 调用类的静态常量
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
5.2 类加载过程
5.2.1 加载
1)加载过程
- 通过一个类的全限定名获取定义此类的二进制流
- 将字符流所代表的静态存储结构转化成方法区的运行时数据结构
- 方法区中中生成一个Class对象,作为方法区这个类的各种数据的访问入口
2)类文件来源
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础
- 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,例如动态代理技术
5.2.2 验证
确保类文件的字节流包含的信息符合当前虚拟机的要求。
- 文件格式验证:判断是否为Class文件格式
- 元数据验证:判断是否语义是否符合语言规范
- 字节码验证:确定程序语义是否是合法的符合逻辑
- 符号引用验证:发生在解析阶段的将符号引用转为直接引用的时候
5.2.3 准备
为类变量(static)分配内存并设定初始值(内存空间为0)的阶段。
注意:这里指类变量不是实例变量,实例变量是和对象一起分配在堆里的,这里是方法区。类变量的初始值设置为代码设定的是要在初始化阶段设置的(<clinit>)。
5.2.4 解析
虚拟机将常量池内的符号引用替换成直接引用。某些解析是可以在初始化时进行(动态绑定)。
5.2.5 初始化
初始化类代码,执行<clinit>类构造器
1)<clinit>类构造器特点
- 由编译器自动收集类中的所有类变量的赋值和静态代码块的语句合并产生。(收集顺序是源文件中顺序决定)
- 接口和类在没有类变量和静态代码块(接口不允许有)的时候,不会生成<clinit>
- 不需要显式地调用父类的类构造器,父类的<clinit>会在子类前完成初始化。
- 只有当父接口定义的变量执行时才会执行<clinit>
- 如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕,其它线程唤醒后也不会再执行<clinit>方法
2)<init>方法构造器特点
<init>方法负责初始化对象方法构造器和实例变量,只有在实例化对象的时候才会调用
5.3 类加载器
类加载阶段的加载动作中的获取类权限定名来获取二进制流
由虚拟机外部实现,实现这个动作代码模块称为类加载器。
5.3.1 类加载器分类
1)启动类加载器
此类加载器负责将存放在 <jre_home>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。</jre_home>
启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
2)扩展类加载器
负责将 <java_home>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。</java_home>
3)应用程序类加载器
负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
5.3.2 双亲委派模型
模型特点:
除了启动类加载没有父加载器,其余都要有父加载器, 并且不是继承关系而是组合关系,
工作过程:
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。