目录
? ? ? 一个 CPU 在某个时间点,只能做一件事情,在多线程的情况下,CPU 运行时间被划分成若干个时间片,分配给各个线程执行;
程序计数器的作用就是记录当前线程执行的位置,当线程被切换回来的时候,能够找到该线程上次运行到哪儿了;所以程序计数器一定是线程隔离的。
方法区用于存放已被加载的类信息、常量、静态变量、即编译器编译后的代码等。
还有要注意的一点:方法区是 JVM 的规范,在 JDK 1.8 之前,方法区的实现是永久代;从 JDK 1.8 开始 JVM 移除了永久代,使用本地内存来存储元数据并称之为:元空间(Metaspace)。
对于堆栈的区别总结一句话:堆中存对象,栈中存基本数据类型和堆中对象的引用;一个对象的大小是可以动态变化的,而引用是固定大小的。
这么看就容易理解堆为什么是线程公有的了,省地儿啊
适用于堆内存完整的情况,已分配的内存和空闲内存分表在不同的一侧,通过一个指针指向分界点,当需要分配内存时,把指针往空闲的一端移动与对象大小相等的距离即可,用于Serial和ParNew等不会产生内存碎片的垃圾收集器。
适用于堆内存不完整的情况,已分配的内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的内存块信息,当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录,最常见的使用此方案的垃圾收集器就是CMS。
记录了该对象锁相关的信息、分代年龄、hashCode,在32位JVM中占32bit,在64位JVM中占64bit
指向方法区对应class信息的指针,在32位JVM中占32bit,在64位JVM中占64bit(开启指针压缩的情况下占32bit)
如果是数组的话,组成中会包含数组长度,32bit
类的实例信息
JVM要求Java对象的大小应该是8bit的倍数,这部分就是将对象大小补充为8bit的倍数
使用句柄访问对象,Java堆中会划分出一块内存作为句柄池
,reference中存储
的就是对象的句柄地址
,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储
的直接就是对象地址
。(Sun HotSport VM的使用方式)
引用计算器判断对象是否存活的算法是这样的:给每一个对象设置一个引用计数器,每当有一个地方引用这个对象的时候,计数器就加1,与之相反,每当引用失效的时候就减1。
优点:实现简单、性能高。
缺点:增减处理频繁消耗cpu计算、计数器占用很多位浪费空间、最重要的缺点是无法解决循环引用的问题。
因为引用计数器算法很难解决循环引用的问题,所以主流的Java虚拟机都没有使用引用计数器算法来管理内存。
在主流的语言的主流实现中,比如Java、C#、甚至是古老的Lisp都是使用的可达性分析算法来判断对象是否存活的。
这个算法的核心思路就是通过一些列的“GC Roots”对象作为起始点,从这些对象开始往下搜索,搜索所经过的路径称之为“引用链”。
当一个对象到GC Roots没有任何引用链相连的时候,证明此对象是可以被回收的。如下图所示:
在代码中普遍存在的,类似“Object obj = new Object()”这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当jvm认为内存不足时,才会去试图回收软引用指向的对象。jvm会确保在抛出OutOfMemoryError之前,清理软引用指向的对象。
非必需对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
也称为幽灵引用或幻影引用,是最弱的一种引用关系,无法通过虚引用来获取一个对象实例,为对象设置虚引用的目的只有一个,就是当着个对象被收集器回收时收到一条系统通知。
标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现,在标记清理过程中不会导致用户线程无法定位引用对象。仅作用于老年代收集。它的步骤如下:
G1收集器的内存结构完全区别去CMS,弱化了CMS原有的分代模型(分代可以是不连续的空间),将堆内存划分成一个个Region(1MB~32MB, 默认2048个分区),这么做的目的是在进行收集时不必在全堆范围内进行。它主要特点在于达到可控的停顿时间,用户可以指定收集操作在多长时间内完成,即G1提供了接近实时的收集特性。它的步骤如下:
JVM中有这样一个参数?-XX: PretenureSizeThreshold?,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区以及2个Survivor区之间来回复制,产生大量的内存复制操作
对象通常在Eden区诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor中,并且将其对象设为1岁,对象在Survivor区中每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认15),就会被晋升到老年代中,对象晋升老年代的年龄阈值, 可以通过参数-XX:MaxTenuringThreshold设置
HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
加载阶段,简言之,查找并加载类的二进制数据,生成 Class 的实例
在加载类时,Java 虚拟机必须完成以下3件事情:
当类加载到系统后,就开始链接操作,验证是链接操作的第一步
它的目的是保证加载的字节码是合法、合理并符合规范的
准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。
在准备阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用
类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行 Java 字节码。(即:到了初始化阶段,才真正开始执行类中定义的 Java 程序代码)
任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“万事俱备,只欠东风”,就等着开发者使用了
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用 new 关键字为其创建对象实例
当 Sample 类被加载、链接和初始化后,它的生命周期就开始了。当代表 Sample 类的 Class 对象不再被引用,即不可触及时,Class 对象就会结束生命周期,Sample 类在方法区内的数据也会被卸载,从而结束 Sample 类的生命周期
一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期
JVM为什么会抛出ClassNotFund异常?在抛出这个异常的时候JVM的类加载器做了什么工作?
Java程序在执行的过程中,是先执行父类还是先执行子类。如果加载父类,那么父类还有父类呢,这个时候JVM还要怎么处理,
JVM是如何保证类加载的有序性和安全性?
一个类在收到类加载请求后,不会自己加载这个类,而是把这个类加载请求向上委派给它的父类去完成,父类收到这个请求后又继续向上委派给自己的父类,以此类推,直到所有的请求委派到启动类加载器中。
当父类加载器在接收到类加载请求后,发现自己也无法加载这个类(这个情况通常是因为这个类的Class文件在父类的加载路径中不存在)这时父类会把这个信息反馈给子类,并向下委派子类加载器来加载这个类,直到这个请求被成功加载,但是一直到自定义加载器都没有找到,JVM就会抛出ClassNotFund异常。