一、基础篇JVM
JVM内存结构
堆(Heap):用于存放java实例或对象,gc操作的主要区域
方法区(Method Area):常量池、静态变量、构造函数(这里插入一点)
(!! 构造函数定义的是,对象在调用功能之前,在建立时,应该具备的一些内容,也就是对象的初始化内容。 构造函数的函数名要与类名一样,且无返回值。)
程序计数器(Program Counter Register):保存着当前线程执行的虚拟机字节码指令的内存地址,白话文来说就是一般在多线程的场景下,任何处理器都只会处理线程中的一个指令,所以为了保证多线程切换后,还能回到原来的状态,每个线程都会设立一个程序计数器。
栈(Stack):先图示一下
首先是虚拟机栈:每创建一个线程,就会对于一个java栈,当你调用方法的时候,就将栈帧压入,栈本身的特点是先进后出,那么当前活动的就是它的栈顶元素。
接下来就是本地方法栈:相比于虚拟机栈,本地方法栈则是为Native方法服务的。(!!这里解释一下关于java中的Native方法,简单的来说,就是java调用非java代码的接口,即有时候java需要与外界环境相交互的时候而衍生的一种交流机制。)
最后就是一个直接内存的概念了:这部分被频繁使用的时候是可能引起OOM问题出现,属于堆外内存,它的作用是避免了在Java 堆和Native 堆中来回复制数据。
Java内存模型(对一些名词的解释)
首先先来说一下
Java内存模型(JMM):引用一下图
主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)。
工作内存
主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的。
(1)Java所有变量都存储在主内存中
(2)每个线程都有自己独立的工作内存,里面保存该线程的使用到的变量副本
然后来阐释一些这些名词的区别:
内存可见性:一个线程对共享变量的修改,更够及时的被其他线程看到.
这个时候就引出了两者的区别(大佬博客的解析,引用一下)
(1)Synchronized:保证可见性和原子性
Synchronized能够实现原子性和可见性;在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。当然有锁以后的显而易的是可能会出现阻塞队列。
(2)Volatile:保证可见性,但不保证操作的原子性
Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。但volatile不保证volatile变量的原子性。
重排序:为了优化程序性能而采取的对指令进行重新排序执行的一种手段。分为编译期重排序(通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。)与运行期重排序(!!插入一则:volatile还有一个作用就是局部阻止重排序的发生)。
顺序一致性:在单线程环境下,指令执行的最终效果应当与其在顺序执行下的效果一致。
final关键字: 对于final首先,他所修饰的类是不能被继承的,比如说String,同时fanal还可以修饰变量,被final修饰的变量就是一个常量,只能赋值一次。
垃圾回收(gc)
内存分配策略:首先我们先来了解一下jvm内存的内存结构
java8以后去掉了永久代,这里就不加详细阐述了,想了解的小伙伴可以自行查这方面相关内容,这里主要先将堆内结构,分为新生代(eden,s1,s2||默认的分配比例是8:1:1)与老年代。首先对象会优先在Eden中进行分配,当eden中没有足够内存的时候就会发起一次Minor GC。当对象为需要大量连续内存空间的对象的时候,会直接进入老年代,还有就是长期存活的对象会进入老年代,这里指Minor GC不是只执行一次的,除非Survivor空间不够,一般来说,默认是15次以后进入老年代。
垃圾收集器(G1):首先它是面向服务端的,同时它是一种不会产生内存碎片的垃圾回收,类似于“标记-整理算法”,使用G1收集器时,Java堆得内存布局就与其它收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。G1垃圾收集器的运行过程大致为:
1)初始标记(Initial Marking)
2)并发标记(Concurrent Marking)
3)最终标记(Final Marking)
4)筛选标记(Live Data Counting and Evacuation
提gc算法之前,插一句嘴,我们先了解了解jvm中垃圾回收的判定,以前是属于引用计数算法,引用计数算法的原理是这样的:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;在任何时刻计数器的值为0的对象就是不可能再被使用的,也就是可被回收的对象。(但同时这种算法也会带来一个问题就是对象之间相互循环引用的问题,比如父类对象与子对象相互引用),然后就引出了现在所使用的可达性算法:
在上图中,objectA、objectB、objectC是可达的,不会被回收;objectD、objectE虽然有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java中,可作为GC Roots的对象包括下面几种:
1、虚拟机栈中引用的对象;
2、方法区中类静态属性引用的对象;
3、方法区中常量引用的对象;
4、本地方法栈中Native方法引用的对象。
说完这些就可以回过头来看看gc算法了
首先是最基础的,“标记清除算法”:
标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象;
清除阶段:清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header信息),则将其回收。
当然这种算法随之带来的问题就是,效率低下,和可能会出现内存碎片问题。
为了解决这种问题,引入了复制算法:将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块内存上,然后把这一块内存所有的对象一次性清理掉。不过这种算法带来的问题就是,可能会造成大量的内存浪费,将内存缩小为原来的一半,浪费了一半的内存空间,代价太高,而且极其耗费时间,所以这种算法也是不是可取的,然后进行进一步的优化。这个时候“标记-整理”算法出现了,标记/整理算法的标记过程任然与标记/清除算法一样,但后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边线以外的内存。最后给三个算法来个排序:
效率:复制算法 > 标记/整理算法 > 标记/清除算法(标记/清除算法有内存碎片问题,给大对象分配内存时可能会触发新一轮垃圾回收)
内存整齐率:复制算法 = 标记/整理算法 > 标记/清除算法
内存利用率:标记/整理算法 = 标记/清除算法 > 复制算法
还有一种终极分代算法,取各家之所长,如果有兴趣的朋友可以自己去查询相关知识,这里就不加详述了。