目录
一、运行时内存结构
1. 内存结构概述
2. 内存泄漏和内存溢出
二、常量池
1. 静态常量池
2. 运行时常量池
3. 字符串常量池
4. 常量池的好处
三、类加载机制
1. JAVA中类加载的过程
2. 类加载机制--双亲委派机制
3. 类加载的顺序
四、Java对象的创建到消亡
1. Java对象的创建方式
2. Java创建对象的步骤
3. 对象的整个生命周期
4. JVM如何判断一个对象可以被回收(判断对象存活)
5. 哪些对象可作为GC Root
五、垃圾回收机制
1. 垃圾回收机制概述
2. GC发生在哪个部分
3. 垃圾收集算法
1. 标记-清除
2. 复制算法
3. 标记-整理
4、分代收集算法(重)
4. 分代的知识
1. 年轻代:Eden区、survivor1区、survivor2区
2. 年老代(Tenured区),存放超过15次循环的对象
3. 永久代,jdk8之后已经取消,用元空间代替
5. 垃圾回收器
1. CMS垃圾回收器
2. G1垃圾回收器
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
jdk1.8之前:
jdk1.8:
线程私有的:
线程共享的:
堆:是所有线程共享的一块内存,java虚拟机启动时创建,会经常发生垃圾回收操作。
方法区:JVM只有一个方法区,被所有线程共享! 方法区实际也是堆。
直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
程序计数器:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
虚拟机栈:栈是运行时创建,是线程私有的,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储存储局部变量、操作数、方法出口等。每一个方法从调用到结束,就对应着一个栈帧的入栈到出栈的过程。
本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 jvm调用操作系统方法所使用的栈。
内存溢出(out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个Integer,但给它存了Long才能存下的数,那就是内存溢出。 (你要求分配的内存超出了系统能给你的)
内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
内存泄露最终会导致 out of memory!
即.class文件中的常量池,不仅仅包含字符串字面量,还包含类、方法的信息,占用.class文件绝大部分空间。
主要存放两大类常量:
字面量:相当于JAVA语言层面常量的概念,如文本字符串,final的常量值
符号引用量:类和接口的全限定名(包名+类名),字段名称和描述符,方法名称和描述符
是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于class文件常量池的另外一个重要特征是具备「动态性」,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals快。(对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
类加载的步骤为,加载->验证->准备->解析->初始化
1、第一步:加载
通过类的全限定名(包名+类名),获取到该类的.class文件的二进制字节流
将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构
在内存中生成一个代表该类的java.lang.Class字节码对象,作为方法区这个类的各种数据的访问入口
2、第二步:链接
链接是讲上面创建好的class类合并至java虚拟机中,分为验证、准备、解析三个阶段。
验证:确保class文件中的字节流信息符合当前JVM的要求,不会危害到JVM的安全。
准备:准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。静态变量独立于类的实例而存在,所以在类加载的时候,跟随类一起进行初始化。
解析:解析阶段是虚拟机将常量池内的符号引用(就是对于类、变量、方法的描述,存放在静态常量池中)替换为直接引用(直接指向目标的指针)的过程。
3、第三步:初始化
初始化是整个类加载过程的最后一个阶段。整个类加载的五个阶段只有加载和初始化是开发者可以参与的,因此初始化阶段也是需要重点关注的阶段。
简单来说初始化阶段就是执行类的构造器方法init()的过程。若该类具有父类,jvm会保证父类的init先执行,然后在执行子类的init。
jvm中对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会把它的class文件加载到内存中产生class对象。
在加载类的时候,使用的是双亲委派机制,即把请求交给父类处理的一种任务委派模式
工作原理:
如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。
如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader
如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式
一些打破双亲委派机制的例子
Tomcat:应用的类加载器优先自行加载应用目录下的class,并不是先委派给父加载器,加载不了才委派给
父加载器。打破的目的是为了完成应用间的类隔离。
1.首先加载父类的静态字段或者静态语句块
2.子类的静态字段或静态语句块
3.父类普通变量以及语句块
4.父类构造方法被加载
5.子类变量或者语句块被加载
6.子类构造方法被加载
1、使用new关键字来调用一个类的构造函数来显式的创建对象
2、反射机制--Class类的newInstance、Constructor类的newInstance
3、clone()
Object根类提供了clone方法,如果要使用一个对象的clone()方法,必须实现Cloneable接口,这个接口没有任何实现,跟Serializable一样是一种标志性接口
1、判断对象对应的类是否被加载完成
当JVM执行到new关键字时,首先会去运行时常量池中定位这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过,如果没有被加载,那么会进行类的加载过程,如果已经被加载,那么进行下一步。
2、为对象分配内存空间
内存分配的两种方式:
1、指针碰撞
java堆中的内存是绝对规整的,所有用过的内存放在一边,空闲的放在一边,中间有指针作为分界点指示。这时分配内存就是把指针向空闲空间移动一段与对象大小相等的距离。
2、空闲列表
java堆中的内存不是规整的,已使用的和未使用的相互交错,虚拟机就必须维护一个列表,记录哪些内存块可用,分配的时候找到一块足够大的空间划分给对象实例,并更新表的记录。JVM默认使用的是指针碰撞的方式。而空间是否规整与垃圾收集器有关(比如CMS垃圾回收器不带空间整理功能)
3、处理内存分配的并发问题(线程安全)
4、为分配到的内存空间初始化零值(不包括对象头)
5、设置对象头信息(包括对象所属的类,类的元数据信息,GC分代年龄)
6、执行构造方法init方法
1、判断类有没有被加载
2、创建对象
3、创建栈帧:创建线程的同时会创建一个虚拟机栈,跟踪线程运行中一系列方法调用的过程,每调用一个方法就创建并压入一个栈帧。
4、判断对象存活 (引用计数法、根结点回溯法)
5、JVM GC
Student stu = new Student("zhangsan");
对应到JVM中,有三个地方存储信息
1、将Student类加载到方法区,“zhangsan”存在于方法区中的字符串常量池。
2、在堆区为Student实例对象开辟内存空间存储。
3、JVM开启线程运行时,会创建一个虚拟机栈,stu存在于虚拟机栈中。
判断一个对象是否可以被回收,最重要的是判断它是否还在被使用,只要没有被使用,就可以回收。
主要有两种算法:
1、引用计数器:为每一个对象添加一个引用计数器,用来统计指向当前对象的一个引用次数,如果当前对象存在引用的更新,那么计数器就会增加。当引用计数器变为0,那么它就可以被回收了。虽然这种方法需要额外空间来存储引用计数器,但它的实现比较简单效率也比较高,不过主流JVM都没有采用这种方式,因为它处理某些循环或者相互依赖的情况下(A引用B,B也引用A),出现即使不再使用但是也无法回收的内存。
2、可达性分析:首先判断一系列肯定不能回收的对象,作为GC Root,比如虚拟机栈里的一个引用对象以及本地方法在里面的一些对象。以GC Root为起始节点,从这些节点开始向下去搜索,去寻找它的直接或间接的引用对象,遍历完后,如果发现有一些对象是不可达的,那么就认为这些对象没有引用了,需要回收。
在回收的时候,JVM首先会找到所有的GC Root,这个过程会暂停用户的所有线程,即stop the world,再从GC Root这些根节点向下搜索,不可达的被回收,是目前主流JVM使用的一个算法。
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终将导致内存溢出,所以对内存资源的管理非常重要。垃圾回收就是对这些无效资源的处理,是对内存资源的管理。
在Java中,对象所占用的内存在对象不再使用后会自动被回收。这些工作是由一个叫垃圾回收器 Garbage Collector
的进程完成的。
1、判断是否已经被回收
重写Object类中的finalize方法,这个方法在垃圾收集器执行的时候,被收集器自动调用执行的
2、没有被回收的话,判断是否可以被回收
引用计数法、终点可达法
3、通知垃圾回收器回收
调用System类的静态方法gc(),通知垃圾收集器去清理垃圾,具体的执行时间取决于垃圾收集的算法
4、执行回收
GC主要发生在堆中,堆区由所有线程共享,在虚拟机启动时创建。
堆区主要用于存放
「对象实例」
「数组」
「所有new出来的对象」
线程私有区域【JVM虚拟栈,本地方法栈,程序计数器】不需要进行垃圾回收因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。
分为“标记”和“清除”两个阶段:
效率不高,标记清除后会产生大量不连续的内存碎片。
复制算法是为了解决「效率」问题而出现的
这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。
缺点体现在
标记-整理算法过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。
当前大多数垃圾收集都采用的分代收集算法,根据对象存活周期的不同将内存划分为几块,每一块使用不同的上述算法去收集。
在jdk8以前分为三代:年轻代、老年代、永久代。在jdk8以后取消了永久代的说法,而是元空间取而代之。
年轻代--使用复制算法(对象存活率低)
老年代--使用标记-整理算法(对象存活率高)。
尽可能快速的收集掉那些生命周期较短的对象,一般情况下新生成的或者朝生夕亡的对象都是放在新生代里面
Minor GC:清理年轻代区域,Eden区满了后会触发一次
当回收时,先将Eden区存活对象复制到一个s0区,然后清空Eden区,存活的对象年龄+1;当这个s0区也存放满了时,则将Eden区和s0区存活对象复制到另一个s1区,然后清空Eden和这个s0区,存活的对象年龄+1;此时s0区是空的,然后将s0区和S1区交换,即保持s0区为空(此时的so是原来的s1区),如此往复。年轻代执行的GC是Minor GC。
为什么要分三个区:
如果分两个区第一次gc时存活对象由e区进入s区然后清空e区,此时老年代也可以启担保作用,一切看起来都很正常;但是如果再发生第二次gc会怎样呢,此时e区有存活对象而容纳第一次gc时存活对象的s区也肯定会有存活对象,此时需要将e,s区的存活对象复制到老年代,此时,问题也就显而易见了,与不分代的时候情形一样了,对象只能进入老年代;而三个区的话:
此时新生代分成了e区,s0区和s1区;这时候再发生mionrGc就不会有上面出现的问题了:第一次GC时存活对象由e进入s0,然后清空e区;第二次gc时将e区和s0区的存活对象复制到s1区,清空e区和s0;第三次将e区和s1的对象复制到s0,清空e和s1。。。。。。
无论多少次mionrGc,如此反复,直到对象年龄达标或者s区(s0或s1)容不下存活对象时再晋升到老年代,这样也就降低了老年代的压力,减少了fullGc的次数;
Major GC:清理老年代区域
Full GC:清理年轻代、年老代、永久代区域,成本高,会对系统性能产生影响。
存储地址由堆内存,变成主机内存(物理内存)
CMS全称Concurrent Mark Sweep,是一款并发的、使用「标记-清除算法」的垃圾回收器。在标记清理过程中不会导致用户线程无法定位引用对象。仅作用于老年代收集。
步骤如下:
1.初始标记(CMS initial mark):独占CPU,stop-the-world,仅标记GCroots能直接关联的对象,速度比较快;
2.并发标记(CMS concurrent mark):可以和用户线程并发执行,通过GCRoots Tracing标记所有可达对象;
3.重新标记(CMS remark):独占CPU,stop-the-world,对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新逃逸对象;
4.并发清理(CMS concurrent sweep):可以和用户线程并发执行,清理在重复标记中被标记为可回收的对象。
两次STW
CMS的优点:
支持并发收集
低停顿,因为CMS可以将耗时的两个STW操作保持与用户线程在恰当的时机并发执行,并且能在短时间内完成,达到近似并发的效果
CMS的缺点:
对CPU资源敏感,因为占用了一部分cpu资源,在CPU资源不足的情况下会卡顿
无法处理浮动垃圾,并发清理步骤时,用户线程也会产生一部分可回收对象,但是这部分对象在下次清理的时候才会被回收。
CMS清理后会产生大量内存碎片,没有整理操作。
G1 垃圾收集器整体上采用标记-整理算法,细节上是一种复制算法,作为一种全功能全代的垃圾收集器,是 JDK9 之后的默认垃圾收集器,用于替代 CMS。
G1收集器的内存结构完全区别于CMS,弱化了CMS原有的分代模型(分代可以是不连续的空间),将堆内存划分成一个个Region(1MB~32MB,默认2048个分区),这么做的目的是在进行收集时不必在全堆范围内进行。它主要特点在于达到可控的停顿时间,用户可以指定收集操作在多长时间内完成,即G1提供了接近实时的收集特性。
垃圾回收的主要过程跟 CMS 一样,主要也是 4 个阶段
初始标记 (Inital Marking)
STW, 仅标记 GC Roots 能直接关联到的对象,耗时很短,跟 CMS 一样
并发标记 (Concurrent Marking)
从 GC Roots 直接关联对象开始可达性分析,扫描对象图,耗时很长,并发执行。
同理会造成浮动垃圾和漏标问题;为了避免漏标问题,采用 SATB 原始快照记录下在并发时有引用变动的对象。即白色对象从灰色对象的引用中断开
最终标记 (Final Marking)
STW, 用于处理并发标记阶段留下的少量 SATB 记录。将断开的白色对象置为灰色,让 GC 线程重新扫描这些对象
筛选回收 (Live Data Counting And Evacuation)
负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序。然后根据用户预期停顿时间,自由任意选择多个 Region 构成回收集(CSet)
STW, GC 线程并行将需要回收的 CSet 中的存活对象复制到空闲状态的 Region 中。
G1的特点:
并行与并发:G1充分发挥多核性能,使用多CPU来缩短Stop-The-world的时间。
分代收集:G1能够自己管理不同分代内已创建对象和新对象的收集。
空间整合:G1从整体上来看是基于‘标记-整理’算法实现,从局部(相关的两块Region)上来看是基于‘复制’算法实现,这两种算法都不会产生内存空间碎片。
可预测的停顿:它可以自定义停顿时间模型,可以指定一段时间内消耗在垃圾回收商的时间不大于预期设定值。