JVM总结的部分内容
内存分配的两种方式
指针碰撞法
维护一个指针,指针左边为已分配的内存,右边为空闲内存,动态调整指针。在右边进行分配。
要求内存规整。
空闲表法
维护一张表,记录哪里分配了哪里没有分配。分配的时候找能存放对象的空间分配即可。
要求内存不规整。
内存分配并发问题(补充内容,需要掌握
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
判断是否对象存活
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
可达性分析
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
GC ROOTS
虚拟机栈中引用的对象
方法去中类静态属性引用的对象
方法去中常量引用的对象
本地方发栈中引用的对象
四种引用
强引用
普通的new一个对象就是强引用,一般不会被回收,除非他已经不存活。
软引用
通过SoftReference来创建,当内存不足的时候会被回收。
软引用可用来实现内存敏感的高速缓存
弱引用
通过WeakReference来创建,当下一次GC来临的时候被回收。
虚引用
通过PhantomReference来创建,一般用于被引用对象回收后的消息传递。
虚引用主要用来跟踪对象被垃圾回收的活动。
垃圾清除算法
标记-清除
该算法分为“标记”和“清除”阶段:首先比较出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
效率问题
空间问题(标记清除后会产生大量不连续的碎片)
标记-整理
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
复制
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
分代收集
垃圾收集器
serial(串行)
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
会stop the world.
ParNew
它是 Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
Parallel Scavenge 收集器
吞吐量优先,多线程
Serial old
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
Parallel Old
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
CMS 并发
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
初始标记和重新标记需要stw。其他不需要。
优点:并发收集,低停顿
理由: 由于在整个过程和中最耗时的并发标记和 并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,Cms收集器的内存回收过程是与用户线程一起并发执行的
缺点:
1.CMS收集器对CPU资源非常敏感
在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低,为了解决这种情况,虚拟机提供了一种“增量式并发收集器”
的CMS收集器变种, 就是在并发标记和并发清除的时候让GC线程和用户线程交替运行,尽量减少GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐)
CMS处理器无法处理浮动垃圾
CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾” ,
CMS是基于“标记--清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。
为了解决这个问题,CMS提供了一个开关参数,用于在CMS顶不住要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了
G1
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
在G1下不再区分新生代,老年代。而是直接将堆划分成一个一个区域Region.
不同代的垃圾扫描算法
新生代使用:复制算法
老年代使用:标记 - 清除 或者 标记 - 整理 算法
JVM内存模型
线程独占:JAVA虚拟机栈,本地方法栈,程序计数器
线程共享:堆,方法区
JAVA虚拟机栈:执行每个方法时会创建一个,用于保存局部变量表,操作栈,动态链接,方法出口等信息。调用入栈,返回出栈。
本地方法栈:执行JAVA方法调用JAVA虚拟机栈,调用Native方法时,调用本地方法栈。保存的信息与JAVA虚拟机栈类似。
程序计数器:保存当前线程执行的字节码的位置,只为JAVA方法服务。如果是Native方法,计数器会为空。
堆:存放对象的实例,没空间抛出OOM
方法区:已被虚拟机加载的类信息,常量,静态变量,JIT编译后的代码。
永久代和METASPACE都是方法区。
JAVA内存模型
https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md#十java-内存模型
Volatile可以解决的问题
防止指令重排序(保证有序性)
强制主内存读写同步(保证可见性)
volatile 是Java语言的关键字, 功能是保证被修饰的元素(共享变量):
任何进程在读取的时候,都会清空本进程里面持有的共享变量的值,强制从主存里面获取;
任何进程在写入完毕的时候,都会强制将共享变量的值写会主存。
volatile 会干预指令重排。
volatile 实现了JMM规范的 happens-before 原则。
原子性:基本数据类型的操作(除long和double, 因为他们是8字节的,在Jvm中会强制分成两个4字节的操作)+ synchronized
可见性:synchronized + volatile
有序性:volatile+happens-before
Happens-Before
语义串行性
锁规则 -- 加锁要发生在解锁前
传递性
启动中断
类加载
包括以下 7 个阶段:
加载(Loading)
将文件读入到内存,查找字节码文件,利用文件创建class对象。
验证(Verification)
检验文件内容是否符合虚拟机标准。文件格式,元数据,字节码,符号引用验证。
准备(Preparation)
类变量内存(static变量,赋值默认为0或者null)
如果是常量(final修饰的)会直接赋值
解析(Resolution)
进行引用替换、字段解析、接口解析、方法解析
符号引用替换成直接引用
初始化(Initialization)
静态块执行
静态变量(这里才真正将值赋予给变量)
先对父类初始化
只有对类的主动使用才会执行。
创建实例、访问静态方法(变量),class.forName()反射,子类初始化
使用(Using)
实例化
卸载(Unloading)
GC
卸载类需要满足3个要求:
该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
该类没有在其他任何地方被引用
该类的类加载器的实例已被GC
所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
类加载器
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型
为了解决一个类被重复加载的问题。
1. 工作过程
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
2. 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
防止类的重复加载
防止JAVA核心API被篡改。