JMM & JVM & 垃圾回收
目录
1.1 垃圾:内存中已经不再被使用的空间1.2 如何判断垃圾(引用计数法、可达性分析)
2、4种GC算法(引用计数 /复制拷贝/标记清除/标记整理)
3.1 主要4个(Serial / Parallel / CMS / G1)
3.2 其它3个(ParNew / ParallelOld / SerialOld)
本文通过学习:周阳老师-尚硅谷Java大厂面试题第二季 总结的JMM,JVM,GC垃圾回收相关的笔记
一、JMM内存模型
1、定义
JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定:
(1)线程解锁前,必须把共享变量的值刷新回主内存
(2)线程解锁前,必须读取主内存的最新值,到自己的工作内存
(3)加锁和解锁是同一把锁
|
主内存 | 计算机的内存,也就是经常提到的8G内存,16G内存 |
工作内存 | 实例化 new student,那么 age = 25 也是存储在主内存中。当同时有三个线程同时访问 student中的age变量时,那么每个线程都会拷贝一份,到各自的工作内存,从而实现了变量的拷贝 |
2、JMM的三大特性(可见性+原子性+有序性)
2.1 可见性
当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
2.2 原子性
一个或多个操作,要么全部执行,要么全部不执行。
2.3 有序性
(1)有序性:在本线程内观察,所有的操作都是有序的;而在一个线程内观察另一个线程,所有操作都是无序的。前半句指 as-if-serial 语义:线程内似表现为串行,后半句是指:“指令重排序现象”和“工作内存与主内存同步延迟现象”。处理器为了提高程序的运行效率,提高并行效率,可能会对代码进行优化。编译器认为,重排序后的代码执行效率更优。这样一来,代码的执行顺序就未必是编写代码时候的顺序了,在多线程的情况下就可能会出错。
(2)有序性问题:在多线程的环境下,由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致计算结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。
3、JMM中的8种原子操作
(1)read 读取:作用于主内存,将共享变量从主内存传送到线程的工作内存中。
(2)load 载入:作用于工作内存,把 read 读取的值放到工作内存中的副本变量中。
(3)store 存储:作用于工作内存,把工作内存中的变量传送到主内存中。
(4)write 写入:作用于主内存,把从工作内存中 store 传送过来的值写到主内存的变量中。
(5)use 使用:作用于工作内存,把工作内存的值传递给执行引擎,当虚拟机遇到一个需要使用这个变量的指令时,就会执行这个动作。
(6)assign 赋值:作用于工作内存,把执行引擎获取到的值赋值给工作内存中的变量,当虚拟机栈遇到给变量赋值的指令时,就执行此操作。
(7)lock锁定: 作用于主内存,把变量标记为线程独占状态。
(8)unlock解锁: 作用于主内存,它将释放独占状态。
二、JVM
1、JVM体系结构(类加载子系统+运行时数据区+执行引擎)
JVM的组成:类加载子系统 + 运行时数据区 + 执行引擎
堆和方法区是所有线程共有的, 栈、本地方法栈、程序计数器是每个线程独有的。 |
1.1 类装载子系统
(1)类加载器(ClassLoader)
- 1. BootStrap ClassLoader(引导类加载器)
- 2. Extension ClassLoader(扩展类加载器)
- 3. Application/System ClassLoader(应用程序类加载器)
- 4.自定义类加载器
加载器类型 | 加载内容 | 位置 | 父类加载器 |
Bootstrap类加载 | 加载核心类库 | <JAVA_HOME>/lib/rt.jar中 | 无 |
Extension类加载 | 加载扩展类 | <JAVA_HOME>/lib/ext中 | Bootstrap类加载 |
Application类加载或System类加载 | 加载classpath路径下的自定义类 | Extension类加载 |
【高频问答】
1.Object类是由哪个类加载器加载的? 答:BootStrap ClassLoader
2.我们自己写的类是由哪个类加载器加载的? 答:System ClassLoader
3.Applicaton类加载器和system类加载器,有什么区别?
答:无区别,系统加载器用于启动应用程序
4.类加载器都是我们Java中的一个类ClassLoader的子类吗?
答:BootStrapClassLoader不是的,另外两个是的。
5.一个自定义类的加载过程?
答:比如我们自己写了一个类Student类,经过编译后会得到Student.class文件,然后经过类加载器得到Class实例,例如通过Class.forName(“com.***.Student”),通过全路径加载进来。然后我们用Student.class.getClassLoader()得到它的类加载器,得到的是AppClassLoader(即系统类加载器),如果用Student.class.getClassLoader().getParent()得到的是它的父加载器ExtClassLoader(即拓展类加载器),然后用Student.class.getClassLoader().getParent().getParent()得到将会是Null,因为启动类加载器是用C++写的,我们无法通过程序直接得到。
(2)JVM类加载机制(全盘负责+父类委托+双亲委派模型)
全盘负责:当前线程的类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用CLassLoader.loadClass()指定类加载器来载入
父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。所以我们在开发中尽量不要使用与JDK相同的类(例如自定义一个java.lang.System类),因为父类加载器中已经有一份java.lang.System类了,它会直接将该类给程序使用,而你自定义的类压根就不会被加载。
双亲委派模型:双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
- 当AppClassLoader加载一个class时,它把类加载请求委派给父类加载器ExtClassLoader,
- 当ExtClassLoader加载一个class时,它把类加载请求委派给BootStrap ClassLoader,
- 如果BootStrap ClassLoader加载失败,会使用ExtClassLoader来尝试加载;若ExtClassLoader也加载失败,则会使用AppClassLoader来加载;如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
双亲委派机制的优劣
优点 | 1.避免类的重复加载。有优先级的层级关系,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次 2.保护程序安全,防止核心API被随意篡改。 |
缺点 | 1.委派过程是单向的,顶层的ClassLoader无法访问底层的ClassLoader所加载的类。 |
(3)类的加载过程
加载->【链接: 验证->准备->解析】->初始化->使用->卸载
JVM将javac编译好的class字节码文件加载到内存中,并对该数据进行验证、解析和初始化、形成JVM可以直接使用的JAVA类,最终回收(卸载)的过程。
(4)双亲委派模型意义(系统类防止内存中出现多份同样的字节码+保证Java程序安全稳定运行)
1.2 运行时数据区
(1)方法区
针对常量池的回收和类型的卸载。
(2)Java堆
最大的一块内存,目的就是存放对象的实例,几乎所有对象实例都在堆中分配内存虚拟机栈
也叫栈内存,主管 Java 程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就仔放,对于栈来说不存在垃圾回收问题,只要线程结束该栈就释放,生命周期和线程一致,是线程私有的。8种基木类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
栈的大小和具体JVM的实现有关,通常在 256K~1024K 之间, 1M 左右。
(3)JVM栈
特点:局部变量表所需的内存空间在编译期间完成内存分配。当进入一个方法时,这个方法需要在帧中分配多大的内存空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常状态:如果线程请求的栈的深度大于虚拟机允许的深度,将抛出StackOverFlowError异常(栈溢出);如果虚拟机栈可以动态扩展(现在大部分Java虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的java虚拟机栈),如果扩展时无法申请到足够的内存空间,就会抛出OutOfMemoryError异常(没有足够的内存)。
(4)本地方法栈
本地方法栈与虚拟机栈基本类似,区别在于:
虚拟机栈为虚拟机执行的java方法服务;本地方法栈则是为Native方法服务(栈的空间大小远远小于堆)
(5)程序计数器
一个指向方法区中的方法字节码的指针
1.3 字节码执行引擎
2、JVM参数调优
查看JVM默认参数的三种方法
方法1 | jps + jinfo | jps -l 查看java进程 jinfo flag 参数名 进程ID 查看某JVM参数 jinfo flags 进程ID 查看所有JVM参数 |
方法2 | java -XX:+PrintFlagsInitial java -XX:+printFlagsFinal | PrintFlagsInitial 打印JVM初始参数 =表示未修改过, :=表示已修改过 |
方法3 | java -XX:+PrintCommandLineFlags | 打印JVM默认的简单初始化参数 |
2.1 三大参数类型
参数类别 | 说明 | 示例 |
标配参数 |
| java -version java -help |
X参数(掌握) |
| java -Xint -version java -Xcomp -version java -Xmixed -version |
XX参数(重点) |
| java -XX:-PrintGCDetails 关闭了GC详情输出 java -XX:MetaspaceSize=21807104 查看Java元空间的值 |
问:如何查看JVM系统默认值?
答:一般使用jps和jinfo进行查看。jps查看java的后台进程,jinfo查看正在运行的java程序,
jinfo -flags *** 查看jvm的全部默认参数。jinfo -flag PrintGCDetails 进程ID 查看某个参数。
//运行一个HelloGC的java程序 public class HelloGC { public static void main(String[] args) throws InterruptedException { System.out.println("hello GC"); Thread.sleep(Integer.MAX_VALUE); } } | jps -l 查看到HelloGC的进程号为:12608 jinfo -flag PrintGCDetails 12608 jinfo -flag 然后查看是否开启PrintGCDetails这个参数 -号表示关闭,即没有开启PrintGCDetails这个参数 |
问:如何开启PrintGCDetails参数?
答:step1、IDEA的 Run ---> Edit Configurations
然后,在VM Options中加入下面的代码,现在+号表示开启
-XX:+PrintGCDetails
step2、再次查看
jps -l
jinfo -flag PrintGCDetails 13540
得到:-XX:+PrintGCDetails,说明通过VM Options配置的JVM参数已经生效了。
2.2 九个调优参数
6个常用调优参数
-Xms | 初始化堆内存 | 默认为物理内存的1/64,等价于 -XX:initialHeapSize java -xms 10m |
-Xmx | 最大堆内存 | 默认为物理内存的1/4,等价于 -XX:MaxHeapSize java -xmx 10m |
-Xss | 单个线程栈的大小 | 默认为512K~1024K,等价于 -XX:ThreadStackSize |
-Xmn | 设置年轻代大小 | |
-XX:MetaspaceSize | 设置元空间大小 |
|
-XX:PrintGCDetails | 输出详细GC收集日志信息 |
|
3个垃圾回收相关的调优参数
-XX:SurvivorRatio | 调节新生代中 eden 和 S0、S1的空间比例 | java -XX:SuriviorRatio=8 表示 Eden:S0:S1 = 8:1:1 |
-XX:NewRatio (了解即可) | 配置年轻代new 和老年代old 在堆结构的占比 | java -XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3 |
-XX:MaxTenuringThreshold | 设置垃圾最大年龄 | java -XX:MaxTenuringThreshold=15 SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代 这里就是调整这个次数的,默认是15,并且设置的值 在 0~15之间。
|
GC垃圾收集器
GC | GC在新生区 |
Full GC | Full GC大部分发生在养老区 [名称: GC前内存占用 -> GC后内存占用 (该区内存总大小)] 当出现了老年代都扛不住的时候,就会出现OOM异常 |
三、垃圾回收器
1、GCRoot和可达性分析
1.1 垃圾:内存中已经不再被使用的空间
1.2 如何判断垃圾(引用计数法、可达性分析)
(1)引用计数法
每有引用则计数器值+1,每有一个引用失效则-1,=0则可回收。但它存在循环引用问题。
(2)枚举根节点(GC Roots)做可达性分析
GC Roots就是一组必须活跃的引用。通过一系列名为 GC Roots的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的对象就被判定为死亡
1.3 哪些对象可以当做GC Roots?
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中的JNI(Native方法)的引用对象
public class GCRootDemo {
// 方法区中的类静态属性引用的对象
// private static GCRootDemo2 t2;
// 方法区中的常量引用,GC Roots 也会以这个为起点,进行遍历
// private static final GCRootDemo3 t3 = new GCRootDemo3(8);
public static void m1() {
// 第一种,虚拟机栈中的引用对象
GCRootDemo t1 = new GCRootDemo();
System.gc();
System.out.println("第一次GC完成");
}
public static void main(String[] args) {
m1();
}
}
GC算法和垃圾回收器的区别:GC算法是内存回收的方法论,垃圾收集器就是算法的落地实现
2、4种GC算法(引用计数 /复制拷贝/标记清除/标记整理)
- 引用计数(几乎不用,无法解决循环引用的问题)
- 复制拷贝(用于新生代)
- 标记清除(用于老年代)
- 标记整理(用于老年代)
3、4种垃圾回收器
3.1 主要4个(Serial / Parallel / CMS / G1)
UseSerialGC: 串行垃圾收集器 | UseParallelGC: 并行垃圾收集器 | UseConcMarkSweepGC: 并发标记清除(即CMS) | UseG1GC: G1垃圾收集器 |
它为单线程环境设计且值使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行。所以不适合服务器环境 | 多个垃圾收集线程并行工作,此时用户线程也是阻塞的,适用于科学计算 / 大数据处理等弱交互场景,也就是说Serial 和 Parallel其实是类似的,不过是多了几个线程进行垃圾收集,但是主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短 | 用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景。并发是可以有交互的,也就是说可以一边进行收集,一边执行应用程序。 | 将堆内存分割成不同区域,然后并发的进行垃圾回收 |
3.2 其它3个(ParNew / ParallelOld / SerialOld)
UseParNewGC 年轻代的并行垃圾回收器 | UseSerialOldGC 串行老年代垃圾收集器 (已经被移除) | UseParallelOldGC 老年代的并行垃圾回收器 |
4、使用范围
新生代 | UserSerialGC、UserParallelGC、UserParNewGC |
老年区 | UseConcMarkSweepGC、UseSerialOldGC、UseParallelOldGC |
各区都能用 | UseG1GC |