个人读书笔记,希望能给大家带来帮助,面试之前再看一遍。
https://blog.csdn.net/airjordon/article/details/72867397
较小的内存空间,概念模型中,可以看作当前线程所执行的字节码行号指示器,实际会有高效方式,**字节码解释器通过程序计数器的值选择下一跳需要执行的字节码指令。**在多线程中,每个线程都需要一个独立的程序计数器,各线程中的程序计数器不影响,独立存储,这类内存区域被称为“线程私有”的内存。native方法https://www.jianshu.com/p/22517a150fe5
native方法就是用java调用其他语言写的方法,java方法就是指用java写的方法,并且这个区域没有OutOfMemoryError情况,因为在程序调用中,只是改变其中的指,并不需要更大的空间。
书上没有得:
1.局部变量表:用于存放方法参数和内部定义的局部变量。局部变量表的容量是以变量槽(slot)为最小单位的,32为虚拟机中一个Slot可以存放32位以内的数据类型。
returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。
Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。
https://www.cnblogs.com/Codenewbie/p/6184898.html
2.操作数栈:
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。
操作数栈也常被称为操作栈。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:
1. begin
2. iload_0 // push the int in local variable 0 onto the stack
3. iload_1 // push the int in local variable 1 onto the stack
4. iadd // pop two ints, add them, push result
5. istore_2 // pop int, store into local variable 2
6. end
3.动态链接:Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
采用了运行时动态链接,相当于把操作系统中的把逻地址接转化物理地址。(就我理解)
4/方法出口,自然就是一个方法结束的,有正常完成出口和异常完成出口,一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。
与虚拟机栈发挥作用相似,虚拟机栈位虚拟机执行java方法服务,本地方法栈位虚拟机用到的native方法服务,Sun Hot Spot虚拟机将本地方法栈和虚拟机栈合二为一,和虚拟机栈一样本地方法栈区也会抛出栈溢出和内存泄漏异常
java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,存放所有的对象实例,这里说的对象实例就是new出来的东西,引用就是Student a; 类似a这样的叫引用。本来对象实例都应该在这里分配内存,但是由于JIT编译器的发展和逃逸分析技术优化技术会导致一些微妙的变化,导致不那么绝对了。
java堆是辣鸡收集器管理的主要区域,很多时候被称为“GC堆”,细分可分为新生代和老生代,逻辑连续即可。内存无法完成实例分配,并且堆无法再扩展的时候,就会抛出内存泄漏异常即OutOfMenoryError
和java堆一样是各个线程共享内存区域的地方,保存的是虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据,也叫非堆,在HotSpot虚拟机开发的人把这个区域称为永久代,有放弃永久代改为Native Memory来实现方法去的规划,目前1.7中,已经把放在永久代的字符串常量池移出了。(这个位置会考一个问题,要理解1.7中移除常量池又放到了哪里?)
虽然GC回收在这个区域比较少出现,到那时并不是永久存在的,这个区域的内存回收主要是针对常量池的回收和堆类型的卸载。
当方法区无法满足内存分配需求的时候,将抛出OutOfMemoryError异常。
运行时常量池时方法区的一部分,并不是内存结构中的特定一部分,是被包含,让你介绍JVM内存结构有什么东西,不要说错了。Class文件中有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池。常量池用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中
运行时常量池相对于Class文件常量池的另一个特征就是具备动态性,运行期间也可以将新的常量放入池中,利用的最多的是String的intern()方法。
会抛出OutOfMemoryError异常
不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,频繁使用,可能导致OutOfMemoryError异常出现。
1.4加入了NIO,采用通道与缓冲区的I/O方式,他用Native函数库直接分配堆外内存,通过java堆中的DirectByteBuffer对象作为内存引用进行操作,避免了在java堆和Native堆中来回复制数据。
(普通java对象,不包括数组和Class对象)
先去检查这个指令的参数是否能再常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载解析和初始化,没有就进行类加载过程。
接下来就是为新生对象分配内容,假设java堆中内存规整就,就把用过内存和空闲内存分开放,中间放一个指针作为分界点的指示器,分配内存就是将指针移动与对象大小相等的距离,这种分配方式叫做指针碰撞
如果已经使用的内存和空闲内存相互交错。虚拟机需要维护一个列表记录哪些内存可用,再分配的时候找一块足够大的空间划分给对象实例,这种分配方法叫做空闲表法。
java堆是否规整的看GC是否带有压缩整理功能,Serial(see)、ParNew等有Compact(整理)过程的收集器,系统采用分配算法就是指针碰撞,CMS这种就用的是空闲表法
对象头的设置,后面再说
到这一步,一个新的对象已经创建了,但是还没有调用init方法,所有方法字段都还为0,执行完new指令还会执行init方法,把对象按照程序员的意愿初始化,才算创建了一个可用的对象。
至于并发虚拟机采用的CAS配和失败重试的方式保证更新操作的原子性,另一种是给每个线程都在堆中预先分配一小块内存,这个区域叫做TLAB。
对象在内存中布局分为三块区域:对象头,实例数据和对齐填充
包含两部分信息,第一部分:
用于存储自身运行时数据,hashcode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据被称为Mark Wrod,例如32位虚拟机中,32bit中,25bit用以存储对象哈希码,4bit存储对象分代年龄、2bit用于存储标志位,1bit固定为0
第二部分是类型指针,即对象指向类元数据的指针,虚拟机通过这个指针来判断这个对象是哪个类的实例。如果对象是一个java数组,那么在对象头重还必须有一块用于记录数组长度的数据。
自然是无法从数组元数据确定数组的大小,才会在头重记录数组长度。
元数据是什么?
任何文件系统中的数据分为数据和元数据。数据是指普通文件中的实际数据,而元
数据指用来描述一个文件的特征的系统数据,诸如访问权限、文件拥有者以及文件数据
块的分布信息(inode…)等等。
对象真正存储的有效信息,相同宽度的字段会被分配到一起,父类定义的变量会出现在子类之前,如果CompactFields参数为true,子类中较窄的变量也可能会插入到父类的空隙中。
不是必然存在,没有特别含义,仅起着占位符的作用,由于HotSpot VM自动内存管理系统要求对象的大小必须是八字节的整数倍,而对象头部正好是8字节的倍数(1到2倍),因此对象实例数据部分没有对齐时,就通过对齐补充来补全
把内存分配动作按照线程划分在不同空间之中进程,每个线程在java堆中预先分配一小块内存,称为本地线程分配缓(TLAB),只有TLAB用完并分配新的TLAB的时候,才需要同步锁定
-XX:+/-UseTLAB
使用TLAB可以提高创建对象的速度,这篇文章写的很清楚
https://blog.csdn.net/nangeali/article/details/81866132
这个在1.7的时候已经被移动到了堆区,1.8去了元空间在本地内存
-XX:PermSize=20M -XX:MaxPermSize=20
主流访问有两种方式:使用句柄和直接指针两种方法,reference数据就是在java栈中,一个指向对象的引用。
Java堆中会划出一块内存为作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
java堆中存放到对象类型数据的指针何对象实例数据
使用句柄的好处:稳定的句柄地址,在对象被移动的时候只会改变句柄中的实例数据指针,而reference本身不用移动。
使用直接指针访问方式最大的好处就是速度更快,节省了一次指针定位的时间开销,由于对象访问十分频繁,所以可以诀曰非常可观的执行成本,HotSpot使用的是直接指针访问
程序计数器、虚拟机栈和本地方法栈随着线程生随着线程灭
Java堆和方法区不一样,这部分内存分配是动态的,只有处于运行期间时才知道会创建哪些对象。
给对象添加一个引用计数器,被引用一次就加一,引用失效的时候,计数器就减一,如果为0就是不可能再被使用了。但是主流Java虚拟机里面没有选用这个来管理内存,主要原因时它很难解决对象之间互相循环引用:
objA.instance = objB
ObjB.instance = ObjA
虽然不可能再被访问,但是相互引用导致引用计数器不为0
在主流中这个用的比较多,通过可达性分析来检测对象是否存活,这个算法的思路是,通过一系列叫做GC Roots的对象作为起始点,当GC Roots到这个对象不可达的时候,证明对象不可用。
Java中可以作为GC Roots的对象包括下面几种:
同时在回收内存的时候,需要两次标记过程,如果对象在GC Roots相连接的引用链,会被标记一次,但是如果这个时候对象被引用了,就不进行回收,如果对象还是不可以到达,就会执行回收。
1.2之后堆引用概念进行了扩充
强引用>软引用>弱引用>虚引用
强引用:类似Object obj = new Object();,只要这个引用还存在就不会回收掉被引用的对象
软引用用来描述还有用,但是非必需的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入第二次回收,1.2之后用SoftReference类实现软引用
弱引用代表不是必需的对象,它的强度比软引用弱,只能生存到下一次垃圾收集发生之前,1.2之后提供了WeakReference类实现。
虚引用被称为幽灵引用,他是最弱的一种引用关系,一个对象有没有虚引用不会对其生存时间产生影响,也无法通过虚引用来取得一个对象,这个唯一目的就是在这个对象被收集器回收的时候收到一个系统通知,PhantomReference类
(永久代在1.8已经被删除了,而且目前EE中用大部分还是1.8,类似于阿里巴巴就自己写了DragonWell JDK,基于java8和java11),所以这一部分内容看看就好了。
永久代的垃圾收集主要回收两部分,废弃常量和无用的类,回收废弃常量和回收java堆中的对象非常类似:
以常量池的字面量回收举例:
一个字符串“abc”进入了常量池,但是没有引用这个字面量,如果这个时候发生内存回收,如果必要,就会被清出常量池。常量池中的其他类、接口、方法、字段的符号引用于此类似。
如何判断一个常量是废弃常量:
该类所有的实例都已经被回收了,也就是java堆中不存在该类的任何实例
加载该类的ClassLoader已经被回收了
该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类方法。
这里说明是可以回收,不代表满足上面三个条件必定回收
可以通过堆虚拟机
算法分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后同一回收所有被标记的对象,是前面说的两次标记,这是最基础的收集算法,后续算法是基于这种思想,并对其不足进行改进的得到的。
两个不足:
标记和清除两个过程效率不高
另一个就是空间问题,由于回收之后会产生大量不连续内存碎片,空间碎片太多可能会导致以后在程序运行的时候,分配大对象时,引发另一次垃圾收集动作。
为了解决效率问题,一种称为复制的收集算法出现,将内存划为为两半,每次只用一般,然后这边用完了,就将还存活的复制到另外一块,然后清除之前的整个半区,但是将空间缩小了,代价过高.
目前商业虚拟机都采用这中算法来回收新生代,不是1:1划分,将内存分为较大的Eden和Survivor空间,每次使用Eden和其中一块Survivor。当回收的时候,将Eden和Survivor还存活的复制到另外一块Survivor空间上Eden:Survivor的大小比例是8:1每次新生代可用空间为整个容量的90%,只有10%的空间会被浪费。
但是如果幸存下来的内存容量超过新生代的10%,老年代就要进行分配担保
由于复制操作效率变低,50%的内存不能用,老年代一般不采用复制算法。
先标记,然后让标记的对象向以端移动,然后清除掉端边界以外的内存。
根据对象存活周期的不太能,将内存划分为几块,一般是分成新生代和老年代,然后根据各代采用不同的收集算法。
可作为GC Roots的结点在全局性引用(常量或类静态属性)与执行上下文(例如栈帧的本地变量表)中。
从GC找引用链这个操作太消耗时间了,而且还要GC停顿,这个分析工作需要一个能确保一致性的快照汇总进行,不能出现分析过程中对象引用关系还在发生变化的情况,不然分析结果保证性无法保证。GC时必须停顿所有Java执行线程
CMS收集器几乎不会停顿,但是枚举根节点时也是必须要停顿的
使用OopMap的数据结构来辨析哪些地方存放对象引用
安全点的概念就是前面说到使用OopMap来帮助快速且准确地完成GC Roots枚举,但是每一条指令都生成对应地OopMap需要大量额外空间,在特定地位置记录这些信息,这些位置称为安全点。程序不能在任何时候停下来GC,只能到达安全点才能暂停。
如何让线程跑到安全点上再停顿,这里有两种方案:
程序不执行没有分配CPU事件,典型地例子就是线程处于Sleep或Blocked状态,只时候线程无法响应JVM地中断请求,走到安全点中断挂起,对于这种情况需要安全区域来解决。
安全区域中是指在一段代码片段,引用关系不会发生变化,在区域地任意地方都可以GC,可以把安全区域看作安全点地扩展。
离开安全区域需要接收到可以安全离开地信号,也就是完成了GC动作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s827DHim-1583159923368)(C:\Users\李正阳\AppData\Roaming\Typora\typora-user-images\image-20200124231352651.png)]
连线代表可以搭配使用。
最基本最悠久的收集器,单线程收集器,必须暂停其他工作线程,直到收集结束。
新生代:Serial:采取复制算法
老年代:Serial Old:采取标记整理算法
客户端模式下默认使用的垃圾收集器
使用一小时暂停五分钟,没有线程交互的开销,简单高效,收集几十MB到一两百MB的新生代,停顿大概在几十MS和一百多MS以内。
ParNew是Serial收集器的多线程,除了使用多线程进程GC以外,也可以使用Serial的所有参数,收集算法、停止、对象分配规则、回收策略和Serial收集器完全一样。
ParNew和SerialOld收集器,新生代采取复制算法,老年代依旧是标记整理算法。
是Server模式下虚拟机首选的新生代收集器,除了性能最重要的原因就是可以和CMS配合工作,这款收集器就是HotSpot中第一款真正意义上的并发收集器,第一次实现了用户进程和工作进程同时工作。
这里面的并发和并行的解释:
并行:指多条垃圾收集器线程并行,但是此时用户进程处于等待状态
并发:用户进程和垃圾回收进程同时执行(单不一定是并行,可能会交替执行),用户程序在运行,垃圾收集程序运行在另一个CPU上。
使用复制算法的收集器,也是并行多线程收集器,ParNew是并发收集器,拥有自适应调节策略,ParNew没有。
这个收集器关注点是达到一个可控制的吞吐量,吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,这个收集器主要是为了高效率利用CPU时间,尽快完成程序运算任务,主要用于后台运行而不需要太多交互任务。
最大垃圾收集停顿时间:-XX:MaxGCPauseMillis参数以及直接设置吞吐量的 -XX:GCTimeRatio参数。
-XX:MaxGCPauseMillis设计的过小可能会导致收集过于频繁。
-XX:GCTimeRatio默认值为99,代表允许最大1%的垃圾回收时间
-XX:UseAdaptiveSizePolicy开关参数,打开就不需要指定
新生代的大小-Xmn
新生代中Eden和Survivor的比例-XX:SurvivorRatio
晋升老年代对象年龄-XX:PretenureSizeThreshold
会根据当前系统的运行情况收集性能监控信息,动态调整参数,这种调节方式称为GC自适应调节策略。
标记整理算法,给Client模式下的虚拟机使用,
两种用户:
作为CMS收集器的后备预案,在并发收集器发生Concurrent Mode Failure 时使用/
与Parallel Scavenge收集器搭配使用
使用多线程和标记整理算法,JDK1.6才开始提供的,之前Parallel Scavenge收集器只能和Serial Old搭配,这种搭配被Serial Old拖累无法充分利用服务器上的多个CPU。所有还不如ParNew + CMS。
直到Parallel Old收集器出现后,在注重吞吐量及CPU资源敏感的场合,“吞吐量优先”收集器可以搭配Parllel Scavenge + Parallel Old收集器搭配。
CMS收集器是一种获取最短时间回收停顿时间为目标的收集器。主要用于Java应用几种在互联网站或者B/S系统的服务端。这类应用重视响应速度,系统系统停顿时间最短,给用户带来较好的体验。
标记清除算法实现。
过程:
初始标记、重新标记需要停止系统,初始标记标记GC Roots能直接关联到的对象,速度很快。
并发标记就是 GC Roots Tracing的过程
重新标记阶段是为了修正并发标记期间因为用户程序继续运作导致标记产生变动的那一部分标记记录,停顿时间较长,但是比并发标记短。
并发标记和并发清除过程收集器线程都可以和用户线程一起工作。
所以从总体说,CMS收集器内存回收过程适合用户线程一起并发。
三个缺点:
在并发阶段,不会导致用户线程停顿,由于占用了一部分线程,会导致应用程序变慢,总吞吐量会降低,CMS默认启动的回收线程是(CPU数量 + 3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随CPU数量怎加而下降,不足4个,CMS对用户程序影响就很大,因为CPU负载本来就大,分出一半去执行收集器,为了应付这种情况,提供了一种叫做增量式并发收集器的CMS变种i -CMS让GC和用户进程交替运行,对用于程序影响不大。i - CMS以及被淘汰。
无法处理浮动垃圾,可能出现”Concurrent Mode Failure“失败导致另一次Full GC的产生,。由于清理阶段用户线程还在运行,垃圾还在产生,而且本次GC无法处理,只能留给下一次GC,这一部分垃圾被称为浮动垃圾,优化见优化CMS优化建议1.
基于标记清除算法实现,会有大量空间碎片产生。大对象分配很麻烦,所以可以优化,优化见CMS优化建议2
G1未来可以打算替换CMS收集器,但是测试了多年,面向服务端的应用垃圾收集器。
特点:
G1不在是老年代和新生代了,而是将堆划分为多个大小相等的独立区域,保留了老年代新生代的概念,但是不再是物理隔离,都是一部分区域(不需要连续)的集合
G1收集器之所以可以建立预测停顿时间模型,是因为它可以有计划的进行全区域垃圾收集,G1会跟踪每个区域里面垃圾堆积的价值大小(所获得空间以及回收所需要的时间),后台会维护一个优先列表,每次更具允许的收集时间,先回收价值最大的区域,确保了有限时间获得尽可能高的收集效率。
为了不对全堆扫描,使用了Remembered Set来避免全堆扫描,G1中每个Region区域都有一个Rememvered Set,虚拟机发现程序堆Reference类型数据写操作,会查看是否老年代对象引用了新生代对象,然后通过CardTable把引用信息记录到对象所需区域的Remembered Set中。
步骤:
初始标记和CMS一样
大多数情况,对象在新生代Eden中分配,出现空间不足,发起一次Minor GC
例子:
分配3个2Mb的对象,一个4MB的对象,新生代为10MB,老年代为10MB,然后分配,并且Eden和Survivor区为8:1,到4MB对象必定无法分配,发生GC,但是无法回收,所以只能老年代担保,然后将6MB放入老年代,Eden中有4MB
既然虚拟机采用了分代收集的思想来管理内存,为了实现这点给每个对象定义了一个对象年龄计数器,对象在Eden初始经过第一次Minor GC后仍然存活就能被Survivor容纳,对象年龄设为1,对象每在Survivor中每次熬过一次就增加一岁,默认为15岁就进入老年代。
一般没到达MaxTenuringThreshold,也能进入老年代,当Survivor相同年龄所有对象大小综合大于Survivor空间的一半,年龄大于或等于对象就可以直接进入老年代。
在发生Minor GC之前,虚拟机先检查老年代可用的连续空间是否大于新生代所有对象总空间,成立 Minor GC可以确保是安全的。不成立就查看HandlePromotionFailure设置查看是否允许担保失败。允许就检查老年代最大可用连续空间是否大于晋升到老年代对象的平均大小,大于就尝试进行Minor GC,小于或者设置不允许冒险就进行 Full GC。
冒险:新生代使用复制算法,如果Eden中对象太大无法进入Survivor,需要老年代进行担保,但是无法确定老年代是否有空间,就需要晋升到老年代平均对象容量作为经验值,与老年代比较剩余空间,决定是否进行Full GC,让老年代腾出空间。
担保失败之后,那就重新发起一次Full GC,一般会打开冒险,避免Full GC频繁。
1.6之后,不会再影响老年代分配担保,规则变为只要老年代的连续空间大于新生代总大小或者历次晋升的平均大小就进行Minor GC ,否则就Full GC
类被加载到虚拟机内存中开始到被卸载,有七个阶段:
加载、(验证、准备、解析(连接))、初始化、使用、卸载
其中验证、准备、解析被称为连接
加载、验证、准备、初始化、卸载五个阶段顺序是确定的,类加载的过程必须按照这种顺序开始。解析阶段不一定,在某些情况下可以在初始化阶段之后再开始。这些阶段通常都是互相交叉地混合式进行,通常会在一个阶段执行中调用、激活另一个阶段。
加载是虚拟机来把控的,虚拟机规范规定了5种情况必须要马上对类进行初始化(加载、验证、准备在初始化以前)
只有这5种情况才会触发初始化,所有引用类的方式都不会触发初始化,称为被动引用,有三种引用方式。
接口只会符合只有情况的第三种。
加载是类加载过程的一个阶段,加载中需要完成三件事情
通过一个类全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
数组类情况不同,数组类本身不通过类加载器创建,由java虚拟机直接创建。但是数组类与类加载器仍然有很密切的关系,因为数组的元素类型最终要靠加载器去创建,数组需要遵守的规则:
加载完成后,虚拟机外部二进制字节流按照虚拟机格式存储在方法区,也就是1.8中的元空间,然后再内存中实例化一个java.lang.Class类对象(HotSpot虚拟机放在方法区(元空间)。
加载阶段和连接阶段的部分内容(字节码文件格式验证)交叉运行的。
验证是连接阶段的第一步,目的时为了确保Class文件字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段的工作量占用了虚拟机的类加载子系统中很大一部分。
有四个阶段:
正式为类变量分配内存并设置类变量初始值的阶段
例如 public static int value = 123;
这个值在这个阶段是0,并不会执行任何java方法,只有到初始化阶段才会执行。
public static final int value = 123;
编译时javac会为value生成ConstantVale,再准备阶段就会赋值。
解析阶段是虚拟机将常量池内的符号引用转为直接引用的过程。
符号引用:用一组符号来描述引用的目标,只要能无歧义地定位到目标即可,符号引用和内存布局无关,引用的目标并不一定加载到内存中。
直接引用:直接执行目标的指针,相对偏移量或者是一个能直接定位到目标的句柄。与内存布局相关。同一个符号引用再不同虚拟机翻译出来一般不会相同,直接引用代表目标一定在内存。
虚拟机规范之中未规定解析阶段发生的具体时间,执行16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
对同一个符号引用进行多次解析请求是很常见的事件,虚拟机可以对第一次解析结果进行缓存,在运行时常量池中记录直接引用,并把常量标识未已解析状态,从而避免解析动作重复进行。无论执行了多少次解析,如果一个符号引用直接被解析了,那么后续引用请求就应当一直成功,如果第一次失败了,那么其他解析应该收到相同异常。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等棋类符号引用进行、分别对应于常量池七种常量类型
前面四种引用的解析过程,后面四种与1.7的动态语言相关,故先介绍前四种:
类或接口的解析:当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那么虚拟机完成解析过程需要以下3个步骤:
字段解析
查找成功了返回引用会对字段进行权限验证,发现不具备访问就抛出异常。
如果子类在类文件中定义父类的同名字段,就会报错。
类方法解析:
和字段解析一样,也需要解析出类方法表的class_index项中的索引的方法所属类或接口的符号引用。解析成功就用C表示这个类,接下来会进行类方法搜索
接口方法解析
同上
类初始化阶段是类加载过程的最后一步,前面类加载过程中,除了在加载阶段用户应用程序可以通过自定义加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中的定义的java代码。
初始化阶段的是执行类构造器()方法的过程,下面是()方法执行过程中一些可能会影响程序运行的特点和细节。
init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。
()方法是由编译器自动收集类中所有类变量赋值动作和静态语句块(static{})中语句合并产生的。 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
()方法与类构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。所以虚拟机中第一个被执行的()方法的类肯定是Object
由于父类()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
()方法对于类和接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成()方法。但接口与类不同的是执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类初始化的也一样不会执行接口的()。
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步。如果执行()方法地那条线程退出之后,其他线程不会再进入这个方法,同一个类加载器下,一个类型只会初始化一次
通过一个类地全限定名来获取描述此类地二进制字节流这个动作放到了java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要地类。
比较两个类是否相等,前提:两个类是由同一个类加载器加载地前提下才有意义,否则这两个类来源于同一个Class文件,被同一个虚拟机加载,但是加载他们地加载器不同,这两个类必定不相等。
这里地相等,代表Class对象地equals()方法、isAssignableFrom()方法、isInstance()方法地返回结果、也包括使用instanceof关键字做对象所需关系判断等情况。
这个位置地代码例子需要原书。
只存在两种不同地类加载器,一种是启动类加载器,这个加载器用C++语言实现地,是虚拟机自身地一部分,另一种就是所有其他的类加载器。这些类加载器由java语言实现,独立于虚拟机外部。并且全部继承抽象类java.lang.ClassLoader
系统提供的类加载器:
启动类加载器:这个类将器负责将存放再lib目录中,
扩展类加载器
应用程序类加载器
三种情况:
JDK1.2之前,这个模型在JDK1.2之后才被引入,之前还没有双亲委派模型。
自身缺陷导致的,基础类调用回用户的代码。会通过通过上下文加载器加载,也就是父类叫子类加载器去完成类加载的动作。
程序的动态性追求导致的。代码热替换、模块热部署。OSGI实现模块化热部署。每一个程序模块(Bundle)都有一个自己的类加载器,需要更换一个Bundle时,就把Bundle联通类加载器一起换掉,实现代码的热替代。
OSGI环境下,双亲委派不是树状结构,而是网状结构
java jdk1.7中的常量池确实是移到了堆中,同时在jdk1.8中移除整个永久代(方法区),取而代之的是一个叫元空间(Metaspace)的区域,如果想了。
用jdk1.6运行后会报错,永久代这个区域内存溢出会报:
Exception in thread “main” java.lang.OutOfMemoryError:PermGen space的内存溢出异常,表示永久代内存溢出。
在Java7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。但是在之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。Java7中已经将运行时常量池从永久代移除,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。而在Java8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域被叫做元空间。
https://blog.csdn.net/weixin_35663229/article/details/52796157
移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
https://blog.csdn.net/qq_16681169/article/details/70471010
它的大小是在启动时固定好的——很难验证并进行调优。-XX:MaxPermSize
HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,非强类型,难以跟踪调试,需要存储元数据的元数据信息。
简化垃圾回收:对每一个回收集使用专门的元数据迭代器。
可以在GC不进行暂停的情况下并发地释放类数据。
使得原来受限于持久代的一些改进未来有可能实现
这篇文章写的太好了,推荐阅读
https://blog.csdn.net/qq_16681169/article/details/70471010
摘抄:
Parallel Scavenge没有使用原本HotSpot其它GC通用的那个GC框架,所以不能跟使用了那个框架的CMS搭配使用。更底层的原因在于原文之中:
原文链接:https://blog.csdn.net/qq_33915826/article/details/79672772
-XX:CMSInitiatingOccupancyFraction 68 就是当老年代使用了68%的时候再GC,在中老年代增强不快,可以调高这个值得参数,降低回收得次数,提高性能,但是设置过高,会导致Concurrent Mode Failure,然后启动Serial Old收集器,大量Concurrent Mode Failure性能反而降低。
-XX:+UseCMSCompactAtFullCollection (默认开启)用于CMS因为大对象要FullGC得时候开启内存碎片得合并整理过程
-XX:CMSF7890 .吗,/9ullGCsBeforeCompaction 这个参数用于设置执行多少次不压缩Full GC之后执行一次压缩得的.
-XX:+PrintGCdetails打印收集器日志参数,发生垃圾回收的时候打印内存回收日志,并且在内存退出的输出当前内存区域分配情况。
-XX:MaxTenuringThreshold = 年龄设置
-XX:+HandlePromotionFailure
允许就检查老年代最大可用连续空间是否大于晋升到老年代对象的平均大小,大于就尝试进行Minor GC,小于或者设置不允许冒险就进行 Full GC。
名称 | 主要作用 |
---|---|
jps | 显示指定系统内所有的HotSpot虚拟机进程 |
jstat | 用于收集HotSpot虚拟机各方面的运行数据 |
jinfo | 显示虚拟机配置信息 |
jmap | 生成虚拟机的内存转储快照(heapdump文件) |
jhat | 用于分析heapdump文件,会建立一个HTTP/HTML服务器,让用户在浏览器上查看分析结果 |
jstack | 显示虚拟机线程快照 |
https://www.cnblogs.com/lizhonghua34/p/7307139.html
-XX:+HeapDumpOnOutOfMemoryError,可以让虚拟机出现OOM异常出现之后自动生成dump文件。
-Xverify:none