一、运行时数据区域
java虚拟机会在程序运行期间将虚拟机内存进行分区管理,每个区域都有自己独特的用途
程序计数器
当前线程所执行字节码的行号指示器(指令地址),负责处理程序的分支,循环跳转等,生命周期和线程同步,如果正在执行是一个Native方法,这个程序计数器记录的地址为空
虚拟机栈
用于描述方法执行的内存模型,方法执行的同时都会创建一个栈帧,里面存储了局部变量表,操作数栈,动态链接,方法出口等信息,一个方法的执行就是对应栈帧在虚拟机中入栈出栈的过程
虚拟机栈中归档了两种异常情况
- 线程请求栈深度大于虚拟机所允许的深度,会抛出StackOverFlowError异常
- 如果虚拟机栈扩展时无法申请到足够的内存,就是抛出OutofMemoryError异常
本地方法栈
本地方法栈是给Native(非java)方法提供服务的,类似于虚拟机栈给java方法服务一样,本地方法也会抛出StackOverFlowError、OutofMemoryError异常
Java堆(GC堆)
是虚拟机管理内存中最大的一块,用于存放对象实例,基本上所有对象都在该区域分配内存空间
- 从回收内存角度来看java堆中还会有新生代,老年代等
- 从分配内存来看,堆内存中划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer TLAB)来提高对象分配时的效率
java堆中的空间不要求连续,但是在逻辑上是被看作是连续的,并且堆的大小是可以通过-Xms,-Xmx来扩展的
-Xmx:设置java堆空间的起始大小
-Xms:设置最大值
方法区
用于存放虚拟机加载的类型信息,常量,静态变量即时编译器编译后的代码缓存等数据
JDK8以前方法区通过"永久代"实现,这种设计因为内存回收效率不高,更容易遇到内存溢出的问题,因为永久代有-XX:MaxPerSize的上限,即使不设置也有默认大小;所以JDK8后移除永久代,通过本地内存中实现的"元空间"(Meta Space)来代替,并且将原来存放在永久代的字符串常量池,静态变量移出
永久代:是收集器的分代设计的一种命名,这样HotStop的垃圾收集器可以像管理java堆内存一样管理该区域
运行时常量池
- 属于方法区的一部分,用于存放Class中类的信息,字段,方法,接口等描述信息,还有常量池表(Constant Pool Table),这些信息会在类加载后全部放入运行时常量池
- 相对于Class的常量池,运行时常量池具有动态性,该区域常量不一样只有编译期间才能产生,运行期间也可以将新的常量放入池中,例如常见的String.intern()方法
直接内存
直接内存并不是虚拟机运行时数据区的一部分,但是频繁被使用,也会导致OOM
JDK1.4加入的NIO类,引入了通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据
二、HotSpot虚拟机对象
对象的创建
- 首先判断该引用在常量池中是否存在,该类是否被加载,解析,初始化过,如果没有需要执行相应的类加载过程
- 在类加载检查通过后,jvm会为新生对象分配内存,所需内存大小在类加载完后就可以确定
- 内存分配后会进行初始化默认值,如果使用了TLAB,这个工作也会提前到TLAB分配时顺便执行。这步操作保证了对象的实例字段在java代码中不赋值就可以直接使用
- jvm为对象设置详细信息,例如对象的哈希码,GC分代年龄等
- 上面工作完成后,从JVM角度看一个对象已经产生了,但是从java程序角度来说,对象的创建才刚刚开始,开始执行构造函数(Class文件中的方法),构造函数会使对象按照程序员的意愿进行初始化
并发情况下创建对象也是不安全的,有两种方案可以解决
- 虚拟机是采用CAS配上失败 重试的方式保证更新操作的原子性
- 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定
-XX:+/-UseTLAB参数来 设定是否采用TLAB
对象的布局
1:对象头 Header
- 存储对象运行时数据,如哈希码,GC分代年龄,锁状态表示等,这部分数据在32位和64位虚拟机中占32个bit和64个bit,这部分数据又叫Mark Word
- 类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例;如果对象是一个数组,对象头中还必须要有一块记录数组长度的数据区域
2:实例数据 Instace Data
存储正真有效的信息,即程序中定义各种类型的字段,包括父类继承下来的,这部分存储顺序按照HotSpot随机默认分配顺序来的
-XX:FieldsAllocationStyle
3:对齐填充 Padding
这部分不是必须的,也没有特别的含义,主要是起到了占位符的作用,因为HotSpot虚拟机要求对象起始地址必须是8字节的整数倍,任何对象的大小都必须是8字节的整数倍,如果部分实例数据没用对齐的话就需要补齐来补全
三、垃圾收集
对象是否可被回收
1:引用计数法
在对象中添加一个引用计数器,当有一个地方引用它,计数器就+1,当引用失效时,计数器就-1,当对象计数器为0时,该对象则不可用。
虽然这种方法占用了额外的空间,但是这种算法简单高效;这种算法没用被主流采用是因为它面对的情况很单一,和很难解决对象之间互相引用的问题,如下:对象objA,objB字段中都引用了对方,除此之外这两对象再无其他引用,但是因为他们两个互相依赖,导致对象的引用计数器都不为0,这种算法也无法回收他们
public class ReferenceCountingGC {
private static final int _1MB = 1024 * 1024;
public Object instance = null;
/*** 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 */
private final byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null; // 假设在这行发生GC,objA和objB是否能被回收? System.gc();
}
}
2:可达性算法
主流的内存管理系统都是通过可达性算法来判断对象存活的。通过一些列"GC Roots"根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过后的路径很自然成为了引用链;如果某个对象到GC Roots间没用任何引用链,就称GC Roots到该对象不可达,可以被回收。
Java中GC Roots对象可以包括:
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- 虚拟机栈中局部变量表中引用的对象,例如被调用的方法堆栈中使用到的参数,局部变量,临时变量等
3:引用
如果refrence类型的数据中存储的数值代表另一块内存的起始地址,就称refrence数据是代表某块区域,某个对象的引用,这种概念太过狭隘;我们希望描述为,一类对象当空间足够时,可以保存在内存中;当内存不够时,可以抛弃这些对象,因此定义了如下四类引用:
强引用(Strongly Reference)
代码中普遍存在的引用赋值,Object obj = new Object()这种的引用关系,无论任何情况下,只要强引用关系还存在,垃圾收集器永远不会收集被引用的对象
软引用(Soft Reference)
当垃圾收集时,只有在内存不足的时候,会回收被弱引用关联的对象,用SoftReference类来实现
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
弱引用(Weak Reference)
当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象,用WeakReference类来实现
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
虚引用(Phantom Reference)
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期,如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收,用PhantomReference类来实现
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
4:对象自我拯救
在可达性算法判定不可达的对象,也不是非死不可,宣告一个对象死亡要经历两个标记:
- 当对象不可达后,对象会被第一次标记,并且进行第一次筛选,如果对象没有覆盖过finalize()方法,或者该方法已经被调用过一次,那么这次筛选就失败;如果成功,会将该对象放入F-Queue队列中
- 在F-Queue队列中的对象会再次被标记,如果对象在finalize()方法中重新与引用链关联,那么该对象会被移出"即将收集"的集合;不然就真的GG了
垃圾收集算法
1:分代收集理论
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难消亡
因此收集器应该将堆划分为不同的区域,并且根据对象的年龄分配到不同区域,根据区域内存放对象的性质来进行垃圾收集,这样可以兼顾效率和内存空间的有效利用,目前java堆的主流划分方式:
-
新生代(Young Generation):该区域中每次垃圾收集都会有大批对象死去,每次存活的少量对象都会晋升到老年代中存放
-
老年代(Old Generation):该区域中的对象存放都是很难死亡的对象
2:收集名词
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中分为:
- 新生代收集(Minor GC/Young GC):只收集新生代
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
3:标记-清除算法
首先标记出需要回收的对象,标记完成后,统一回收所有被标记的对象(当然也可以反过来)
优点:过程比较简单
缺点:执行效率不稳定,如果java堆中包含大量需要被清除的对象,那么标记清除这个动作需大量进行;第二会引起内存碎片化的问题,空间碎片太多可能会导致再分配大对象时无法找到连续内存而不得已在出发另外一次垃圾回收动作
4:标记-复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还活着的对象复制到另外一块上面,然后把使用过的内存空间一次清理掉;算法需要复制的只是占少数存活的对象,并且每次都是针对半区进行内存回回收
优点:不会产生碎片内存,运行高效
缺点:可用内存是原来的一半,内存利用率低
Appel针对后来根据对象"朝生夕灭"的特点,提出了更加优化的半区复制算法(Appel式回收),具体做法把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和一块Survivor,垃圾收集时,将存活的对象一次性复制到另外一块Survivor,然后直接清理掉Eden和Survior空间即可。
HotSpot虚拟机默认Eden和Survivor的比例是8:1,所以新生代中可用内存空间是整个新生代容量的90%,但是不能保证Minor GC后存活的对象小于10%的Survivor大小,因此当Survivor区域无法容纳一次Minor GC之后存活的对象时,就需要原来其他内存区域(大多数是老年代)作为分配担保
5:标记-整理算法
标记-复制算法在对象存活率高的时候需要较多复制操作,效率变低,并且需要额外的空间作为担保,以应对全部对象都是100%存活的极端情况,老年代中一般不用这种算法、
过程和标记-清除算法一样,后面不是清理对象,而是而是让所有存活的对象都向内存空间一端移动,直接清理掉边界外的内存,属于移动式回收算法;因为老年代中有大量存活的对象,移动对象并更新所有引用是一个很负重的操作,并且这种移动操作需要暂停全程应用程序才能进行,这种停顿被称为"Stop The World"
垃圾收集器
如下图各种垃圾收集器所属的区域表示他们属于新生代或者是老年代,连线表示他们可以搭配使用
1:Serial收集器
Serial翻译为串行,单线程工作模式,当前垃圾回收线程工作时,其他线程必须暂停,直到它结束
Serial优点是简单高效,对于收集器额外内存消耗是最小的,单核处理器或者少核处理器来说没有其他线程交互开销,可以活得最高的单线程收集效率
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的
2:ParNew收集器
Serial收集器的多线程版本,支持多线程并行收集吗,JDK7之前首选的新生代收集器,并且可以和CMS收集器配合工作
3:Parallel Scavenge收集器
同属新生代收集器,同样基于标记-复制算法实现,也是能够并行收集的多线程收集器
CMS等收集器的关注点是缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge的目标是达到一个可控制的吞吐量(Throughput),高的吞吐量表示可以最高的利用处理器资源,尽快完成程序运算任务,该收集器提供下面两个参数来精确控制吞吐量
-XX:MaxGCPauseMillis -- 最大垃圾收集停顿时间
-XX:GCTimeRatio --运行垃圾收集时间
-
-XX:MaxGCPauseMillis设置大于0的毫秒值,收集器尽力将花费时间控制在这个时间内,但是这个值不是越小越小,因为垃圾收集的停顿时间是以牺牲吞吐量和新生代空间为代价来换取的,新生代空间容量变小了,直接导致了小内存下垃圾收集变得更加频繁了,吞吐量随着下来了
-
-XX:GCTimeRatio取值(0,100),等于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%,(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间
-
-XX:+UseAdaptiveSizePolicy 这个参数打开后,不需要人工指定新生代各个区域等细节参数,虚拟机会根据当前系统的运行情况动态调整这些参数,因此这个收集器也叫自适应调节策略(GC Ergonomics)
4:Serial Old收集器
是 Serial 收集器的老年代版本,采用标记-整理算法,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
5:Parallel Old收集器
Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现,Parallel Scavenge 和 Parallel Old搭配用在注重吞吐量或者处理器资源较为稀缺的场合
6:CMS收集器(Concurrent Mark Sweep)
一种获取最短回收停顿时间为目标的收集器,目前基于互联网的java应用响应速度都很重要,因此停顿时间要尽可能短;CMS收集器是基于标记-清除算法实现的,实现过程国家复杂,主要有四个步骤:
- 初始标记(CMS initial mark ):需要Stop The World,仅仅是标记GC Roots能直接关联的对象,速度很快
- 并发标记(CMS concurrent mark):GC Roots的直接关联对象开始遍历整个对象图的过程,耗时长,不需要停顿
- 重新标记(CMS remark):修正并发标记期间,部分对象继续运作导致标记产生变动,耗时短,需要停顿
- 并发清除(CMS concurrent sweep):清理标记阶段判断的已经死亡的对象,耗时长
整个过程中耗时最长的是并发标记和并发清除阶段,垃圾收集器线程都可以和用户线程一起工作,总的来说CMS收集器是和用户线程一起并发执行的
缺点:
- 会因为占用一部分线程,导致程序变慢,吞吐量低
- 当处理器核心数不足4个时,CMS对程序影响很大,并且使得处理器负载很高,导致用户程序的执行速度大幅度降低
- 无法处理"浮动垃圾",可能导致Con-current Mode Failure,浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC
7:Garbage First收集器
简称G1,开创了面向局部收集的设计思路和基于Region的内存布局,目的是为了替换CMS,在JDK9成为了服务端默认的垃圾收集器
G1不再局限于新生代老年代了,而是面向整个堆内存的所有回收集(Collection Set)来回收,回收标准是哪快内存中存放的数量最多,回收收益最大,就是G1收集器的Mixed GC模式
基于Region的堆内存布局是基础,G1将堆划分为多个大小相等的独立区域(Region),每个区域都根据自己的需要来扮演新生代的Eden区,Survior区,或者是老年代的空间;收集器可以按照不同的策略去处理;Region中Humongous区域是专门存储大对象的,只要是超过了Region容量一般的对象都认为是大对象,可以通过-XX:G1HeapRegionSize设定,范围1~32MB,并且G1把这块区域看作是老年代的一部分来对待
如何保证收集效率呢?
虽然G1只是逻辑上保留了新/老年代的概念,但是物理都是一些列连续的动态集合,取而代之通过收集Region最小内存单元,这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region;每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记:标记一下GC Roots能直接关联的对象,这个阶段需要停顿线程,并且耗时很短,通常借用Minor GC时候同步完成,所以G1在这个阶段没有额外的停顿
- 并发标记:从GC Roots中开始堆中对象开始可达性分析,递归扫描整个对象图,耗时长,但是可以并发执行
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
对象的分配和回收策略
对象的内存分配,从概念上讲,是堆上分配;从分代设计下,新生代对象通常会分配在新生代中,少数大对象可能会分配在老年代,不同分配规则不是固定的,取决于虚拟机按当前使用的是哪个收集器,以及虚拟机和内存中设定的参数有关,当前测试我们基于Serial加Serial Old客户端默认组合
1:对象优先在Eden分配
大多数情况,对象在Eden中分配,当Eden区没有足够空间进行分配时,虚拟机发生Minor GC
2:大对象直接进入老年代
大对象指需要大量来连续占用内存空间的java对象,典型的很长的字符串,元素数量很庞大的数组,因为在分配空间的时候,容易导致明明还有内存就触发了GC,以获得足够的连续空间,而复制对象的时候,也意味着高额的内存复制开销,因此我们在程序中尽量避免大对象的原因
-XX:PretenureSizeThreshold = ?
通过设置大于该值的对象直接分配到老年代,这样也可以避免在Eden区和两个Survivor区之间来回复制,产生了大量的内存复制的操作
-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,HotSpot 的其他新生代收集器,如Parallel Scavenge并不支持这个参数。如果必须使用此参数进行调优,可考虑 ParNew加CMS的收集器组合
3:长期存活的对象直接进入老年代
每个对象都有一个对象年龄计数器,在对象头中,可以通过设置晋升老年代的年龄阈值来使对象提前进入老年代,默认判定条件是15
-XX:MaxTenuringThreshold=1
4:动态对象年龄判定
HotSpot虚拟机不是要求所有对象的年龄必须达到设置值才会晋升到老年代,如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
5:空间分配担保
在发生Minor GC之前,必须先检查老年代最大可用连续空间是否大于新生代所有存活对象总空间来保证这次回收哦是安全的,如果不成立的话虚拟机会查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败,允许会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试进行一次Minor GC,空间即便不够也有老年代最为担保;若小于,就要进行一次Full GC
虚拟机日志分析
JDK9之后有统一的日志格式,各种设置针对不同的虚拟机也不一样,JDK9统一HotSpot所有功能都归到-Xlog参数
-Xlog[:[selector][:[output][:[decorators][:output-options]]]]
命令行最关键的参数是selector,由标签(Tag)和日志级别(Level)共同组成,标签指虚拟机某个功能模块的名字,告诉日志框架用户想要哪些功能的日志输出。录入垃圾收集器的标签名字是gc,因此垃圾收集器的日志集合了众多日志功能
add,age,alloc,annotation,aot,arguments,attach,barrier,biasedlocking,blocks,bot,breakpoint
日志级别从低到高
Trace ,Debug,Info,Warning,Error,Off六种级别,并且可以使用修饰器来要求每行都附加上的额外内容
1:查看GC的基本信息
JDK 9前 JDK 9后
-XX:+PrintGC -Xlog:gc
2:查看GC详情
JDK 9前 JDK 9后
-XX: +PrintGCDetails -Xlog:gc*
3:查看GC前后堆、方法区的可用容量变化
JDK 9前 JDK 9后
-XX: +PrintHeapAtGC -Xlog:gc+heap=debug
四:类的加载机制
类加载机制:jvm将字节码文件加载进内存,并且对数据进行校验,转换解析和初始化。最后形成可以被虚拟机直接使用的类型
在java中,类的加载,链接,初始化过程都是在程序运行期间完成的,虽然会使提前编译和类加载过程中增加开销,但是也给java带来了可扩展性和灵活性,Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的
类的生命周期
包括以下 7 个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
类加载过程
1:加载
- 通过一个类的全限定名来获取该类的二进制字节流(不一定是从Class文件中获取)
- 将该字节流所代表的静态存储结构转为方法区的运行时数据结构
- 在内存中生成一个代表该类的Class对象,作为方法区中这个类各种数据的访问入口
非数组类型的加载阶段自由性更强,加载阶段既可以用JVM内置的类加载器完成,也可以用用户自定义的类加载器完成(重写一个类加载器的findClass或loadClass方法)
2:验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
3:准备
为类中定义静态变量分配内存并设置类变量初始值,JDK7以前,静态变量存放在永久代上;JDK7起,静态变量,字符常量这些从永久代中移除出去;JDK8后,类变量随着Class对象一起存放在Java堆中,所以类变量在方法区只是一种逻辑概念的表述;实例(成员)变量会在对象实例化随着对象一起分配到java堆中
基本数据类型的零值
public static final int value = 123;
那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把 value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行
如果类变量是常量(final),那么它将初始化为表达式所定义的值而不是0,例如下面的常量value 被初始化为123而不是0
public static final int value = 123;
4:解析
将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定
5:初始化
JVM真正执行java代码,该阶会根据程序去初始化变量和其他资源,初始化阶段就是执行类构造器()方法的过程,该方法是Javac编译器自动生成的,他会收集类中所有类变量的赋值动作和静态语句块中(Static {})的语句来合并,收集的顺序是按照语句在源文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,在它之后的变量,静态语句块可以赋值,但是不能访问
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
- 方法和类构造函数不同,它不需要显示的调用父类构造器,JVM保证子类在方法执行前父类的方法已经执行完毕,JVM第一个执行的肯定是Object的方法
- 方法不是必须的,如果类中没有静态语句块,也没有对变量进行赋值操作,这个方法不会生成
由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
类加载器
任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,两个类相等,只能是两个类是否由同一个类加载器加载的前提下才有意义,即便是来自同一个Class的两个类,被同一个虚拟机加载,如果类加载器不同,那么他们两个类也不会相等
类加载器分类
1:启动类加载器(BootStrap ClassLoader)
加载<JAVA_HOME>\lib目录中,并且虚拟机可以识别,例如rt.jar,在自定义类加载器,如果需要把加载请求委派给引导类加载器,直接使用null替代
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if(cl == null)return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader ccl = ClassLoader.getCallerClassLoader();
if(ccl!=null && ccl!=cl &&!cl.isAncestor(ccl)){
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
} return cl;
}
2:扩展类加载器(Extension ClassLoader)
负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展
3:应用程序类加载器(Application ClassLoader)
也叫系统类加载器,负责加载ClassPath上所指定的类库,可以直接使用,如果我们没有自定义过类加载器,就是程序中默认的类加载器;有必要的话可以加入自定义类加载器
双亲委派模型
除了顶层的启动类加载器外,其余类加载器都应该有自己的父类加载器,类加载器之间父子关系通常以组合的关系来复用父加载器的代码;该模型不是约束模型,而且推荐的一种类加载器的实现方式
工作过程
如果类加载器收到一个类的加载请求,他会把这个请求委派给父类加载器中完成,最终都会传送到顶层的启动器中,如果父类无法加载,子类加载器才会自己加载
**优点:**保持通用类所的类加载器的环境相同;相反如果由各个类来加载,java体系最基础的行为都无法保证