java基础篇(五)

一、 jvm

jvm体系结构
在这里插入图片描述
1.Class Loader类加载器
负责加载 .class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定。
  ① 定位和导入二进制class文件
  ② 验证导入类的正确性
  ③ 为类分配初始化内存
  ④ 帮助解析符号引用.
  
2.Native Interface本地接口
  本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体作法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。
  目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见。
  因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等。
  
3.Execution Engine 执行引擎:执行包在装载类的方法中的指令,也就是方法。

4.Runtime data area 运行数据区
从整个计算机内存中开辟一块内存存储Jvm需要用到的对象,变量等,分为:方法区,堆,虚拟机栈,程序计数器,本地方法栈。

jvm内存结构
运行时数据区主要包括以下几个数据区域:虚拟机栈、本地方法栈、程序计数器(PC寄存器)、堆、方法区。

程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。

如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。

程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。

虚拟机栈是java 方法执行的数据区域,每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。

每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

(局部变量表:存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间)。

栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的。

线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常。

虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。

本地方法栈是本地方法执行的数据区域,即native方法。和虚拟机栈类似。区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务

方法区用于存储虚拟机加载的:静态变量+常量+类信息+运行时常量池 (类信息:类的版本、字段、方法、接口、构造函数等描述信息 )

默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小

对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

是jvm中内存占用最大的一块数据区域,几乎所有new出来的对象都存储堆中,是线程共享的,是GC的主要管理对象。又可分为新生代、老年代。新生代又可细分为Eden区和2个Survivor区。

直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存。

JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。

由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

二、垃圾回收算法
转载至jvm垃圾回收

1.如何计算对象已死

1.1 引用计数器算法
引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。

引用计数器实现简单,效率高;但是不能解决循环引用问问题(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后,这个算法已经不再使用了。

1.2 可达性分析算法
可达性分析算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的,如下图所示。
在这里插入图片描述
GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中的引用的对象。
方法区域中的类静态属性引用的对象。
方法区域中常量引用的对象。
本地方法栈中JNI(Native方法)的引用的对象。

上面只是标记了对象是否可以被回收,实际上在java中首先会标记下对象,会调用对象里面的protected void finalize()这个方法,这个时候对象还有救,只要在这个方法把该对象和引用链对接上,其实可以逃脱被回收。
2. 回收的区域
新生代
永久代
新生代,这个很容易理解,一般来说永久带是最不好回收的。永久代主要回收以下部分内容:
废弃常量
无用的类
3. 垃圾回收算法
3.1 标记—清除算法
标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。

标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。如下图所示:
在这里插入图片描述
3.2 复制算法
复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。这种方式听上去确实是非常不错的方案,但是总的来说对内存的消耗十分高。
复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是1:1(大概是8:1),也就是常提到的一块Eden(80%)和两块Survivor(20%)。当然也会存在10%不够用的情况,这个后面在进行梳理,会有一个补偿机制,也就是分配担保
在这里插入图片描述
3.3 标记—整理算法
复制收集算法会存在一种极端情况,就是对象都没死。这种情况会在老年代有几率的出现,所以根据老年代的特点提出了标记—整理算法。 标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存,如下图所示:

在这里插入图片描述
3.4 分代收集
分代收集是根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用标记—复制算法,老年代采用标记—整理算法。

垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。上面介绍的只不过是基本思想。

三、垃圾收集器
在这里插入图片描述
上面是目前比较常用的垃圾收集器,和他们直接搭配使用的情况,上面是新生代收集器,下面则是老年代收集器,这些收集齐都有自己的特点,根据不同的业务场景进行搭配使用。

收集器

1.1 Serial收集器
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)也就是传说中的Stop The World。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

1.2 ParNew收集器
ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。 使用方式可以使用-XX:+UseConcMarkSweepGC,或者是使用-XX:+UseParNewGC来强制开启,可以通过-XX:ParallelGCThreads 来调整或者限制垃圾收集的线程数量。

