1.运行时数据区域
1.区域划分
2.程序计数器
- 内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
- 如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
Java 虚拟机栈
- 线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
- 局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
- 内存泄漏异常:
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
3.本地方法栈
- 区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
- 内存泄漏异常:
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。。
4.Java 堆
- 对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。new出来的实例都存放与此,其内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
- 内存泄漏异常: OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
5.方法区
- 属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
6.运行时常量池
- 属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。
- 内存泄漏异常: OutOfMemoryError:无法申请内存时抛出 OutOfMemoryError。
7.直接内存
- 非虚拟机运行时数据区的部分,在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
- 内存泄漏异常: OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。
2.虚拟机对象创建使用
1.对象的创建
在我们使用new一个对象的时候,对于jvm来说的操作就不是代码看上去那么简单的。
- JVM回去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有就必须先执行相应的类加载过程。
- 当类加载检查通过之后,JVM将给新生的对象分配内存空间,对象所需要的内存大小在类加载完成后就可以完全确定了。在jvm堆中分配内存的又分两种情况:
(1) 指针碰撞式:如果Java堆中内存绝对规整的,堆中所有用过的内存都放在一边,空闲的内存放在另一边,中间有一个指针作为分界点的指示器,分配内存的过程就是将把指针向空闲的的空间那边挪动一段与对象大小相等的距离。
(2)那如果Java堆内存不是规整的,而是空闲的和已使用的内存是相互交错的,那就没有办法用指针碰撞的方式来分配内存。所以就有另外一种方式,JVM必须维护一个列表,列表记录着哪些内存是可用的,然后根据记录找到足够存放对象大小的内存空间,划分给对象,并且在列表中进行登记。 - 当内存分配完成后,JVM就将分配到的内存空间都初始化为零值(不包括对象头)。
- 接下来JVM将对对象进行必要的设置,比如设置对象属于哪个类的实例,对象的哈希码,对象的GC分代年龄等信息,这些信息放在了对象头中。
- 对于JVM来说,到此已经产生了一个新的对象,但是对于Java程序来说,对象的创建才刚刚开始,init方法还没有执行,所有的字段都为零。执行new指令之后接着执行init方法,把对象按照程序员的意愿进行初始化,这样子一个真正可用的对象就算完全产生出来了。
2.对象的内存布局
在jvm对象分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头(Header):包含两部分:
第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。
第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。 - 实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
- 对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。
3.对象的访问定位
我们知道在java中创建新对象的时候会产生一个引用类型和实例对象。
引用类型(reference):保存的是实例对象的引用,存储在栈中
实例对象:存储在堆中
注:新建实例对象的时候JVM同时会加载实例对象java类文件,将实例对象的类型信息保存在方法区中,后面我们可以看到访问对象的时候会同时去方法区中访问类型信息。
由于JVM规范中reference类型只定义了一个指向对象的引用,并没有定义引用如何去定位这个对象,因此不同的虚拟机有不同的方式去访问对象,主流的对象访问方式有两种:使用句柄和直接指针。
- 句柄方式 当使用句柄方式访问,java堆中会分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了实例对象数据和类型数据的两个具体地址信息。
- 直接指针方式 当使用直接指针方式访问,java堆对象的布局中就必须考虑如何存放访问对象类型数据的相关信息,因为reference中存储的就是对象地址。
- 优缺点
使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。
直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。
如果是对象频繁GC那么句柄方法好,如果是对象频繁访问则直接指针访问好。
3.垃圾回收和内存分配策略
1.概述
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。
2.垃圾回收期回收内存条件,对象已死
在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。怎么来判断就是比较关键的问题了。
- 引用计数法 给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。引用计数法难以解决循环引用问题。
- 可达性分析法
通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
对于可达性分析算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段:
1.如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。
2.对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。如下代码演示了一个对象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。
3.四种引用状态
在JDK1.2之前,Java中引用的定义很传统:如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过于狭隘,一个对象只有被引用或者没被引用两种状态。我们希望描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次减弱。
- 强引用 代码中普遍存在的类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用 描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java中的类SoftReference表示软引用。
- 弱引用 描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。
- 虚引用 这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用。
4.垃圾回收算法
1.引用计数法
- 引用计数法实现简单,效率较高,在大部分情况下是一个不错的算法。其就是引用计数法分析对象存活的应用,给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加1,当引用失效时,计数器减1,当计数器值为0时表示该对象不再被使用。需要注意的是:引用计数法很难解决对象之间相互循环引用的问题,现在主流Java虚拟机没有选用引用计数法来管理内存。
2.标记清除算法
标记清除算法很简单,分为标记和清除两个阶段
- 标记:从根集合(GC Root)开始扫描,每到达一个对象就会标记该对象为存活状态。
- 清除:在扫描完成之后将没有标记的对象给清除掉。 这个算法有个缺陷就是会产生内存碎片,清除掉一个对象后会留下一块内存区域,如果后面需要分配比它大的对象就会导致没有连续的内存可供使用。
3.标记整理算法
为了解决标记清除算法的内存碎片,可以优化为标记整理算法。
- 标记:从根集合(GC Root)开始扫描,每到达一个对象就会标记该对象为存活状态。
- 清除:在扫描完成之后将没有标记的对象给清除掉。
- 整理:在清除完所有内存后,对存货对象进行移动整理,使存活的对象内存和空闲的内存都在一片连续的内存上。 这个算法会产生的另外一个问题是:每次都得移动对象,因此成本很高。
4.复制算法
- 把内存等分为2块内存,分配内存的时候总是只在其中一个内存块上进行。
- 当进行回收的时候,标记所有存活对象。
- 复制所有存活对象到另一块空闲的内存里。
- 清除使用过的那块内存。 这个算法典型的是用空间换时间,不需要再对对象进行整理,两块区域交替使用,最大问题就是会导致空间的浪费,内存的使用率只有50%。
5.分代回收算法
一般情况下将堆区划分为老年代和新生代,在堆区之外还有一个代就是永久代。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收。两种类型有不同的特性,根据它们的特性来选择不同的回收算法。
1.新生代回收
默认将新生代区域按照8:1:1的比例划分为3个区域,8:Eden(伊甸园区),1:Survivor1(幸存者区),1:Survivor2(幸存者区)。
如何处理呢:
- 始终会有一块Survivor是空着的,使内存使用率是90%。
- 程序运行会在Eden和其中一块Survivor1中分配内存。
- 等到执行gc的时候,会将存活下来的对象移动到空着的Survivor2中。
- 然后在Eden和Survivor2中继续分配内存,Survivor1空着等着下次使用。 该算法提升了内存使用率,不会存在内存碎片。但是需要考虑内存分配比例,如果第一次gc时,Survivor2内存不够用的话,会把对象放到老年代区域,合理调整分配比例才能达到gc的更优处理,需要注意的是,如果Eden区分配太小,会频繁产生gc。
2.老年代回收
- 老年代的对象都是存活较久的对象,所以即使进行了gc存活率也会较高,所以采用标记清除或标记整理算法都是不错的选择。
3.永久代回收
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。
5.常见的垃圾收集器
垃圾收集器就是对垃圾回收算法的应用。
1.Serial收集器(复制算法)
特点:
- 针对新生代收集
- 采用复制算法;
- 单线程收集;
- 进行垃圾收集时,必须暂停所有工作线程,直到完成;
- 简单高效。 应用场景:
- 依然是HotSpot在Client模式下默认的新生代收集器;
- 简单高效(与其他收集器的单线程相比);
- 对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
- 在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的。
2.ParNew收集器(复制算法)
ParNew垃圾收集器是Serial收集器的多线程版本。 特点:
- 除了多线程外,其余的行为、特点和Serial收集器一样; 应用场景:
- 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
- 但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
3.Parallel Scavenge收集器(复制算法)
Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。 特点:
- 新生代收集器;
- 采用复制算法;
- 多线程收集;
- CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput); 应用场景:
- 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
- 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序; ps:吞吐量与收集器关注点说明
- 吞吐量(Throughput)
CPU用于运行用户代码的时间与CPU总消耗时间的比值;
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);
高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间; - 垃圾收集器期望的目标(关注点)
停顿时间:
1.停顿时间越短就适合需要与用户交互的程序;
2.良好的响应速度能提升用户体验;
吞吐量:
1.高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;
2.主要适合在后台计算而不需要太多交互的任务;
覆盖区(Footprint)
1.在达到前面两个目标的情况下,尽量减少堆的内存空间;
2.可以获得更好的空间局部性;
4.Serial Old收集器(标记-整理算法)
Serial Old是 Serial收集器的老年代版本。 特点:
- 针对老年代;
- 采用"标记-整理"算法;
- 单线程收集; 应用场景
- 主要用于Client模式;
- 而在Server模式有两大用途:
1.在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
2.作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用;
5.Parallel Old收集器(标记-整理)
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本。 特点:
- 针对老年代;
- 采用"标记-整理"算法;
- 多线程收集; 应用场景:
- JDK1.6及之后用来代替老年代的Serial Old收集器;
- 特别是在Server模式,多CPU的情况下;这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;
CMS收集器(标记-清理算法)
并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器。 特点
- 针对老年代;
- 基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
- 以获取最短回收停顿时间为目标;
- 并发收集、低停顿;
- 需要更多的内存;
- 是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;第一次实现了让垃圾收集线程与用户线程(基本上)同时工作; 应用场景:
- 与用户交互较多的场景;
- 希望系统停顿时间最短,注重服务的响应速度;
- 以给用户带来较好的体验,如常见WEB、B/S系统的服务器上的应用;
6.jvm类加载机制
1.概述
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
2.类加载时机
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。
以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
- 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
3.类的加载过程
1.加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:
- 如果数组的组件类型是引用类型,那就递归采用类加载加载。
- 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
- 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。 加载.class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
2.验证
是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。
- 文件格式验证
1.是否以魔数 0xCAFEBABE 开头
2.主、次版本号是否在当前虚拟机处理范围之内
3.常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
4.指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
5.CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
6.Class 文件中各个部分集文件本身是否有被删除的附加的其他信息
- ... 只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。
- 元数据验证
1.这个类是否有父类(除 java.lang.Object 之外)
2.这个类的父类是否继承了不允许被继承的类(final 修饰的类)
3.如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
4.类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)
这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。 - 字节码验证
1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(不会出现按照 long 类型读一个 int 型数据)
2.保证跳转指令不会跳转到方法体以外的字节码指令上
3.保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
4.……
这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。 - 符号引用验证
1.符号引用中通过字符创描述的全限定名是否能找到对应的类
2.在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
3.符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
4.……
最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。 符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。
3.准备
这个阶段正式为类分配内存并设置类变量初始值,内存在方法区中分配(含 static 修饰的变量不含实例变量)。需要注意的是:
- 只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
- 对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)。
public static int value = 10;
这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 10的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。
4.解析
这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
- 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。
- 假设:一个类有一个静态变量,该静态变量是一个自定义的类型,那么经过解析后,该静态变量将是一个指针,指向该类在方法区的内存地址
5.初始化
1.赋初值两种方式:
- 定义静态变量时指定初始值。如 private static String x="123";
- 在静态代码块里为静态变量赋值。如 static{ x="123"; } 注意:只有对类的主动使用才会导致类的初始化。
2.clinit 与 init
在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init。
- clinit
clinit指的是类构造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括静态变量初始化和静态块的执行。
1.如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成。
- 在执行clinit方法时,必须先执行父类的clinit方法。
- clinit方法只执行一次。
- static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定。
- init
init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。
1.如果类中没有成员变量和代码块,那么init方法将不会被生成。
- 在执行init方法时,必须先执行父类的init方法。
- init方法每实例化一次就会执行一次。
- init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块
6.卸载
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
7.双亲委派模型
从java 虚拟机角度来讲,只存在俩种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种是所有其他的类加载器,这些类加载器由java 语言实现,独立于虚拟机外部,并且全部继承自抽java.lang.ClassLoader
从Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都
会使用到以下3种系统提供的类加载器。
- 启动类加载器(Bootstrap ClassLoader):这个类将器负责将存放在JAVA_HOME\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。
- 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher $App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型的工作过程是: 如果一个类加载器接收到了类加载的请求,它首先不会自己去尝试去加载这个类,而是把这个请求委派给弗雷加载器去完成,每一个层次的类加载都是如此,因此所有的类加载请求都应该传送到顶层启动类加载器中,只有当父加载器反馈自己无法加载这个请求,子类加载器才会去尝试自己加载。
双亲委派模式优势: - 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
- 可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类,该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出java.lang.SecurityException: Prohibited package name: java.lang异常。