在JVM中表示两个class对象是否为同一个类存在两个必要条件:
类的完整类名必须一致,包括包名。
加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
换句话说,在JvM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
分析内存的常用工具
LeakCanary
Java虚拟机
线程私有内存区 = 程序计数器+虚拟机栈+本地方法栈。
虚拟机栈:每一个方法执行的同时都会创建一个栈帧(有局部变量表、方法返回地址等信息),并将栈帧压栈,当方法执行完毕之后,便会将栈帧移除栈。由此可知,线程当前执行方法对应的栈帧比定位于Java的栈顶。使用递归方法的时候容易崩栈就是这个原因。
共享数据区=常量池+方法区+堆。
常量池存放字面量等。
方法区存放ClassLoader 、编译后的代码。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
堆存放对象的实例、数组的内容,是GC的主战场。如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常。
1、类加载过程
1.1、JVM会先去方法区中找到没有相应类的.class存在。如果有,就直接使用;如果没有就把相关的.class加载到方法区。
1.2、在.class加载到方法区时,会分为两个部分加载:先加载非静态内容,再加载静态内容。
1.3、加载非静态内容:把.class中的所有非静态内容加载到方法区下的非静态区域内
1.4、加载静态内容:
1.4.1、把.class中的静态内容加载到方法区下的静态区域内
1.4.2、静态内容加载完成之后,对所有静态变量进行默认初始化
1.4.3、所有静态变量默认初始化完成之后,再进行显示初始化
1.4.4、当静态区域下的所有静态变量显示初始化后,执行静态代码块
1.5、当静态区域下的静态代码块,执行完之后,整个类的加载就完成了。
2、对象创建过程
2.1、在堆内存中开辟一块空间
2.2、给开辟空间分配一个地址
2.3、把对象的所有非静态成员加载到所开辟的空间下
2.4、所有非静态成员变量默认初始化完成之后,调用构造函数
2.5、所有非静态变量默认初始化完成之后,调用构造函数
2.6、在构造函数入栈时,分为两部分:先执行构造函数中的隐式三式,再执行构造函数中书写的代码
2.6.1、隐式三步
2.6.1、执行super语句
2.6.2、对开辟空间下的所有非静态成员变量进行显式初始化
2.6.3、执行构造代码块
2.6.2、在隐式三步执行完之后,执行构造函数中书写的代码
2.7、在整个构造函数执行完并弹栈后,把空间分配的地址赋值给一个引用对象
GC垃圾回收器
引用计数算法
如上图对象A和对象B相互引用,导致他们的引用计数器都不为0,那么垃圾收集器就永远不会回收他们。
可达性分析算法
可作为GC ROOT的对象:
虚拟机栈(栈帧中的局部变量表)中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中Native引用的对象
垃圾收集算法
-
标记清除算法
标记-清楚算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象进行标记,玩标记完毕后,再扫描整个空间未被标记的对象进行回收,如下所示:
标记-清除算法的主要不足有两个:
- 效率问题:标记和清除两个过程效率都不高;
空间问题:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
-
标记整理算法
标记整理算法的标记过程类似标记清楚算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代),其原来如下:
标记整理算法与标记清除算法最显著的区别是:标记清除算法不进行对象的移动,并且仅对不存活对象进行处理;而标记整理算法会将所有的存活对象移动到一端,并对不存货对象进行处理,因此其不会产生内存碎片。标记整理算法如下:
-
复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。该算法如下:
事实上,现在商用的虚拟机都采用这种算法来回收新生代。因为研究发现,新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。
分代收集算法
对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对昂也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况是不一样的),故而不同声明周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高JVM的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就采用标记清楚算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久带三个模块。
- 新生代
新生代的目标是尽可能快速收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。- 老年代
老年代存放的都是一些生命周期长的对象,就像上面的所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。- 永久代
永久代主要用于存放静态文件,如Java类、方法等。
大对象直接进入老年代。所谓的大对象是值,需要大量的连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认是15)的Minor GC后,就会晋升到老年代中。
回收也和引用类型有关系
对象引用的四个方式:强、软、弱、虚。
虚引用:跟踪GC,不关心对象生命周期。不可以通过get方法取出对象,可以通过引用队列知道是否被回收。
弱引用:跟踪GC,关心对象生命周期。可以通过get方法取出对象,可以通过引用队列知道是否被回收。
软引用:内存不足就回收,存放不重要的对象,可以在需要的时候重新加载。
强引用:Object obj=new Object();
软引用和弱引用完全相同,但它不急于释放它所引用的对象。在下一个垃圾回收周期来临时,弱引用对象将被回收,但软引用通常会保留一段时间。