1.3 Parallel Scavenge收集器
Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
Parallel Scavenge提供了两个参数用来精确控制,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间
Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

1.4 Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中详细讲解。

1.5 Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集器无法与CMS收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

1.5 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
CMS是一款基于“标记—清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

1.6 G1收集器
G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

2 总结
各个收集器都有自己适应的业务场景,一般来说在生产中都会使用ParNew+CMS的方式分代收集回收。针对于新生代来说,根据其特性,总的来说还是使用复制算法。而针对于老年代来说算法一般会使用两种,标记-清除和标记-整理,每一种都有自己适用的场景。但是在目前我遇到的生产中还是使用CMS 标记清除的这种比较多,也是比较常用的,虽然标记-清除这种算法会导致大量的内存碎片,但是也是可以通过一种暴力的方式解决,就是FULL GC,G1收集器是比较完美的一种收集器,但是在目前的生产中依旧用得很少。

四、jvm参数详解
参数详解

五、jvm调优
jvm参数调优

六、 类加载
转载至:类加载

什么是类的加载
在这里插入图片描述
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

1、在什么时候才会启动类加载器?

其实,类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

2、从哪个地方去加载.class文件

在这里进行一个简单的分类。例举了5个来源

(1)本地磁盘

(2)网上加载.class文件(Applet)

(3)从数据库中

(4)压缩文件中(ZAR,jar等)

(5)从其他文件生成的(JSP应用)

类加载的过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:
在这里插入图片描述
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

下面就一个一个去分析一下这几个过程。

1、加载

”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:

(1)通过一个类的全限定名来获取其定义的二进制字节流

