软件科技公司网站模板下载,iis添加网站主机名,什么网站可以做任务领赏金,受欢迎的唐山网站建设内存划分 如上图#xff1a;蓝色的代表线程公用#xff0c;橙色的代表线程私有
即总共可划分为五块#xff1a;
线程公用#xff1a;方法区、堆
线程私有#xff1a;程序计数器、虚拟机栈、本地方法栈
需要注意的是:JVM指的是Java内存模型#xff0c;是不存在的东西蓝色的代表线程公用橙色的代表线程私有
即总共可划分为五块
线程公用方法区、堆
线程私有程序计数器、虚拟机栈、本地方法栈
需要注意的是:JVM指的是Java内存模型是不存在的东西是概念是约定,JVM试图定义一种统一的内存模型分为工作内存和主内存能够将各种底层硬件以及操作系统的内存访问差异进行封装。
1. 方法区
当虚拟机要使用一个类时它需要读取并解析 Class 文件获取相关信息再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
需要注意的是在jdk1.8之前方法区的实现称为永久代jdk1.8及以后称为元空间,即 2. 堆
Java 虚拟机所管理的内存中最大的一块Java 堆是所有线程共享的一块内存区域在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例几乎所有的对象实例以及数组都在这里分配内存。**Java 堆是垃圾收集器管理的主要区域因此也被称作 GC 堆
JVM内存会划分为堆内存和非堆内存堆内存中也会划分为年轻代和老年代而非堆内存则为方法区(程序计数器本地方法栈Java虚拟机栈属于线程私有不在划分范围内)方法区1.8之前的实现是永久代使用堆内存1.8及之后被原空间取代不再使用JVM内存改为使用本地内存。年轻代又会分为Eden和Survivor区。Survivor也会分为FromPlace和ToPlacetoPlace的survivor区域是空的。EdenFromPlace和ToPlace的默认占比为 8:1:1
当Eden空间满了之后会触发一个叫做Minor GC就是一个发生在年轻代的GC的操作存活下来的对象移动到Survivor0区。Survivor0区满后触发 Minor GC就会将存活对象移动到Survivor1区此时还会把from和to两个指针交换这样保证了一段时间内总有一个survivor区为空且to所指向的survivor区为空。经过多次的 Minor GC后仍然存活的对象这里的存活判断是15次对应到虚拟机参数为 -XX:MaxTenuringThreshold 。为什么是15因为HotSpot会在对象投中的标记字段里记录年龄分配到的空间仅有4位所以最多只能记录到15会移动到老年代。老年代是存储长期存活的对象的占满时就会触发我们最常听说的Full GC期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。
而且当老年区执行了full gc之后仍然无法进行对象保存的操作就会产生OOM这时候就是虚拟机中的堆内存不足原因可能会是堆内存设置的大小过小这个可以通过参数-Xms、-Xmx来调整。也可能是代码中创建的对象大且多而且它们一直在被引用从而长时间垃圾收集无法收集它们 3. 程序计数器
程序计数器主要有两个作用 字节码解释器通过改变程序计数器来依次读取指令从而实现代码的流程控制如顺序执行、选择、循环、异常处理。 在多线程的情况下程序计数器用于记录当前线程执行的位置从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
⚠️ 注意 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域它的生命周期随着线程的创建而创建随着线程的结束而死亡。
4. 虚拟机栈
与程序计数器一样Java 虚拟机栈后文简称栈也是线程私有的它的生命周期和线程相同随着线程的创建而创建随着线程的死亡而死亡。除了一些 Native 方法调用是通过本地方法栈实现的(后面会提到)其他所有的 Java 方法调用都是通过栈来实现的也需要和其他运行时数据区域比如程序计数器配合。方法调用的数据需要通过栈进行传递每一次方法调用都会有一个对应的栈帧被压入栈中每一个方法调用结束后都会有一个栈帧被弹出。
栈由一个个栈帧组成而每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似两者都是先进后出的数据结构只支持出栈和入栈两种操作
5. 本地方法栈
和虚拟机栈所发挥的作用非常相似区别是 虚拟机栈为虚拟机执行 Java 方法 也就是字节码服务而本地方法栈则为虚拟机使用到的 Native 方法服务,即调用操作系统的相应本地库或API来实现功能
垃圾回收算法
1. 标记清除算法
标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象标记结束后统一回收。不足的方面就是标记和清除的效率比较低下。且这种做法会让内存中的碎片非常多。这个导致了如果我们需要使用到较大的内存块时无法分配到足够的连续内存。
2. 标记整理算法
标记过程仍然与“标记-清除”算法一样但后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都向一端移动然后直接清理掉边界以外的内存在整理存活对象时因为对象位置点变动还需要该调整虚拟机栈中的引用地址
3. 标记复制算法
它将可用内存按容量划分成两等分每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了就把存活的对象copy到另一块toPlace上然后交换指针的内容。这样就解决了碎片的问题。这个算法的代价就是把内存缩水了这样堆内存的使用效率就会变得十分低下了
4. 分代收集理论
也有人认为这算是一种思想而非算法它相当于新生代标记复制 老年代标记整体
根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中每次垃圾收集时都发现有大批对象死去只有少量存活那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保就必须使用“标记-清理”或者“标记-整理”算法来进行回收
标记算法
垃圾判断
垃圾如果一个或多个对象没有任何的引用指向它了那么这个对象现在就是垃圾
作用释放没用的对象清除内存里的记录碎片碎片整理将所占用的堆内存移到堆的一端以便 JVM 将整理出的内存分配给新的对象
垃圾收集主要是针对堆和方法区进行程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的只存在于线程的生命周期内线程结束之后就会消失因此不需要对这三个区域进行垃圾回收
在堆里存放着几乎所有的 Java 对象实例在 GC 执行垃圾回收之前首先需要区分出内存中哪些是存活对象哪些是已经死亡的对象。只有被标记为己经死亡的对象GC 才会在执行垃圾回收时释放掉其所占用的内存空间因此这个过程可以称为垃圾标记阶段判断对象存活一般有两种方式引用计数算法和可达性分析算法
引用计数法
引用计数算法Reference Counting对每个对象保存一个整型的引用计数器属性用于记录对象被引用的情况。对于一个对象 A只要有任何一个对象引用了 A则 A 的引用计数器就加 1当引用失效时引用计数器就减 1当对象 A 的引用计数器的值为 0即表示对象A不可能再被使用可进行回收Java 没有采用
优点
回收没有延迟性无需等到内存不够的时候才开始回收运行时根据对象计数器是否为 0可以直接回收在垃圾回收过程中应用无需挂起如果申请内存时内存不足则立刻报 OOM 错误区域性更新对象的计数器时只是影响到该对象不会扫描全部对象
缺点 每次对象被引用时都需要去更新计数器有一点时间开销 浪费 CPU 资源即使内存够用仍然在运行时进行计数器的统计。 无法解决循环引用问题会引发内存泄露最大的缺点
可达性分析
GC Roots
可达性分析算法也可以称为根搜索算法、追踪性垃圾收集
GC Roots 对象
虚拟机栈中局部变量表中引用的对象各个线程被调用的方法中使用到的参数、局部变量等本地方法栈中引用的对象堆中类静态属性引用的对象方法区中的常量引用的对象字符串常量池string Table里的引用同步锁 synchronized 持有的对象
GC Roots 是一组活跃的引用不是对象放在 GC Roots Set 集合
工作原理
可达性分析算法以根对象集合GCRoots为起始点从上至下的方式搜索被根对象集合所连接的目标对象
分析工作必须在一个保障一致性的快照中进行否则结果的准确性无法保证这也是导致 GC 进行时必须 Stop The World 的一个原因
基本原理 可达性分析算法后内存中的存活对象都会被根对象集合直接或间接连接着搜索走过的路径称为引用链 如果目标对象没有任何引用链相连则是不可达的就意味着该对象己经死亡可以标记为垃圾对象 在可达性分析算法中只有能够被根对象集合直接或者间接连接的对象才是存活对象
三色标记法 白色尚未访问过 灰色本对象已访问过但是本对象引用到的其他对象尚未全部访问 黑色本对象已访问过而且本对象引用到的其他对象也全部访问完成
当 Stop The World (STW) 时对象间的引用是不会发生变化的可以轻松完成标记遍历访问过程为
初始时所有对象都在白色集合将 GC Roots 直接引用到的对象挪到灰色集合从灰色集合中获取对象 将本对象引用到的其他对象全部挪到灰色集合中将本对象挪到黑色集合里面 重复步骤 3直至灰色集合为空时结束结束后仍在白色集合的对象即为 GC Roots 不可达可以进行回收 并发问题
并发标记时对象间的引用可能发生变化多标和漏标的情况就有可能发生
多标情况当 E 变为灰色或黑色时其他线程断开的 D 对 E 的引用导致这部分对象仍会被标记为存活本轮 GC 不会回收这部分内存这部分本应该回收但是没有回收到的内存被称之为浮动垃圾 针对并发标记开始后的新对象通常的做法是直接全部当成黑色也算浮动垃圾 浮动垃圾并不会影响应用程序的正确性只是需要等到下一轮垃圾回收中才被清除
漏标情况 条件一灰色对象断开了对一个白色对象的引用直接或间接即灰色对象原成员变量的引用发生了变化 条件二其他线程中修改了黑色对象插入了一条或多条对该白色对象的新引用 结果导致该白色对象当作垃圾被 GC影响到了程序的正确性 为了解决问题可以操作上面三步将对象 G 记录起来然后作为灰色对象再进行遍历比如放到一个特定的集合等初始的 GC Roots 遍历完并发标记再遍历该集合重新标记 所以重新标记需要 STW应用程序一直在运行该集合可能会一直增加新的对象导致永远都运行不完 解决方法添加读写屏障读屏障拦截第一步写屏障拦截第二三步在读写前后进行一些后置处理 写屏障 增量更新黑色对象新增引用会将黑色对象变成灰色对象最后对该节点重新扫描 增量更新 (Incremental Update) 破坏了条件二从而保证了不会漏标 缺点对黑色变灰的对象重新扫描所有引用比较耗费时间 写屏障 (Store Barrier) SATB当原来成员变量的引用发生变化之前记录下原来的引用对象 保留 GC 开始时的对象图即原始快照 SATB当 GC Roots 确定后对象图就已经确定那后续的标记也应该是按照这个时刻的对象图走如果期间对白色对象有了新的引用会记录下来并且将白色对象变灰说明可达了并且原始快照中本来就应该是灰色对象最后重新扫描该对象的引用关系 SATB (Snapshot At The Beginning) 破坏了条件一从而保证了不会漏标 读屏障 (Load Barrier)破坏条件二黑色对象引用白色对象的前提是获取到该对象此时读屏障发挥作用 以 Java HotSpot VM 为例其并发标记时对漏标的处理方案如下 CMS写屏障 增量更新G1写屏障 SATBZGC读屏障
finalization
Java 语言提供了对象终止finalization机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
垃圾回收此对象之前会先调用这个对象的 finalize() 方法(该方法继承自Object)finalize() 方法允许在子类中被重写用于在对象被回收时进行后置处理通常在这个方法中进行一些资源释放和清理比如关闭文件、套接字和数据库连接等
但是绝大多数情况下强烈不建议使用该方法因为它非常影响性能严重时会导致OOM并且在该方法内的任何异常与错误都会被直接忽视(底层源码直接catch Throwable并且不做任何处理)。
为什么说finalize非常不好严重影响性能
非常不好
FinalizationThread是守护线程finalize方法内的代码很可能还每来得及执行线程就结束了造成资源没有正确释放异常被吞如果该方法内出现异常开发者得不到任何的反馈
影响性能
重写了finalize方法的对象在第一次被gc时并不能及时释放它占用的内存因为要等着FinalizerThread调用完finalize把它从第一个unfinalized队列移除后第二次gc时才能真正释放内存可以想象gc本就因为内存不足引起的finalize调用又很慢(队列的移除操作串行执行用来释放连接类的资源也不快)不能及时释放内存对象释放不及时就会逐渐移入老年代老年代垃圾积累过多就会容易full gcfull gc后释放速度如果仍跟不上创建新对象的速度就会OOM
四种引用
无论是通过引用计数算法判断对象的引用数量还是通过可达性分析算法判断对象是否可达判定对象是否可被回收都与引用有关Java 提供了四种强度不同的引用类型 强引用被强引用关联的对象不会被回收只有所有 GCRoots 都不通过强引用引用该对象才能被垃圾回收 强引用可以直接访问目标对象虚拟机宁愿抛出 OOM 异常也不会回收强引用所指向对象强引用可能导致内存泄漏 Object obj new Object();//使用 直接赋值的方式来创建强引用软引用SoftReference被软引用关联的对象只有在内存不够的情况下才会被回收 仅可能有强引用一个对象可以被多个引用有软引用引用该对象时在垃圾回收后内存仍不足时会再次出发垃圾回收回收软引用对象及相当于获得一次免死金牌配合引用队列来释放软引用自身在构造软引用时可以指定一个引用队列当软引用对象被回收时就会加入指定的引用队列通过这个队列可以跟踪对象的回收情况软引用通常用来实现内存敏感的缓存比如高速缓存就有用到软引用如果还有空闲内存就可以暂时保留缓存当内存不足时清理掉这样就保证了使用缓存的同时不会耗尽内存 Object obj new Object();
SoftReferenceObject sf new SoftReferenceObject(obj);
obj null; // 使对象只被软引用关联弱引用WeakReference被弱引用关联的对象一定会被回收只能存活到下一次垃圾回收发生之前 仅有弱引用引用该对象时在垃圾回收时无论内存是否充足都会回收弱引用对象配合引用队列来释放弱引用自身WeakHashMap 用来存储图片信息可以在内存不足的时候及时回收避免了 OOM Object obj new Object();
WeakReferenceObject wf new WeakReferenceObject(obj);
obj null;虚引用PhantomReference也称为幽灵引用或者幻影引用是所有引用类型中最弱的一个 一个对象是否有虚引用的存在不会对其生存时间造成影响也无法通过虚引用得到一个对象为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程能在这个对象被回收时收到一个系统通知必须配合引用队列使用主要配合 ByteBuffer 使用被引用对象回收时会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存 Object obj new Object();
PhantomReferenceObject pf new PhantomReferenceObject(obj, null);
obj null;垃圾回收器
针对 HotSpot VM 的实现它里面的 GC 其实准确分类只有两大种
部分收集 (Partial GC)
新生代收集Minor GC / Young GC只对新生代进行垃圾收集老年代收集Major GC / Old GC只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集混合收集Mixed GC对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC)收集整个 Java 堆和方法区
以下是主要的垃圾收集器图表 常问CMS
1初始标记 2并发标记 3重新标记 4并发清除
其中初始标记、重新标记这两个步骤仍然需要“StopTheWorld”。 初始标记仅仅只是标记一下GCRoots能直接关联到的对象速度很快 并发标记阶段就是从GCRoots的直接关联对象开始遍历整个对象图的过程这个过程耗时较长但是不需要停顿用户线程可以与垃圾收集线程一起并发运行 而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录详见3.4.6节中关于增量更新的讲解这个阶段的停顿时间通常会比初始标记阶段稍长一些但也远比并发标记阶段的时间短 最后是并发清除阶段清理删除掉标记阶段判断的已经死亡的对象由于不需要移动存活对象所以这个阶段也是可以与用户线程同时并发的。
到jdk8为止默认的垃圾收集器是Parallel Scavenge 和 Parallel Old
从jdk9开始G1收集器成为默认的垃圾收集器 目前来看G1回收器停顿时间最短而且没有明显缺点非常适合Web应用
JVM参数与调优
JVM常见参数
参数名称含义默认值说明-Xms初始堆大小物理内存的1/64(1GB)默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时JVM就会增大堆直到-Xmx的最大限制.-Xmx最大堆大小物理内存的1/4(1GB)默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时JVM会减少堆直到 -Xms的最小限制-Xmn年轻代大小(1.4or later)注意此处的大小是eden 2 survivor space).与jmap -heap中显示的New gen是不同的。整个堆大小年轻代大小 老年代大小 持久代永久代大小.增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8-XX:NewSize设置年轻代大小(for 1.3/1.4)-XX:MaxNewSize年轻代最大值(for 1.3/1.4)-XX:PermSize设置持久代(perm gen)初始值物理内存的1/64-XX:MaxPermSize设置持久代最大值物理内存的1/4-Xss每个线程的堆栈大小DK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用 如果栈不是很深 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大需要严格的测试。-XX:NewRatio年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)-XX:NewRatio4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5XmsXmx并且设置了Xmn的情况下该参数不需要进行设置。-XX:SurvivorRatioEden区与Survivor区的大小比值设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10-XX:DisableExplicitGC关闭System.gc()这个参数需要严格的测试-XX:PretenureSizeThreshold对象超过多大是直接在旧生代分配0单位字节 新生代采用Parallel ScavengeGC时无效另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.-XX:ParallelGCThreads并行收集器的线程数此值最好配置与处理器数目相等 同样适用于CMS-XX:MaxGCPauseMillis每次年轻代垃圾回收的最长时间(最大暂停时间)如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值.
eg对于JVM内存参数配置有 -Xmx10240m - Xms10240m -Xmn5120m -XX:SurvivorRation 3, 问起最小内存和Survivor区总大小为多少
答案10g 2g
JVM常见调优
1. 调整最大堆内存和最小堆内存
-Xmx –Xms指定java堆最大值默认值是物理内存的1/4(1GB)和初始java堆最小值默认值是物理内存的1/64(1GB)) 默认空余堆内存小于40%时JVM就会增大堆直到-Xmx的最大限制.空余堆内存大于70%时JVM会减少堆直到 -Xms的最小限制。 开发过程中通常会将 -Xms 与 -Xmx两个参数配置成相同的值其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源
2. 调整新生代和老年代的比值
-XX:NewRatio — 新生代eden2*Survivor和老年代不包含永久区的比值
例如-XX:NewRatio4表示新生代:老年代1:4即新生代占整个堆的1/5。在XmsXmx并且设置了Xmn的情况下该参数不需要进行设置
3. 调整Survivor区和Eden区的比值
-XX:SurvivorRatio幸存代— 设置两个Survivor区和eden的比值
例如8表示两个Survivor:eden2:8即一个Survivor占年轻代的1/10
4. 设置年轻代和老年代的大小
-XX:NewSize — 设置年轻代大小
-XX:MaxNewSize — 设置年轻代最大值
可以通过设置不同参数来测试不同的情况反正最优解当然就是官方的Eden和Survivor的占比为8:1:1。官方推荐新生代占java堆的3/8幸存代占新生代的1/10
5. 永久区的设置
初始空间默认为物理内存的1/64和最大空间默认为物理内存的1/4。也就是说jvm启动时永久区一开始就占用了PermSize大小的空间如果空间还不够可以继续扩展但是不能超过MaxPermSize否则会OOM。
tips如果堆空间没有用完也抛出了OOM有可能是永久区导致的。堆空间实际占用非常少但是永久区溢出 一样抛出OOM。
6. JVM栈参数调优
6.1 调整每个线程栈空间的大小
JDK5.0以后每个线程堆栈大小为1M以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的不能无限生成经验值在3000~5000左右
6.2 设置线程栈大小
-XXThreadStackSize设置线程栈的大小(0 means use default stack size)类加载
1. 类的生命周期
类是在运行期间第一次使用时动态加载的不使用不加载而不是一次性加载所有类因为一次性加载会占用很多的内存加载的类信息存放于一块成为方法区的内存空间。一个类完整的生命周期如下 2. 类加载过程
Class 文件需要加载到虚拟机中之后才能运行和使用那么虚拟机是如何加载这些 Class 文件呢
系统加载 Class 类型的文件主要三步加载-连接-初始化。连接过程又可分为三步验证-准备-解析。 2.1 加载阶段
加载是类加载的其中一个阶段注意不要混淆
加载过程完成以下三件事
通过类的完全限定名称获取定义该类的二进制字节流二进制字节码将该字节流表示的静态存储结构转换为方法区的运行时存储结构Java 类模型将字节码文件加载至方法区后在堆中生成一个代表该类的 Class 对象作为该类在方法区中的各种数据的访问入口
其中二进制字节流可以从以下方式中获取
从 ZIP 包读取成为 JAR、EAR、WAR 格式的基础从网络中获取最典型的应用是 Applet由其他文件生成例如由 JSP 文件生成对应的 Class 类运行时计算生成例如动态代理技术在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码
一个非数组类的加载阶段加载阶段获取类的二进制字节流的动作是可控性最强的阶段这一步我们可以去自定义类加载器去控制字节流的获取方式重写一个类加载器的 loadClass() 方法。数组类型不通过类加载器创建它由 Java 虚拟机直接创建。
2.2 验证阶段 2.3 准备阶段
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段这些内存都将在方法区中分配。这时候进行内存分配的仅包括类变量 Class Variables 即静态变量被 static 关键字修饰的变量只与类相关因此被称为类变量而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
类变量初始化 static 变量分配空间和赋值是两个步骤分配空间在准备阶段完成赋值在初始化阶段完成 如果 static 变量是 final 的基本类型以及字符串常量那么编译阶段值方法区就确定了准备阶段会显式初始化 如果 static 变量是 final 的但属于引用类型或者构造器方法的字符串赋值在初始化阶段完成
此外需要注意
从概念上讲类变量所使用的内存都应当在 方法区 中进行分配。不过有一点需要注意的是JDK 7 之前HotSpot 使用永久代来实现方法区的时候实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。这里所设置的初始值通常情况下是数据类型默认的零值如 0、0L、null、false 等比如我们定义了public static int value111 那么 value 变量在准备阶段的初始值就是 0 而不是 111初始化阶段才会赋值。特殊情况比如给 value 变量加上了 final 关键字public static final int value111 那么准备阶段 value 的值就被赋值为 111。
2.4 解析阶段
将常量池中类、接口、字段、方法的符号引用替换为直接引用内存地址的过程
符号引用一组符号来描述目标可以是任何字面量属于编译原理方面的概念如包括类和接口的全限名、字段的名称和描述符、方法的名称和方法描述符因为类还没有加载完很多方法是找不到的直接引用直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄如果有了直接引用那说明引用的目标必定已经存在于内存之中
例如在 com.demo.Solution 类中引用了 com.test.Quest把 com.test.Quest 作为符号引用存进类常量池在类加载完后用这个符号引用去方法区找这个类的内存地址
2.5 初始化阶段
初始化阶段是执行初始化方法 clinit ()方法的过程是类加载的最后一步这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。 说明 clinit ()方法是编译之后自动生成的。 对于clinit () 方法的调用虚拟机会自己确保其在多线程环境中的安全性。因为 clinit () 方法是带锁线程安全所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞并且这种阻塞很难被发现。
对于初始化阶段虚拟机严格规范了有且只有 5 种情况下必须对类进行初始化(只有主动去使用类才会初始化类)
主动引用 当遇到 new 、getstatic、putstatic或 invokestatic这 4 条直接码指令时比如 new一个类读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量常量会被加载到运行时常量池)。当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname(...), newInstance() 等等。如果类没初始化需要触发其初始化。 初始化一个类如果其父类还未初始化则先触发该父类的初始化。 当虚拟机启动时用户需要定义一个要执行的主类 (包含 main 方法的那个类)虚拟机会先初始化这个类。 MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制而要想使用这 2 个调用 就必须先使用 findStaticVarHandle 来初始化要调用的类
被动引用所有引用类的方式都不会触发初始化称为被动引用
通过子类引用父类的静态字段不会导致子类初始化只会触发父类的初始化通过数组定义来引用类不会触发此类的初始化。该过程会对数组类进行初始化数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类其中包含了数组的属性和方法常量final 修饰在编译阶段会存入调用类的常量池中本质上没有直接引用到定义常量的类因此不会触发定义常量的类的初始化调用 ClassLoader 类的 loadClass() 方法加载一个类并不是对类的主动使用不会导致类的初始化
init
init 指的是实例构造器主要作用是在类实例化过程中执行执行内容包括成员变量初始化和代码块的执行
实例化即调用 ()V 虚拟机会保证这个类的构造方法的线程安全先为实例变量分配内存空间再执行赋默认值然后根据源码中的顺序执行赋初值或代码块没有成员变量初始化和代码块则不会执行
类实例化过程父类的类构造器() - 子类的类构造器() - 父类的成员变量和实例代码块 - 父类的构造函数 - 子类的成员变量和实例代码块 - 子类的构造函数
3.卸载
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
该类的所有的实例对象都已被 GC也就是说堆不存在该类的实例对象。该类没有在其他任何地方被引用该类的类加载器的实例已被 GC
所以在 JVM 生命周期内由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
只要想通一点就好了jdk 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 jdk 提供的类所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的所以使用我们自定义加载器加载的类是可以被卸载掉的。
4. 类加载器
从 Java 虚拟机规范来讲只存在以下两种不同的类加载器
启动类加载器Bootstrap ClassLoader使用 C 实现是虚拟机自身的一部分自定义类加载器User-Defined ClassLoaderJava 虚拟机规范将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器使用 Java 语言实现独立于虚拟机
JVM 中内置了三个重要的 ClassLoader除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
BootstrapClassLoader(启动类加载器) 最顶层的加载类由 C实现负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。ExtensionClassLoader(扩展类加载器) 主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。AppClassLoader(应用程序类加载器) 面向我们用户的加载器负责加载当前应用 classpath 下的所有 jar 包和类。
5. 类加载机制
在 JVM 中对于类加载模型提供了三种分别为全盘加载、双亲委派、缓存机制
**全盘加载**当一个类加载器负责加载某个 Class 时该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入除非显示指定使用另外一个类加载器来载入**双亲委派**某个特定的类加载器在接到加载类的请求时首先将加载任务委托给父加载器依次递归如果父加载器可以完成类加载任务就成功返回只有当父加载器无法完成此加载任务时才自己去加载**缓存机制**会保证所有加载过的 Class 都会被缓存当程序中需要使用某个 Class 时类加载器先从缓存区中搜寻该 Class只有当缓存区中不存在该 Class 对象时系统才会读取该类对应的二进制数据并将其转换成 Class 对象存入缓冲区方法区中 这就是修改了 Class 后必须重新启动 JVM程序所做的修改才会生效的原因
6. 双亲委派模型
双亲委派模型Parents Delegation Model该模型要求除了顶层的启动类加载器外其它类加载器都要有父类加载器这里的父子关系一般通过组合关系Composition来实现而不是继承关系Inheritance
工作过程每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候系统会首先判断当前类是否被加载过。已经被加载的类会直接返回否则才会尝试加载。加载的时候首先会把该请求委派给父类加载器的 loadClass() 处理因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时才由自己来处理。当父类加载器为 null 时会使用启动类加载器 BootstrapClassLoader 作为父类加载器。 AppClassLoader的父类加载器为ExtClassLoader ExtClassLoader的父类加载器为 nullnull 并不代表ExtClassLoader没有父类加载器而是 BootstrapClassLoader
双亲委派机制的优点 可以避免某一个类被重复加载当父类已经加载后则无需重复加载保证全局唯一性 Java 类随着它的类加载器一起具有一种带有优先级的层次关系从而使得基础类得到统一 保护程序安全防止类库的核心 API 被随意篡改 例如在工程中新建 java.lang 包接着在该包下新建 String 类并定义 main 函数 package java.lang;
public class String {public static void main(String[] args) {System.out.println(demo info);}
}此时执行 main 函数会出现异常在类 java.lang.String 中找不到 main 方法。因为双亲委派的机制java.lang.String 的在启动类加载器Bootstrap得到加载启动类加载器优先级更高在核心 jre 库中有其相同名字的类文件但该类中并没有 main 方法。如图
自定义类加载器
自定义加载器的话需要继承 ClassLoader 。如果我们不想打破双亲委派模型就重写 ClassLoader 类中的 findClass() 方法即可无法被父类加载器加载的类最终会通过这个方法被加载。但是如果想打破双亲委派模型则需要重写 loadClass() 方法