- Java虚拟机所管理的内存中最大的一块,Java堆是全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例以及数组都在这里分配内存。
- 垃圾收集器管理的主要区域,亦称“GC堆”
它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 也被称为Non-Heap(非堆),永久代html
方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。java
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各类字面量和符号引用)。 JDK1.7及以后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。程序员
整个永久代有一个 JVM自己设置固定大小上限,没法进行调整,而元空间使用的是直接内存,受本机可用内存的限制。你可使用
-XX:MaxMetaspaceSize标志设置最大元空间大小,默认值为unlimited,这意味着它只受系统内存的限制。
-XX:MetaspaceSize调整标志定义元空间的初始大小若是未指定此标志,则Metaspace将根据运行时的应用程序需求动态地从新调整大小。面试
程序计数器是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。算法
- 字节码解释器经过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的状况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候可以知道该线程上次运行到哪儿了。
程序计数器是惟一一个不会出现 OutOfMemoryError 的内存区域。数组
虚拟机描述的是Java方法执行的内存模型,每次方法调用的数据都是经过栈传递的。缓存
用于存储局部变量表、操做数栈、动态连接、方法出口等信息。安全
- 基本数据类型;
- long和double会占用2个局部变量空间;
- 对象引用;
- returnAddress。
- Stack OverflowError 若Java虚拟机栈的内存大小不容许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
- OutOfMemoryError 若 Java 虚拟机栈的内存大小容许动态扩展,且当线程请求栈时内存用完了,没法再动态扩展了,此时抛出OutOfMemoryError异常。
和虚拟机栈所发挥的做用很是类似,区别是: 虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务服务器
对象的建立过程 数据结构
类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,而且检查这个符号引用表明的类是已被加载过、解析和初始化过。若是没有,那必须先执行相应的类加载过程。
分配内存
- 原理 用过的内存所有整合到一边,没有用过的内存放在另外一边,中间有一个分界值指着,只须要向着没用过的内存方向将指针移动对象内存大小位置便可
- 适合场景 堆内存规整(即没有内存碎片的状况下)
- GC收集器 Serial、ParNew等带compact过程的垃圾收集器
- 原理 JVM会维护一个列表,记录有哪些内存块是可用的,在分配的时候,找一块足够大的内存块来划分给对象实例,最后更新列表记录
- 适合场景 堆内存不规整的状况下
- GC收集器 CMS
在建立对象的时候有一个很重要的问题,就是线程安全,由于在实际开发过程当中,建立对象是很频繁的事情,做为虚拟机来讲,必需要保证线程是安全的,一般来说,虚拟机采用两种方式来保证线程安全:
- CAS+失败重试 保证更新操做的原子性
- TLAB 为每个线程预先在Eden区分配一起内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配
初始化零值
内存分配完成后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头),这一步操做保证了对象的实例字段在 Java 代码中能够不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头
初始化零值完成以后,虚拟机要对对象进行必要的设置,例如这个对象是哪一个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不一样,如是否启用偏向锁等,对象头会有不一样的设置方式。
执行init方法
在上面工做都完成以后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象建立才刚开始, 方法尚未执行,全部的字段都还为零。因此通常来讲,执行 new 指令以后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算彻底产生出来。
创建对象就是为了使用对象,咱们的Java程序经过栈上的 reference 数据来操做堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:
若是使用句柄的话,那么Java堆中将会划分出一块内存来做为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
若是使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。
DirectMemory容量可经过-XX:MaxDirectMemorySize指定,若是不指定,则默认与Java堆最大值相同
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析、链接和初始化,最终造成能够被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
这5种场景中的行为成为对一个类进行主动引用。除此以外全部引用类的方法都不会触发初始化,成为被动引用。
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
- 文件格式验证 验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理
- 元数据验证 对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
- 字节码验证 最复杂的一个阶段,主要目的是经过数据流和控制流分析,肯定程序语言是合法的、符合逻辑的。
- 符号引用验证 发生在虚拟机将符号引用转化为直接引用的时候,这个转化动做将在链接的第三阶段--解析阶段中产生。符号引用验证能够看作是对类自身之外(常量池中的各类符号引用)的信息进行匹配性检验
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
到了初始化阶段,才真正开始执行类中定义的Java程序代码
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其余类加载器均由 Java 实现且所有继承自java.lang.ClassLoader:
一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工做的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,不然才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,所以全部的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器没法处理时,才由本身来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 做为父类加载器。
findClass只是loadClass的其中一步,若是父加载器和根加载器都没有找到这个类,就会调用findClass方法,若是继承了findClass方法,那么双亲委派模型就不会被破坏,这其实就是一个模板方法模型。
内存模型能够理解为在特定的操做协议下,对特定的内存或者高速缓存进行读写访问的过程抽象,不一样架构下的物理机拥有不同的内存模型,Java虚拟机也有本身的内存模型,即Java内存模型(Java Memory Model, JMM)。
在C/C++语言中直接使用物理硬件和操做系统内存模型,致使不一样平台下并发访问出错。
而JMM的出现,可以屏蔽掉各类硬件和操做系统的内存访问差别,实现平台一致性,是的Java程序可以“一次编写,处处运行”。
public class Singleton {
private volatile static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) {
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
复制代码
单例对象instance须要加上关键字volatile禁止指令重排序,保证可见性。(详情请点我)
- JMM要求lock、unlock、read、load、assign、use、store、write这8个操做都必须具备原子性,但对于64为的数据类型(long和double),具备非原子协定:容许虚拟机将没有被volatile修饰的64位数据的读写操做划分为2次32位操做进行。
- 若是多个线程共享一个没有声明为volatile的long或double变量,而且同时读取和修改,某些线程可能会读取到一个既非原值,也不是其余线程修改值的表明了“半个变量”的数值。不过这种状况十分罕见。由于非原子协议换句话说,一样容许long和double的读写操做实现为原子操做,而且目前绝大多数的虚拟机都是这样作的。
JMM保证的原子性变量操做包括read、load、assign、use、store、write,而long、double非原子协定致使的非原子性操做基本能够忽略。若是须要对更大范围的代码实行原子性操做,则须要JMM提供的lock、unlock、synchronized等来保证。
volatile、synchronized和final也能保证可见性
Java程序中自然的有序性能够总结为
- 若是在本线程内观察,全部的操做都是有序的; ( 指“线程内表现为串行的语义”)
- 若是在一个线程中观察另外一个线程,全部的操做都是无序的。 (“指令重排序”现象和“工做内存与主内存同步延迟”现象)。 Java程序提供了volatile和synchronized两个关键字来保证线程之间操做的有序性
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任什么时候候计数器为0的对象就是不可能再被使用的。
这个算法的基本思想就是经过一系列的称为 “GC Roots” 的对象做为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证实此对象是不可用的。
若是一个对象具备强引用,它就不会被垃圾回收器回收。即便当前内存空间不足,JVM也不会回收它,而是抛出OutOfMemoryError 错误,使程序异常终止。若是想中断强引用和某个对象之间的关联,能够显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
使用软引用时,若是内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
具备弱引用的对象拥有的生命周期更短暂。由于当JVM进行垃圾回收,一旦发现弱引用对象,不管当前内存空间是否充足,都会将弱引用回收。不过因为垃圾回收器是一个优先级较低的线程,因此并不必定能迅速发现弱引用对象
顾名思义,就是形同虚设,若是一个对象仅持有虚引用,那么它至关于没有引用,在任什么时候候均可能被垃圾回收器回收。
当垃圾收集器准备回收一个对象时,就会在回收对象内存以前,把这个虚引用加入到与之相关的引用队列中,程序能够经过判断引用队列中是否加入了虚引用,来了解被引用的对象是否将要被垃圾回收,若是程序发现某个虚引用已经被加入到引用队列,那么就能够在引用的对象的内存被回收以前采起必要的行动
不像C语言,咱们能够控制内存的申请和释放,在Java中有时候咱们须要适当的控制对象被回收的时机,所以就诞生了不一样的引用类型,能够说不一样的引用类型实则是对GC回收时机不可控的妥协.有如下几个使用场景能够充分的说明:
算法分为“标记”和“清除”阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进获得。
它能够将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另外一块去,而后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
根据老年代的特色提出的一种标记算法,标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象回收,而是让全部存活的对象向一端移动,而后直接清理掉端边界之外的内存。
- 当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不一样将内存分为几块。通常将java堆分为新生代和老年代,这样咱们就能够根据各个年代的特色选择合适的垃圾收集算法。
- 好比在新生代中,每次收集都会有大量对象死去,因此能够选择复制算法,只须要付出少许对象的复制成本就能够完成每次垃圾收集。而老年代的对象存活概率是比较高的,并且没有额外的空间对它进行分配担保,因此咱们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
若是说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
“单线程” 的意义不只仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工做,更重要的是它在进行垃圾收集工做的时候必须暂停其余全部的工做线程( "Stop The World" ),直到它收集结束。
简单而高效(与其余收集器的单线程相比)
Serial收集器因为没有线程交互的开销,天然能够得到很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来讲是个不错的选择。
- ParNew收集器其实就是Serial收集器的多线程版本
- 它是许多运行在Server模式下的虚拟机的首要选择
- 除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工做。
- Parallel Scavenge 收集器相似于ParNew 收集器。
- Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提升用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
- Parallel Scavenge收集器提供了不少参数供用户找到最合适的停顿时间或最大吞吐量,若是对于收集器运做不太了解的话,手工优化存在的话能够选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
Serial收集器的老年代版本
- Parallel Scavenge收集器的老年代版本
- 使用多线程和“标记-整理”算法
- 在注重吞吐量以及CPU资源的场合,均可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而很是符合在注重用户体验的应用上使用。
基于“标记-清除”算法实现的
整个过程分为4个步骤
优势
缺点
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高几率知足GC停顿时间要求的同时,还具有高吞吐量性能特征.
为了能更好地适应不一样程度的内存情况,虚拟机并非永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代。
参考文献