一、Java内存结构及分区
前置
1、符号引用:
- 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
1)符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
2)在Java中,一个java类将会编译成一个class文件。
在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
2、直接引用: 直接引用可以是
- 1)直接指向目标的指针(比如,指向"类型"【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
- 2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- 3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。
如果有了直接引用,那引用的目标必定已经被加载入内存中了。
内存结构及分区
类的符号引用,常量,放在 【方法区】的【运行时常量池】。
数组,对象,放在【堆】里。
静态变量,类信息,放在【方法区】。
基本类型数据,对象的引用,放在【栈的局部变量表】。
堆和方法区是线程共享,程序计数器和栈是线程私有。
1、程序计数器
每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,是线程私有的内存
【线程私有、指向正在执行的字节码地址、修改计数器的值来选取下一条执行指令、多线程切换后恢复通过计数器找到位置】
- 1)程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。
- 2)虚拟机的字节码解释器工作时,就是通过修改程序计数器的值,来选取下一条需要执行的字节码指令;
- 3)分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖程序计数器来完成。
- 4)Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,在一个时刻一个处理器(多核处理器的一个内核)都只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,是线程私有的内存。 - 5)如果线程正在执行的是一个Java方法,它的程序计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,它的程序计数器值为空(undefined),此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。
2、java虚拟机栈(执行Java方法)
【线程私有,生命与线程相同、方法执行内存模型(栈帧的入栈出栈)、栈帧(局部变量表、操作数栈、动态链接、方法出口)、
-
局部变量表(基本类型、对象引用、returnAddress)
1)Java虚拟机栈是线程私有的,它的生命周期与线程相同。它描述的是Java方法执行的内存模型。
2)方法执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
方法调用到执行完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
3)虚拟机栈的局部变量表,存放了编译器可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、
对象引用、returnAddress(指向一条字节码指令的地址)类型。
4)long、double类型长度64位,会占2个局部变量空间,其余的数据类型只占用1个局部变量空间。
5)局部变量表所需空间在编译期完成分配,运行期间不会改变局部变量表的大小。
6)如果线程请求的栈深入大于虚拟机允许的深度,抛出StackOverflowError;
或者可动态扩展的栈,如果扩展时无法申请到足够内存,会抛出OOM异常。 -
操作数栈
什么是操作数栈? -
1)与局部变量表一样,均以字长为单位的数组。不过局部变量表用的是索引,操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。
-
2)存储的数据与局部变量表一致含int、long、float、double、reference、returnType,操作数栈中byte、short、char压栈前(bipush)会被转为int。
-
3)数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。
-
4)java虚拟机栈是方法调用和执行的空间,每个方法会封装成一个栈帧压入占中。其中里面的操作数栈用于进行运算,当前线程只有当前执行的方法才会在操作数栈中调用指令(可见java虚拟机栈的指令主要取于操作数栈)。
-
5)int类型在-1~ 5、-128 ~ 127、-32768 ~ 32767、-2147483648 ~ 2147483647范围分别对应的指令是iconst、bipush、sipush、ldc(这个就直接存在常量池了)
3、本地方法栈(执行Native方法,线程私有)
- 1)与Java虚拟机栈类似,区别在于Java虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机执行Native方法服务。
- 2)Sun 的 HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样,本地方法栈区域也会抛出StackOverflow/OOM异常。
4、Java堆
【线程共享、存放对象和数组、垃圾回收的主要区域、回收方式分:新生代/老生代、可处于逻辑上连续物理上不连续的空间中】
- 1)Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。
- 2)用于存放对象实例、数组。(随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上渐渐变得不那么绝对了)
- 3)Java堆是垃圾收集器管理的主要区域
a:从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代、老年代;
再细致一点的有:Eden空间、From Survivor空间、To Survivor空间等。
b:从内存分配的角度看,线程共享的Java堆中能划分出多个线程私有的分配缓冲区,存储的仍然是对象实例。
进一步划分的目的是为了更好的回收内存,或者更快的分配内存。 - 4)Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
- 5)如果堆中没有内存完成实例分配,且堆无法再扩展时,抛出OOM异常。
5、方法区(存储已经加载的类信息、常量、静态变量、即时编译后的代码等数据)
【线程共享、可处于逻辑上连续物理上不连续的空间中、运行时常量池属于方法区】
-
1)线程共享的内存区域,存储已经加载的类信息、常量、静态变量、即时编译后的代码等数据
-
2)对于HotSpot虚拟机称为永久代,把GC分代收集扩展至方法区,省去专门为方法区编写内存管理代码的工作。
对于其他虚拟机不存在永久代的概念。 -
3)可以处于物理上不连续的内存空间中,只要逻辑上是连续即可。
-
4)这个区域垃圾收集行为比较少出现,该区域内存回收目标主要针对常量池的回收和对类型的卸载,但是回收和卸载条件苛刻。
-
5)方法区无法满足内存分配需求时,抛出OOM异常。
5.1、运行时常量池 【Class文件中的常量池、翻译出来的常量、动态添加的常量(String.intern())】
1)运行时常量池是方法区的一部分,Class文件中存放有常量池信息,用于存放编译期生成的各种字面量和符号引用,
在类加载后进入方法区的运行时常量池中存放。
2)运行时常量池相对于Class文件常量池的特征:
a:除了保持Class文件中描述的符号引用,还会把翻译出来的直接引用也存入运行时常量池中。
b:具有动态性,除了编译期预置入Class文件中的常量池,还可以将新的常量放入池中,比如String.intern();
3)常量池无法再申请到内存时,会抛出OOM
6、直接内存(不属于Java虚拟机规范的内存区域)
【NIO包引入的Channel、Buffer的I/O方式,直接使用Native函数,不受Java堆大小限制,但是受本机总内存大小限制,会抛OOM】
- 1)不属于虚拟机运行时数据区,非Java虚拟机规范定义的内存区域。但是也可能导致OOM
- 2)NIO(New Input/Output)中引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,该方式可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆中来回复制数据,提高性能。
- 3)本机直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制,
动态扩展超出限制时会抛出OOM
二、Java对象的创建、存储及访问
1、对象的创建过程
new 指令
-> 常量池中能否定位到一个类的符号引用
-> 该符号引用代表的类是否已加载、解析、初始化
-> 没有,则先执行响应的类加载
-> 为新生对象分配内存(对象所需内存大小在类加载完成后可以完全确定)
-> 分配的内存空间初始化为零值(不包括对象头),如果使用线程分配缓冲,初始化为零的操作提前至分配缓冲时进行
(也就是赋初始值,保证对象实例字段在Java代码中可以不赋初始值就能直接使用)
-> 对象的设置(对象的类、类的元数据、对象的哈希码、对象的GC分代年龄、是否启用偏向锁,这些存放在对象的对象头(Object Header)中)
-> 执行<init>方法(由字节码中是否跟随invokespecial指令决定),把对象按照程序员的意愿初始化
-> 完成。
1)内存分配方式:
- a:指针碰撞:Java堆中内存绝对规整,所有用过的内存都放在一边,中间放着一个指针作为分界点的指示器,分配内存就是
把指针向空闲那边挪动一段与对象大小相等的距离。 - b:空闲列表:Java堆中内存不规整,已使用的内存与空闲内存相互交错,虚拟机通过维护空闲列表(记录哪些内存块可用),
分配内存时从列表中找到一块足够大的空间分给对象实例,并更新空闲列表记录。
2)Java堆是否规整由虚拟机采用的垃圾收集器是否带有压缩整理功能决定。
3)分配内存操作并发解决方案: - a:对分配内存空间的动作进行同步处理CAS+失败重试,保证更新操作的原子性。(CAS参考AQS源码解析相关博客)
- b:把内存分配的动作按照线程划分在不同的空间中进行。每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲。
线程要分配内存时,从线程的缓冲上分配,只有缓冲用完并分配新的缓冲时,才需要同步锁定。
线程分配缓冲设置:-XX:+/-UseTLAB 来设定。
2、对象的内存布局 【对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)】
- 1)对象头:包括两部分信息
a:存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
这部分数据长度在32位-64位(未开启压缩指针)的虚拟机分别为32bit和64bit。
b:存储类型指针,即对象指向它的类元数据的指针。虚拟机通过这个指针来确定这个对象是哪个类的实例。
(但不是所有的虚拟机实现都必须在对象数据上保留类型指针,查找对象元数据不一定要经过对象本身)
ps:如果对象是一个Java数组,对象头中还必须有一块用于记录数组长度的数据。
虚拟机可以通过普通Java对象的元数据确定对象的大小,但是从数组的元数据无法确定数组大小。 - 2)实例数据:对象真正存储的有效信息,是代码中定义的各种类型的字段内容(无论父类继承的还是子类定义的,都需要记录)
a:这部分的存储会受虚拟机分配策略参数和字段在Java源码重定义顺序的影响
b:HotSpot分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)
首先,相同宽度的字段总是被分配到一起;
其次,父类中定义的变量会出现在子类之前;
最后,如果CompatFields参数值为true(默认为true),那么子类中较窄的变量可能会插入父类变量的空隙中。 - 3)对齐填充:不是必然存在的,没有特别的含义,仅仅起占位符的作用。
HotSpot虚拟机的自动内存管理系统,要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。
而对象头部正好是8字节的整数倍,因此当对象实例数据部分没有对齐时,需要通过对齐填充来补全。
3、对象的访问定位
- 1)通过栈里的reference数据来操作堆上的具体对象。
- 2)对象的访问方式取决于虚拟机的实现。主流访问堆中对象方式:使用句柄、直接指针。
a:使用句柄:Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,
句柄中包含对象实例数据与类型数据各自的具体地址信息。
b:直接指针:Java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference中存储的是对象地址。 - 3)使用句柄访问的好处是,reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象)时,
只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针访问的好处是,速度更快,节省了一次指针定位的时间开销。HotSpot使用的是直接指针访问对象。
三、Java判断对象是否存活及垃圾回收算法(GC)
(一)、对象可回收状态判断
1、引用计数算法
- 原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减一。
计数器为0的对象,可以回收。 - 缺点:难以解决对象之间互相循环引用的问题;两个对象互相引用,但没有其他引用指向这两个对象,这两个对象
已经不可能再被访问,但是它们的引用计数都不为1,通过引用计数法无法通知GC收集器回收它们。
2、可达性分析算法【主流虚拟机】
-
1)原理:通过一系列称为"GC Roots"的对象作为起始点,从GC Roots节点向下搜索,搜索走过的路径称为引用链(Reference Chain)
当一个对象到GC Roots没有任何引用链相连时,说明对象无法访问,判定为可回收对象。 -
2)Java中可作为GC Roots的对象
a:虚拟机栈(栈帧中的本地变量表)中引用的对象。【可以理解为:引用栈帧中的本地变量表的所有对象】
b:方法区中类静态属性引用的对象。 【可以理解为:引用方法区该静态属性的所有对象】
c:方法区中常量引用的对象。 【可以理解为:引用方法区中常量的所有对象】
d:本地方法栈中JNI(Native方法)引用的对象。【可以理解为:引用Native方法的所有对象】 -
理解:
a)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块
空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机
栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
b)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象
的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。
c)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的
对象也应该作为GC Roots。
d)最后一种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代
码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对
象也会被作为GC Roots。
3、几种引用类型
- 1)强引用
类似Object obj = new Object();这类的引用是强引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象。 - 2)SoftReference软引用(描述一些还有用但是非必须的对象)
用软引用关联的对象,在系统将要发生内存溢出前,会把这些对象列入回收范围中进行二次回收。 - 3)WeakReference弱引用(描述一些还有用但是非必须的对象,强度比软引用弱)
被弱引用关联的对象只能生存到下一次垃圾收集发生之前。垃圾收集器工作时,无论当前内存是否足够,
都会回收掉被弱引用关联的对象。 - 4)PhantomReference虚引用(最弱的一种引用关系)
虚引用的存在,不会对对象的生存时间构成影响,也无法通过虚引用获取对象实例。
虚引用的唯一作用就是能在这个对象被收集器回收时收到一个系统通知。
4、finalize()方法
- 1)对象在可达性分析算法分析后,发现没有与GC Roots相连的引用链,会被第一次标记并进行一次筛选,筛选的条件是
此对象是否有必要执行finalize()方法。当对象没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,
虚拟机会认为这两种情况没必要执行。 - 2)如果对象被判断为要执行finalize()方法,那么该对象将会放置在一个F-Queue队列中,在稍后由一个由虚拟机自动建立的、
低优先级的Finalizer线程去执行对象的finalize()。虚拟机会触发finalize()方法,但不保证会等待方法执行结束。
避免finalize()执行缓慢导致F-Queue中其他对象长期处于等待状态,甚至导致回收系统崩溃。 - 3)finalize()是对象避免回收的最后一次机会,在GC对F-Queue中对象进行第二次标记时,如果对象在finalize()中重新与引用链
上的任何一个对象建立关联,会把该对象移除出"即将回收"的集合。如果没有重新与引用链关联,则对象将真正被回收。 - ps:不鼓励使用finalize()
5、回收方法区
-
1)回收废弃常量
如果常量池中一个元素,在当前程序没有任何对象引用常量池中的该元素,也没有其他地方引用这个字面量
在发生内存回收时,必要的时候(这是个什么时候?),这个元素会被清理出常量池。 -
2)回收无用的类,需要同时满足三个条件
a:该类所有实例都已经被回收
b:加载该类的ClassLoader已经被回收
c:该类对应的Class对象没有被任何地方引用,无法再任何地方通过反射访问该类的方法。
满足以上条件,才可以被回收,但是也不是必然会被回收。HotSpot虚拟机提供了 -Xnoclassgc参数进行控制。
(二)、垃圾收集算法
1、标记-清除算法:
标记所有需要回收的对象,标记完成后统一回收所有被标记的对象。
标记是在上面说的,引用计数算法/可达性分析算法判定时做的。
2、复制算法【新生代】
(对象存活率低时,需要预留用来存放存活对象的空间比较小,成本低效率高,适合新生代,缺点是要浪费一部分内存空出来用于存放存活对象)
将内存按容量划分为大小相等的两块,每次只使用其中一块。当一块内存用完,就将还存活的对象复制到另一块内存,
然后把已使用过的内存空间一次性清理掉。
- 1)复制算法,多用于回收新生代(新生代对象大多"朝生夕死",存活的少,所以不用分成1:1的两块)。
将内存分为较大的Eden和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,回收时,将Eden和Survivor中
还存活的对象一次性复制到另一块Survivor中,然后清理掉Eden和使用过的Survivor空间。(HotSpot的Eden和Survivor比例是8:1) - 2)当Survivor空间不足时,需要依赖其他内存(老年代)进行分配担保。
内存分配担保,如果预留的Survivor空间不够存放上一次新生代收集下来的存活对象时,这些对象通过分配担保机制进入老年代。
3、标记-整理算法【老年代】
1)标记需要回收的对象,标记是在上面说的,引用计数算法/可达性分析算法判定时做的。
2)让所有存活对象向一端移动,然后直接清理掉端边界之外的内存。
4、分代收集算法
根据对象存活周期的不同,将内存划分为几块。
将Java堆分为新生代、老年代
新生代使用复制算法
老年代使用标记-清除或者标记-整理算法。
(三)、内存分配与回收策略
1、对象优先在Eden(新生代)分配
Eden区与一个Survivor的比例配置:XX:SurvivorRatio=8
对象优先在Eden区分配,Eden区空间不足时,虚拟机发起一次新生代GC。GC期间如果Eden区存活的对象无法全部放入Survivor区,
会通过分配担保机制提前转移到老年代。
2、大对象(需要大量连续内存空间的Java对象)直接进入老年代
- 1)常见大对象:很长的字符串、很长的数组
- 2)-XX:PretenureSizeThreshold,可以配置大于这个值的对象直接在老年代分配
避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法回收对象) - 3)经常出现大对象,容易导致内存还有不少空间就提前触发GC以便获取足够连续空间来放置大对象。
3、长期存活的对象将进入老年代
- 1)虚拟机为每个对象定义了一个对象年龄计数器,如果对象在Eden区创建,每经过一次新生代GC,且能被Survivor容纳,
会被移到Survivor空间中,且对象年龄+1。 - 2)默认对象年龄达到15岁,会被晋升到老年代。晋升年龄阈值设置:-XX:MaxTenuringThreshold
4、动态对象年龄判定(同龄对象达到Survivor空间一半的规则)
如果Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,那么年龄>=该年龄的对象直接进入老年代,
无需等到晋升年龄阈值。
5、空间分配担保
-
1)在新生代GC之前,虚拟机会先检查老年代最大可用连续空间,如果大于新生代所有对象总空间,那么新生代GC是安全的。
如果老年代最大可用连续空间小于新生代对象总空间,虚拟机会查看HandlePromotionFailure的值是否允许担保失败。
如果允许担保失败,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,
如果大于,则尝试进行一次新生代GC,尽管有风险。
如果小于,或者HandlePromotionFailure不允许担保失败,那么这是要改为进行一次FullGC。 -
PS:MinorGC(新生代GC,发生最为频繁) MajorGC(老年代GC) FullGC(整个内存GC)
四、Jvm中的常见的垃圾回收器
1. Serial 收集器(新生代)
Serial 即串行的意思,也就是说它以串行的方式执行,它是单线程的收集器,只会使用一个线程进行垃
圾收集工作,GC 线程工作时,其它所有线程都将停止工作。
使用复制算法收集新生代垃圾。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效
率,所以,它是 Client 场景下的默认新生代收集器。
显式的使用该垃圾收集器作为新生代垃圾收集器的方式:-XX:+UseSerialGC
2. ParNew 收集器(新生代)
就是 Serial 收集器的多线程版本,但要注意一点,ParNew 在单核环境下是不如 Serial 的,在多核的条
件下才有优势。
使用复制算法收集新生代垃圾。
Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与
CMS 收集器配合使用。
显式的使用该垃圾收集器作为新生代垃圾收集器的方式:-XX:+UseParNewGC
3. Parallel Scavenge 收集器(新生代)
同样是多线程的收集器,其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是
提高吞吐量(吞吐量 = 运行用户程序的时间 / (运行用户程序的时间 + 垃圾收集的时间))。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高
效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
使用复制算法收集新生代垃圾。
显式的使用该垃圾收集器作为新生代垃圾收集器的方式:-XX:+UseParallelGC
4. Serial Old 收集器(老年代)
Serial 收集器的老年代版本,Client 场景下默认的老年代垃圾收集器。
使用标记-整理算法收集老年代垃圾。
显式的使用该垃圾收集器作为老年代垃圾收集器的方式:-XX:+UseSerialOldGC
5. Parallel Old 收集器(老年代)
Parallel Scavenge 收集器的老年代版本。
在注重吞吐量的场景下,可以采用 Parallel Scavenge + Parallel Old 的组合。
使用标记-整理算法收集老年代垃圾。
显式的使用该垃圾收集器作为老年代垃圾收集器的方式:-XX:+UseParallelOldGC
6. CMS 收集器(老年代)
-
CMS(Concurrent Mark Sweep),收集器几乎占据着 JVM 老年代收集器的半壁江山,它划时代的意义
就在于垃圾回收线程几乎能做到与用户线程同时工作。
使用标记-清除算法收集老年代垃圾。
工作流程主要有如下 4 个步骤:
初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(Stop-the-world)
并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿
重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记
录,需要停顿(Stop-the-world)
并发清除: 清理垃圾,不需要停顿 -
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
-
但 CMS 收集器也有如下缺点:
吞吐量低
无法处理浮动垃圾
标记 - 清除算法带来的内存空间碎片问题
显式的使用该垃圾收集器作为老年代垃圾收集器的方式:-XX:+UseConcMarkSweepGC
7. G1 收集器(新生代 + 老年代)
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的
性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
使用复制 + 标记 - 整理算法收集新生代和老年代垃圾。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
显式的使用该垃圾收集器作为老年代垃圾收集器的方式:-XX:+UseG1GC
五、Java类加载过程
(一)、类加载时机
- 1、流程: 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
- 2、解析一般发生在初始化前,也可以发生在初始化之后,但是其他步骤是不可变的。
- 3、加载一定发生在初始化之前,而以下情况必须立即初始化,在初始化之前自然必定先完成加载
1)遇到new、getstatic、putstatic、invokestatic这4个字节码指令时,如果类没有进行过初始化,
需要先触发其初始化。
而以上4个字节码指令触发场景:new关键字实例化对象、读取/设置一个类的静态字段(被final修饰已在编译器
把结果放入常量池的静态字段除外)、调用一个类的静态方法时。
2)使用java.lang.reflect包的方法对类反射调用时,如果类没有初始化需要先触发其初始化。
3)初始化一个类时,如果发现其父类还没初始化,需要先触发其父类初始化。
4)虚拟机启动时,指定的入口函数所在的类(main()方法所在类),虚拟机先初始化这个类。
5)JDK1.7 的动态语音支持,java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、
REF_putStatic、REF_invokeStatic的方法句柄,且该方法句柄所对应的类没有初始化,先触发其初始化。
(二)、类加载过程
1、加载
-
1)通过类全名获取定义该类的二进制字节流
-
2)将字节流所代表的的静态存储结构转化为方法区的运行时数据结构
-
3)在内存中生成一个该类的java.lang.Class对象,作为方法区该类的数据访问入口
-
非数组类:可以用系统提供的类加载器完成、也可以自定义类加载器完成
-
数组类:
a:数组类本身由Java虚拟机直接创建,但是数组类的元素是由类加载器创建的。
b:数组元素是引用类型,在加载数组元素的类加载器的类名称空间上标识该数组(类加载器与类共同确定唯一性)
c:数组元素是基本类型,虚拟机会把数组标记为与引导类加载器(系统通过的类加载器)关联
d:数组类的可见性与元素类型的可见性一致,如果元素是基本类型,那么数组类的可见性默认为public
加载完成后,类的二进制流按照虚拟机所需格式存储在方法区中,并在内存中实例化一个java.lang.Class类的对象,
虽然它是对象,但是存放在方法区里。
2、验证
- 1)文件格式验证(魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围、常量池常量中是否有不被支持的类型、
指向常量的索引值中是否有执行不存在/不符合类型的常量、…) - 2)元数据验证(是否有父类、父类是否继承了不允许被继承的类(final修饰的类)、非抽象类是否实现了抽象方法/接口、
类中的字段/方法是否与父类产生冲突(覆盖父类的final字段/出现不合规则的方法重载)) - 3)字节码验证(确定语义合法、符合逻辑)
(保证操作数栈数据类型与指令代码序列能配合工作、保证跳转指令不会跳转到方法体外的字节码指令上、
保证方法体中类型转换是有效的) - 4)符号引用验证(虚拟机将符号引用转化为直接引用时触发,对常量池中各种符号引用的信息进行匹配校验)
(符号引用中的全限定名是否能找到对应的类、指定类中是否存在符合方法的字段描述及简单名称所描述的方法和字段、符号引用中的类/字段/方法是否可被当前类访问)
3、准备
- 1)为类变量(被static修饰的变量)分配内存、赋初始值,如果类变量赋值的是常量池中已经有的常量,则赋值为该常量,
否则为该类型零值。 - 2)类变量使用的内存在方法区中进行分配。
4、解析(虚拟机将常量池内的符号引用替换为直接引用的过程)
- 1)符号引用:根据该符号可以无歧义的定位到目标,比如com.example.test.Test.class;符号引用与内存布局无关。
符号引用的目标不一定已经加载到内存中。 - 2)直接引用:直接指向目标的指针、相对偏移量、一个能简介定位到目标的句柄。与内存布局相关。
如果有了直接引用,那引用的目标必定已经在内存中存在了。 - 3)解析动作
a:类/接口解析
i:非数组类型,虚拟机把符号引用全限定名传给当前代码所在类的类加载器,去加载该全限定名对应的类。
ii:数组类型,数组元素类型为对象,把符号引用的全限定名传给当前代码所在类的类加载器,加载数组元素类,
然后由虚拟机生成一个代表该数组维度和元素的数组对象。
iii:符号引用验证,确定当前类是否对符号引用所指的类有访问权限,无权限会抛IllegalAcessError异常
b:字段解析(对字段所属的类/接口的符号引用解析)
i:字段所属的类本身包含了简单名称、字段描述符与目标相匹配的字段,返回该字段的直接引用。
ii:字段所属类实现了接口,按照继承关系从下往上地柜搜索各个接口和父接口,如果接口包含了简单名称、字段描述符
与目标相匹配的字段,返回该字段的直接引用
iii:字段不是java.lang.Object,按照继承关系从下往上地柜搜索其父类,如果父类中包含了简单名称、字段描述符
与目标相匹配的字段,返回该字段的直接引用
iv:如果以上三步都没找到该字段的直接引用,抛出NoSuchFieldError异常。
v:对字段进行权限验证,如果不具备对该字段的访问权限,抛出IllegalAccessError异常
c:类方法解析
…
d:接口方法解析
…
5、初始化(执行类构造器<clinit>()方法的过程)
-
1)<clinit>()方法是编译器按源代码中出现顺序自动收集类中所有类变量(static修饰的变量)的复制动作和静态块中的语句合并产
生的。 -
2)<clinit>()方法与类的构造函数(<init>()方法)不同,它不需要显示调用父类构造器,虚拟机会保证子类的()方法执行之
前, 父类的<clinit>()方法已经执行完毕。因此虚拟机中第一个被执行的<\clinit>()方法的类一定是java.lang.Object。 -
3)父类的<clinit>()先执行,所以父类中定义的静态语句块要先于子类的变量赋值。
-
4)<clinit>()方法对于类/接口不是必须的,类中没有静态块、没有对变量的赋值操作,编译器可以不为这个类生成<clinit>()方法。
-
5)与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父类接口中定义的变量使用时,
父类接口才会初始化。接口的实现类在初始化时也不会执行该接口的<clinit>()方法。 -
6)类的<clinit>()方法是线程安全的,所以<clinit>()中的耗时操作在多线程初始化类时可能造成阻塞。
6、使用
7、卸载
六、Java类加载器(双亲委派模型)
1、类与类加载器
任何一个类,都要由加载它的类加载器和这个类本身共同确定它在Java虚拟机中的唯一性,每一个类加载器,都有一个独立 类名称空间。
所以同一个类,用不同类加载器加载,也是不同的。
2、双亲委派模型
-
1)启动类加载器(Bootstrap ClassLoader)
a:负责把存放在<JAVA_HOME>\lib目录下或被-Xbootclasspath参数指定的路径中,
能被虚拟机识别(仅按文件名识别rt.jar,名字不符合的类库不会被加载)的类库加载到虚拟机内存中。
b:该类加载器无法被应用程序直接引用,自定义类加载器时如果要把加载请求委派给它加载,直接用null代替即可。 -
2)扩展类加载器(Extension ClassLoader)
该加载器由sum.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中或者java.ext.dirs系统变量指定的路径
中所有类库。 -
3)应用程序类加载器(Application ClassLoader)/也称为系统类加载器
该加载器由sum.misc.Launcher$App-ClassLoader实现,在ClassLoader类中的getSystemClassLoader()方法返回该类加载器。
负责加载用户类路径(ClassPath)上所指定的类库,程序默认使用这个类加载器。 -
4)双亲委派模型
一个类加载器收到类加载的请求,它首先把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成 这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。
委派顺序:自定义类加载器 > ApplicationClassLoader > Extension ClassLoader > Bootstrap ClassLoader
双亲委派模型保证最终都由启动类加载器去加载类,从而保证类加载不会混乱。类,由类加载器、类本身共同确定唯一性。 -
5)双亲委派模型实现
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name);//检查是否已经加载过了 if (c == null) { long t0 = System.nanoTime(); try {//以下便是所谓的双亲委派了 if (parent != null) {//先用父加载器加载 c = parent.loadClass(name, false); } else {//没有父加载器,直接用启动类加载器加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { long t1 = System.nanoTime(); c = findClass(name);//父类加载器处理不了,就由加载器的findClass方法来加载 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
-
6)从双亲委派模型的实现可知,如果自定义类加载器,覆盖了loadClass方法,会导致双亲委派模型被破坏,
那么该自定义类加载器与启动类加载器加载同一个class得到的也不会是相同的类对象。
所以自定义类加载器,正常不要去覆盖loadClass方法,但是可以重写findClass方法。
七、Java方法数65535上限的原因
一、class类文件的结构(8位字节,一个字节占8位,以字节为基础单位的二进制流)
-
存储结构:class文件是一组以字节为基础单位的二进制流,各数据严格按照顺序紧凑排列在class文件中,中间没有任何分隔符。
需要占用一个字节(8位)以上空间的数据,会按照高位在前的方式分割成若干个字节(8位)进行存储。 -
概念:无符号数和表组成。
-
无符号数是基本数据类型,用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值。
-
表是由多个无符号数或其他表作为数据项构成的复合数据类型,用来描述有层次关系的复合结构数据。
无符号类型为基本类型,以u1、u2、u4、u8表示1个字节、2个字节、4个字节、8个字节的无符号数。
1、魔数与Class文件的版本
- 1)class文件的头4个字节为魔数,唯一作用,确定该文件是否为一个能被虚拟机接受的class文件。【class文件类型的标识】
- 2)class文件的魔数值为:0xCAFEBABE
- 3)魔数后面的4个字节为class文件的版本号,第5、6个字节是次版本号(Minor Version),第7、8个字节为主板本号(Major Version)
2、常量池(class文件结构中与其他项目关联最多的类型、占用class文件空间最大的数据之一、class中第一个出现的表)
-
1)常量池入口,有一个u2类型数据表示常量池容量,【常量池容量计数是从1开始的】。
0用来表示某些指向常量池的索引,表达不引用任何一个常量池项目,把索引值设置为0 -
2)常量池存放两大类常量:字面量、符号引用。
字面量:文本字符串、声明为final的常量值
符号引用:属于编译原理的概念
a:类和接口的全限定名
b:字段的名称和描述符
c:方法的名称和描述符虚拟机运行时,需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体内存地址中。
-
3)常量池的项目类型中有一项为 CONSTANT_Utf8_info表示UTF-8编码的字符串;
CONSTANT_Utf8_info的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称
3、访问标志
常量池后面两个字节表示访问标志,用于识别一些类、接口层次的访问信息。
包括:class是类还是接口、是否定义为public类型、是否为abstract类型、如果是类的话是否声明为final。
4、类索引、父类索引与接口索引集合
- 1)类索引、父类索引与接口索引集合都按顺序排列在访问标志之后
- 2)类索引、父类索引都是u2类型(2个字节类型)的数据,接口索引集合是一组u2类型的数据集合。
- 3)类索引用于确定类的全限定名
- 4)父类索引用于确定类的父类的全限定名
- 5)class文件中由这三项数据来确定这个类的继承关系。
5、字段表集合
- 1)字段表:用于描述接口/类中声明的变量。字段包括类级变量、实例级变量,不包括方法内部声明的局部变量。
6、方法表集合
7、属性表集合
二、dvm方法数65535上限原因
由Dalvik指令设计导致,因为在Dalvik指令集里,调用方法的invoke-kind指令中,method reference index(方法引用索引)只给了16位,最多能调用65535个方法
成员变量与构造方法执行顺序:
执行父类静态代码,执行子类静态代码
初始化父类成员变量
初始化父类构造方法
初始化子类成员变量
初始化子类构造方法