JDK1.8 Java虚拟机 Java Virtual Machine(JVM)>
1.JVM的位置:
JVM是运行再操作系统(windows、Linux、Mac)之上的
2.内存结构:
2.1.程序计数器:
- 作用: 是记住下一条jvm指令的地址
- 特点: 线程私有的,不会出现内存溢出的情况
2.2.虚拟机栈 (Java Virtual Machine Stack):
栈:线程运行需要的内存空间
栈帧(Stack Frame):每个方法运行时需要的内存
线程私有的,每个线程只能有一个活动栈帧,对应着往前正在执行的方法
2.2.1.栈内存溢出(StackOverflowError)
原因:
- 栈帧过多导致栈内存溢出:最有可能的就是方法递归调用产生这种结果。
- 栈帧过大导致栈内存溢出:可能是局部变量太多,太大造成,但比较少见
存放:
- 参数、局部变量表、返回地址
2.3.本地方法栈:
功能和特点都类似虚拟机栈,都是线程私有的以及都能抛出StackOverflowError异常。
和java虚拟机栈的区别在于,本地方法栈运行的都是由非java语言编写的方法,也就是native方法,
2.4.堆(Heap):
特点:
- 线程共享的,堆内存中需要考虑线程安全的问题
- 有垃圾回收机制
存放:
- new对象、数组、成员变量
- 1.7以后存储静态变量、字符串常量池(String Pool)
2.4.1.堆内存溢出(OOM):
原因:
- 设置的堆内存太小,可以通过
-Xmx8m
(这里是8M)配置堆内存大小 - 由于设计不够合理导致系统需要过多的内存,需要通过设计减少内存的使用。
2.4.2.堆内存诊断
- jps工具:查看当前系统中有哪些java进程
- jmap工具:查看堆内存占用情况
- jconsole工具:图形界面的。多功能的检测工具,可以连续监测
2.5.方法区:
在JDK1.8以前,方法区被称为永久代,而JDK1.8废除了永久代,使用元空间替代永久代,而元空间不在虚拟机内存中实现,而是在本地内存中实现
特点:
- 线程共享的。
- 元空间的大小默认就是机器本地的物理内存,但是可以用
-XX:MaxMetaspaceSize=8M
手动设置元空间的内存大小(1.8以后) - 虚拟机启动时创建
存储:
- 类信息、运行时常量池、字段、方法和构造函数等等
2.5.1.元空间内存溢出
一般来说元空间内存不太会溢出,出现原因:
- 手动设置的元空间内存太小
- 虚拟机加载了太多类信息
2.5.2.运行时常量池
程序要运行,需要把java文件编译成二进制字节码,二进制字节码包括:类基本信息、常量池、类方法定义,虚拟机指令
- 常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 当类被加载时,它的常量池信息会放到运行时常量池,并把里面的符号地址变为真实地址
2.5.3.StringTable
存储位置:
- 1.6存在永久代中,1.7以后为了提高回收效率存在堆内存中
结构:
- 存储结构为HashTable,也就是数组+链表的形式,不允许扩容
特性:
-
常量池中的字符串只是符号,第一次用到时才变成对象
-
字符串池是为了避免重复创建字符串对象
-
字符串变量的拼接原理是StringBuilder(1.8)
-
字符串常量的拼接原理是编译期优化
-
intern方法,主动将字符串池中还没有的字符串对象放入字符串池
String s1 = new ("a) + new String("b"); // 字符串池[a 、 b]; 堆:new String("a")、new String("b")、new String("ab")
String s2 = s1.intern(); // 把new String("ab")对象放到字符串池中,并将字符串池中的对象返回
// s1==s2
调优:
-XX:StringTableSize=800000
桶个数,桶的个数即数组长度,桶越多,越不容易造成hash冲突,链表的节点越少,查询的越快,效率越高
3.垃圾回收(GC)
GC在执行过程中,会暂停其他的所有线程,Stop-the-World(STW)。GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有 高吞吐 、低停顿 的特点。
推荐看《图解垃圾回收机制》
3.1.如何判断对象是否需要回收
3.1.1.引用计数算法
如果一个对象没有被任何引用指向,则可视之为垃圾。
每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为0就代表该对象死亡。但是如果对象之间相互引用,就一直无法回收,可能导致内存问题。
3.1.2.可达性分析算发
扫描堆中的对象,看是否能够沿着GC Roots对象为起点的引用链找到该对象,找不到,表示可以回收
----------------《简单易懂的可达性分析算法》
3.1.3.GC Roots
GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象 )。
- 方法区的静态属性引用的对象
- 常量池引用的对象
- 方法栈中栈帧本地变量表引用的对象
- 本地方法栈中引用的对象
- 被同步锁持有的对象
3.1.4.四种引用
- 强引用:只要对象跟GC Roots有直接或间接强引用关系,就不能被回收
- 软引用:由SoftReference创建的实例引用,就是软引用
Student stu = new Student();
SoftReference soft = new SoftReference(stu);
只有软引用在引用对象时,垃圾回收后,内存还是不足时,会被回收。
当引用的对象被回收后,SoftReference也是一个对象,SoftReference将会进入引用队列,可配合引用队列(ReferenceQueue )释放软引用本身
- 弱引用:由WeakReference创建的实例引用,就是弱引用
Student stu = new Student();
WeakReference soft = new WeakReference(stu);
只有弱引用在引用对象时,不管内存够不够,都会发生回收。当引用的对象被回收后,WeakReference也是一个对象,WeakReference将会进入引用队列,可配合引用队列释放软引用本身
- 虚引用:用PhantomReference 实现,
Student stu = new Student();
ReferenceQueue<Student> rq = new ReferenceQueue<>();
PhantomReference<Student> pr = new PhantomReference<>(stu, rq);
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。由于get方法返回永远是null,无法通过虚引用来获取对象,所以必须配合引用队列使用
3.2.回收算法
3.2.1.标记清除算法
先检查对象是否可回收,并将可回收的对象标记出来,标记完毕后再进行回收
缺点:
- 标记和清除两个过程的效率都不高
- 清除后的空间不会进行移动,因此清除后会产生大量的不连续的空间碎片,空间碎片太多可能会导致以后再需要分配较大的内存时,无法找到足够大并且连续的内存空间而不得不提前进行垃圾回收
3.2.2.标记整理法
一样先检查对象是否可回收,并将可回收的对象标记出来,之后不是直接清除,而是让所有存活的内存都紧凑在一端,然后再清理掉边界的内存,该垃圾回收算法适用于对象存活率高的场景(老年代)
优点是不会产生空间碎片,但由于需要较多的复杂操作,效率将会变低
3.2.3.复制法
会将可用内存空间分成相等容量两块区域(From区 和 To区),每次都先使用FROM区,等FROM区用完了,就将FROM区存活的对象复制到TO区,再将FROM区进行清除,清除完后再交换两个区的地址
优点也是不会产生空间碎片,实现简单,效率高。但缺点是会占用双倍的内存空间
3.2.4.分代回收法
在虚拟机中不同对象的生命周期是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。
分代回收是采用标记清除、标记整理、复制法三中算法结合的算法。
堆内存一般分为新生代(Young Generation)、老年代(Old Generation),用来存放不同生命周期的对象
3.2.4.1.新生代(Young Generation)
新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。
新生代内存按8:1:1的比例分为一个伊甸区(eden) 和两个幸存区(FROM区和TO区) 也叫Survivor区
新生代的GC叫MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。
3.2.4.2.老年代(Old Generation)
老年代存放的都是一些生命周期较长的对象,相对垃圾清理的频率比较低。老年代的内存比新生代内存要大。
当老年代满时会触发Major GC(Full GC)
3.3.回收类型
3.3.1.Minor GC
新生成的对象会先放在新生代中的eden区,当eden区的内存块满时,触发Minor GC,Minor GC对新生代进行回收,不会影响到年老代。
也会触发STW,但是时间比较短
- 先将Eden区中存活的对象复制到幸存区中的s0区,回收Eden区,存活的对象被标记为存活1次
- 下一次触发Minor GC时,会将Eden区和s0区中存活的对象复制到s1区中,然后清除Eden区和s0区,存活对象的存活次数+1,然后交换s1和s0区的地址(这里s0区和s1区就是From区 和 To区,在幸存区中空的区就是TO区,有对象的就是FROM区)。
- Minor GC重复以上的过程,当其中有对象存活的次数到达15次时(
-XX:MaxTenuringThreshold
默认是15),就会从新生代晋升到老年代 - 随着MinorGC一次又一次的进行,老年代的对象越来越多,当老年代的空间不足时会先尝试触发Minor GC,如果仍空间不足,就会触发Full GC
3.3.2.Full GC
对整个堆进行回收,包括新生代和老年代,由于Full GC要对整个堆空间进行垃圾回收,所有要比Minor GC要慢,STW时间更长。
也可通过调用System.gc()
触发Full GC。
4.垃圾回收器
4.1.串行垃圾回收器
单线程的垃圾回收器,在单核CPU的环境下,效率更高,串行的垃圾收集器有两种,Serial与Serial Old
新生代采用Serial,用的是复制算法,老年代使用Serial Old,用的是标记整理法
-XX:+UseSerialGC=Serial+SerialOld
4.2.吞吐量优先回收器
多线程的垃圾回收器,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),也就是高效率利用cpu时间,但由于回收的时候是多线程的一起回收的,那么在垃圾回收时CPU使用率会升高,可以通过-XX:ParallelGCThreads
参数调整垃圾回收时线程的数量。
-XX:MaxGCPauseMillis
可以设置最大停顿时间
-XX:GCTimeRatio
设置吞吐量大小
XX:+UseAdaptiveSizePolicy
随着GC,会动态调整新生代的大小,Eden,Survivor比例等,以提供最合适的停顿时间或者最大的吞吐量
- Parallel Scavenge: 针对新生代的垃圾回收器,采用**“复制”算法**
- Parllel Old: 针对老年代的垃圾回收器,采用标记整理算法
4.3.时间响应优先
- ParNew: Serial收集器的多线程版本,默认开启的收集线程数和cpu数量一样,用于新生代收集,复制算法
- Concurrent Mark Sweep(CMS): 并发的垃圾回收器,是一种以获得最短回收停顿时间为目标的收集器,作用于老年代,采用标记清除算法 ,它的运作过程分为 4 个步骤:
- 初始标记(CMS initial mark)仍会触发STW,但仅标记GC Roots对象,速度很快。
- 并发标记(CMS concurrent mark)GC Roots Tracing,可以和用户线程并发执行。
- 重新标记(CMS remark)在并发标记过程中,有些对象可能产生变动,所以要重新标记,这个阶段的时间一般会比初标记稍长一些,但远比并发标记的时间短。也会触发STW
- 并发清除(CMS concurrent sweep)清除对象,可以和用户线程并发执行。
缺点:
由于CMS是并发的,所以堆CPU的资源很敏感;
因为是基于标记清除算法,所以不可避免会产生空间碎片;
清除的过程也是并发的,所以在清除对象时,还会产生新的垃圾,这些垃圾被称为浮动垃圾,而且只能通过下一次GC清除这些垃圾
4.3.1.G1(Garbage First)
在JDK1.9中,G1成了默认回收器,并且废弃了CMS回收器。可以通过-XX:+UseG1GC
参数指定使用G1回收器。
可以通过-XX:MaxGCPauseMillis
设定最大STW停顿时间,默认250,不一定能达到,但G1会尽可能的达到设定的停顿时间。
G1的堆区在分代的基础上,引入了分区(Region) 的概念。G1将堆分成了许多大小相等的Region,各个Region被标记为 E(Eden区)、S(Survivor区)、O(老年代) 和 H(Humongous巨型对象区域),估计每个Region中垃圾的比例,优先回收垃圾多的Region,所有被叫做Garbage First
G1无需回收整个堆,而是选择一个Collection Set(CS)。
- 回收阶段
- 初始标记(Initial Marking)同CMS,会触发STW,但仅标记GC Roots对象,速度很快。
- 并发标记(Concurrent Marking)是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking)在并发标记过程中,有些对象可能产生变动,所以要重新标记,也会触发STW,但是可并行执行
- 筛选回收(Live Data Counting and Evacuation)对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来确定回收计划。
特点:
能充分利用多CPU、多核环境下的硬件优势;
可以并行来缩短(Stop The World)停顿时间;
也可以并发让垃圾收集与用户程序同时进行;
分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象;
应用场景可以面向服务端应用,针对具有大内存、多处理器的机器;
采用标记-整理 + 复制算法来回收垃圾
4.4.小结
垃圾回收器 | 分类 | 作用位置 | 使用算法 | 特点 |
---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 |
Serial Old | 串行 | 老年代 | 标记整理法 | 响应速度优先 |
Parallel | 并行 | 新生代 | 复制算法 | 吞吐量优先 |
Parllel Old | 并行 | 老年代 | 标记整理法 | 吞吐量优先 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 |
CMS | 并发 | 老年代 | 标记清除法 | 响应速度优先 |
G1 | 并行 | 新生代 | 复制算法 | 响应速度优先 |
5.类加载机制
编写的.java文件经过java编译器编译成.class文件,class文件中保存着一些虚拟机指令,将.class加载到虚拟机内存的过程叫类加载。
5.1.类加载过程
5.1.1.加载(Loading)
虚拟机将类信息加载到元空间(方法区),并利用字节码文件在堆内存中创建一个Class对象,用来做方法区中类信息数据的接口。
5.1.2.链接(Linking)
第二大阶段,链接又分为三个小阶段
- 验证(Verifivation)
验证类是否符合JVM规范,安全性检查,例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型、语义分析、字节码校验等 - 准备(perparation)
为static分配空间,然后设置默认值,赋值在初始化阶段,如果static是final的基本类型或者字符串常量,那么编译阶段值就确定了,赋值会在准备阶段完成 - 解析(Resolution)
在未解析之前,对象之间的引用的是一种符号引用,并没有什么实际的意义,所以需要将常量池中的符号引用解析为直接引用,直接引用可以直接指向目标对象的内存地址。
5.1.3.初始化(Initialization)
初始化即调用() v ,虚拟机会保证这个类的构造方法的线程安全。如果在准备阶段static并不是final的基本类型,则会在这个阶段进行赋值
public class Loading {
public static void main(String[] args) {
System.out.println(A.str); // 不会初始化
System.out.println(A.i); // 不会初始化
System.out.println(A.class); // 不会初始化
System.out.println(new A[10]); // 不会初始化
System.out.println(A.i1); // 会初始化
System.out.println(B.i2); // 子类初始化时,如果父类未初始化,父类会先初始化
System.out.println(B.a); // 子类访问父类静态变量,值初始化父类
}
}
class A {
final static int i = 20; // 在准备阶段就已经赋值,不会初始化
final static String str = "str"; // 在准备阶段就已经赋值,不会初始化
final static Integer i1 = 10; // 准备阶段未赋值,会初始化
static int a = 30; // 准备阶段未赋值,会初始化
static {
System.out.println("A init");
}
}
class B extends A {
static int i2 = 10;
static {
System.out.println("B init");
}
}
5.2.类加载器
在java虚拟机中又3中类加载器:Bootstrap加载器、Extension加载器、System记载器
各个加载器各司其职,加载不同范围的类
名称 | 范围 | 说明 |
---|---|---|
Bootstrap加载器 | JAVA_HOME/jre/lib | 无法直接访问 |
Extension加载器 | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application加载器 | classpath | 上级为Extension |
5.3.双亲委派模式
所谓双亲委派模式,就是指调用类加载器的loadClass方法时,查找类的规则:
一个加载器在加载时,会先找上级加载器,如果存在上级加载器,就让上级器去加载,如果没有上级加载器或是上级加载器没找到类,就由自己加载,如果找不到类,就会报ClassNotFound异常
- 优势
通过这种层级关可以避免类的重复加载,当上级已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,由上往下加载,不会使JDK的类和自己写的类混淆
5.4.自定义加载器
步骤:
- 继承ClassLoader父类
- 重写findClass方法,注意不是loadClass,否则不会走双亲委派模式
- 读取类文件的字节码
- 调用父类的defineClass方法来加载
- 使用者调用该类加载器的loadClass方法
6.内存模型(Java Memory Model)
java内存模型,简称JMM。比如JVM在执行i = i +1
操作的过程中,首先需要从主内存中读取i
到工作内存中,再在工作内存中进行+1
的操作,然后再写回主内存。
在这个过程中,如果是多线程环境下,就可能会造成一些线程安全问题,也就涉及到了JMM的三大特性。
6.1.JMM三大特性
- 原子性:一个或多个操作,要么全部执行,要么全部不执行。
- 可见性:只要有一个线程对共享变量的值做了修改,其他线程都将马上收到通知,立即获得最新值。
- 有序性:java指令在编译时经过优化,可能会出现重排序现象,有序性就是确保虚拟机禁止指令重排序。volatile 和synchronized 都可以确保有序性
7.VM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=n |
晋升详情 | -XX:+PrintTenuringDistribution |
打印GC详情 | -XX:+PrintGCDetails |
Full GC前Minor GC | -XX:+ScavengeBeforeFullGC |
问题
垃圾回收是否涉及栈内存
不涉及,因为每个栈帧在方法执行完成之后会自动出栈
栈内存的分配越大越好吗
如果栈内存太大,可运行的线程数就会变小
一般采用系统默认的大小,但可以通过-Xss1024k
手动分配栈的内存大小
JVM内存【线程数*(最大栈容量)+最大堆值+其他内存(忽略不计或者一般不改动)】= 机器最大内存 - 直接内存
对象都是优先分配在年轻代上的吗?
不是。当新生代内存不够时,老年代分配担保。而大对象则是直接在老年代分配。