(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构

(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。

相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。我们在最后一部分会详细介绍这个类加载器。在这里我们只需要知道类加载器的作用就是上面虚拟机需要完成的三件事,仅此而已就好了。

2、验证

验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:

(1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。

(2)元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。

(3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。

(4)符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。

对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。

3、准备

准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:

(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,

(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如

public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。

当然还有其他的默认值。
在这里插入图片描述
注意,在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。

4、解析

解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?

符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

5、初始化

这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。

在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

①声明类变量是指定初始值

②使用静态代码块为类变量指定初始值

JVM初始化步骤

1、假如这个类还没有被加载和连接,则程序先加载并连接该类

2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(如 Class.forName(“com.shengsiyuan.Test”))
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类
好了,到目前为止就是类加载机制的整个过程,但是还有一个重要的概念,那就是类加载器。在加载阶段其实我们提到过类加载器,说是在后面详细说,在这就好好地介绍一下类加载器。

类加载器

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。

1、Java语言系统自带有三个类加载器:

Bootstrap ClassLoader :最顶层的加载类,主要加载核心类库,也就是我们环境变量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
Extention ClassLoader :扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
Appclass Loader:也称为SystemAppClass。 加载当前应用的classpath的所有类。
我们看到java为我们提供了三个类加载器,应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。这三种类加载器的加载顺序是什么呢?

Bootstrap ClassLoader > Extention ClassLoader > Appclass Loader

一张图来看一下他们的层次关系
在这里插入图片描述
2、类加载的三种方式

认识了这三种类加载器,接下来我们看看类加载的三种方式。

(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。

(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。

(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。

下面代码来演示一下

首先我们定义一个FDD类

public class FDD {
static { System.out.println("我是静态代码块。。。。"); 
}
}

然后我们看一下如何去加载

package com.fdd.test;
public class FDDloaderTest { 
public static void main(String[] args) throws ClassNotFoundException { 
ClassLoader loader = HelloWorld.class.getClassLoader(); System.out.println(loader); //一、 使ClassLoader.loadClass()来加载类,不会执行初始化块 
loader.loadClass("Fdd"); 
//二、 使用Class.forName()来加载类,默认会执行初始化块 
Class.forName("Fdd"); 
//三、使用Class.forName()来加载类,指定ClassLoader,初始化时不执行静态块 
Class.forName("Fdd", false, loader); 
} 
}

上面是同不同的方式去加载类,结果是不一样的。

3、双亲委派原则

他的工作流程是: 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。这个理解起来就简单了,比如说,另外一个人给小费,自己不会先去直接拿来塞自己钱包,我们先把钱给领导,领导再给领导,一直到公司老板,老板不想要了,再一级一级往下分。老板要是要这个钱,下面的领导和自己就一分钱没有了。(例子不好,理解就好)

采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:

可以避免重复加载,父类已经加载了,子类就不需要再次加载
更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
4、自定义类加载器

在这一部分第一小节中,我们提到了java系统为我们提供的三种类加载器,还给出了他们的层次关系图,最下面就是自定义类加载器,那么我们如何自己定义类加载器呢?这主要有两种方式

(1)遵守双亲委派模型:继承ClassLoader,重写findClass()方法。

(2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。

我们看一下实现步骤

(1)创建一个类继承ClassLoader抽象类

(2)重写findClass()方法

(3)在findClass()方法中调用defineClass()

代码实现一下:

public class MyClassLoader extends ClassLoader {
private String libPath; 
public DiskClassLoader(String path) { libPath = path; } @Override 
protected Class<?> findClass(String name) throws ClassNotFoundException { 
String fileName = getFileName(name); 
File file = new File(libPath,fileName); 
try { 
FileInputStream is = new FileInputStream(file); 
ByteArrayOutputStream bos = new ByteArrayOutputStream(); 
int len = 0; 
try { 
while ((len = is.read()) != -1) { 
bos.write(len); 
} } catch (IOException e) {
 e.printStackTrace(); 
 } 
 byte[] data = bos.toByteArray(); 
 is.close(); 
 bos.close(); 
 return defineClass(name,data,0,data.length); 
 } catch (IOException e) { 
 e.printStackTrace();
  } 
  return super.findClass(name);
   } 
   //获取要加载 的class文件名 
   private String getFileName(String name) { 
   int index = name.lastIndexOf('.'); 
   if(index == -1){
    return name+".class"; 
    }else{ 
    return name.substring(index+1)+".class"; } 
    }
    }

接下来我们就可以自己去加载类了,使用方法大体就是两行

MyClassLoader diskLoader = new MyClassLoader("D:\\lib");//加载class文件,注意是com.fdd.TestClass c = diskLoader.loadClass("com.fdd.Test");

七、 对象的内存布局是怎样的?

答:对象的内存布局包括三个部分:对象头,实例数据和对齐填充。

对象头:对象头包括两部分信息,第一部分是存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁等等。第二部分是类型指针,即对象指向类元数据的指针。

实例数据:就是数据啦

对齐填充:不是必然的存在,就是为了对齐的嘛

八、对象是如何定位访问的?
答:对象的访问定位有两种:句柄定位和直接指针
句柄定位:Java 堆会画出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
在这里插入图片描述
直接指针访问:java堆对象的不居中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
在这里插入图片描述
比较:使用直接指针就是速度快,使用句柄reference指向稳定的句柄,对象被移动改变的也只是句柄中实例数据的指针,而reference本身并不需要修改。

九、 引用的分类?
强引用:通过new出来的引用,只要强引用还存在,则不会回收。

软引用:通过SoftReference类来实现,用来描述一些有用但非必须的对象。在系统将要发生内存溢出异常之前,会把这些对象回收了,如果这次回收还是内存不够的话,才抛出内存溢出异常。

弱引用:非必须对象,通过WeakReference类来实现,被弱引用引用的对象,只要已发生GC就会把它干掉。

虚引用:通过PhantomReference类来实现,无法通过徐引用获得对象的实例,唯一作用就是在这个对象被GC时会收到一个系统通知。
万物皆对象

发布了16 篇原创文章 · 获赞 12 · 访问量 160

猜你喜欢

转载自blog.csdn.net/qq_36435947/article/details/104856303