深入理解Java虚拟机
什么是JVM?
Java虚拟机(Java Virtual Machine)是运行Java字节码的虚拟机!对于不同的操作系统有特定的实现,相同的字节码(*.class 文件),它都会给出相同的结果。
*.class 文件 <—— javac 编译源文件
一次编译,随处运行
字节码 + (不同系统的)虚拟机是实现“一次编译,随处运行”的关键;
Java基本类型
byte、short、boolean、char、int、float、double、long
在Java体系中、基本类型占用的存储空间不受硬件架构的变化的影响。
构造器——constructor
可以重载、不可重写
重载: 方法名相同,参数列表不同(参数类型、参数个数、参数顺序至少一个不同),返回值类型、访问修饰符可以不同
重写: 方法名相同,对父类允许访问的方法的实现过程重新编写。发生在子类中,参数列表必须一致,返回值&抛出异常范围不大于父类;访问修饰符不小于父类。
区别 | -重载 | -重写 |
---|---|---|
英文 | Overload | Ooverride |
定义 | 方法名称相同,参数列表不同 | 方法名称、参数列表、返回类型一致 |
范围 | 同一类中 | 子类中 |
权限 | 不受权限控制 | 访问权限不小于父类 |
继承是子类使用父类的方法,而多态是父类使用子类重写的方法。
JVM体系结构(JVM运行在操作系统之上,与硬件无直接交互)
*.java ——> (Javac 编译) ——> *.class 文件 ——> JVM编译、解释 ——> 机器可执行的二进制机器码
JVM内存模型
栈管运行、堆管存储
其中,
(紫色)区域线程共有、发生GC;
(绿色)线程私有,内存很小,几乎不需要GC
程序计数器不会OOM,记录了方法之间的条用和执行情况。存储了指向下一条指令的地址,它是当前线程所执行的字节码的行号指示器。
方法区
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。<——规范
不同虚拟机有不同的实现,如永久代、元空间。
注意: 实例变量存在堆内存中和方法区无关
栈:
主管Java程序的运行,随线程的创建而创建,结束时释放,对栈而言不存在垃圾回收。
基本数据类型、对象的实例变量、实例方法都是在栈中分配。
主要存储:
-
本地变量
-
栈操作
-
栈帧数据
-
基本类型变量
-
引用
堆
一个JVM只有一个堆内存,堆内存的大小可调节,类加载器读取了class文件之后,需要把类方法、常量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆内存逻辑分三部分: 新生代:老年代:元空间
新生区由 Eden、from、to组成 三者占比8:1:1
伊甸园区和幸存区(from、to)
新生区记录类的诞生、成长、消亡
元空间
本质与永久代相似,不再在虚拟机内存中分配,直接在物理内存中分配
垃圾回收过程
当伊甸园空间用完时,程序继续创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(Minor GC), Eden区中的不再被其他对象引用的对象进行销毁,被引用的移动到幸存区的From区(0区);
再次触发Minor GC (复制 -> 清空 -> 互换) 时,0区和Eden区同时垃圾回收,将幸存的移到To区,如此往复,当移动15次后依然不能被回收,将幸存了15次的对象移到养老区;
当养老区满了,触发Major GC(Full GC ), 若Full GC后仍然无法保存对象,就会产生OOM。
survivor from 复制到survivor to 年龄+1;
-
Eden满 ——> Minor GC ——> 幸存的拷贝到 survivor from 区
-
Eden再满 ——> Eden & survivor from ——> Minor GC ——> 幸存的拷贝到 survivor to 区
复制之后有交换,谁空谁是To
垃圾回收时,确定对象无引用时,调用finalize()方法。
产生OOM的原因
-
堆内存设置不足
-
代码中创建了大量的对象,且长时间不能被垃圾收集器收集
打印详细的垃圾回收日志
-XX: +PrintGCDetails
FULL GC 永久代、养老区、新生区均垃圾回收
如何确定垃圾?
-
引用计数 数字为0时,无任何引用(不能解决循环依赖)
-
根可达
默认垃圾回收器
Serial
、Parallel Scavenge
、ParNew
用户回收新生代;SerialOld
、ParallelOld
、CMS
用于回收老年代。而G1
收集器,既可以回收新生代,也可以回收老年代。
CMS收集器
并发标记清除收集器,是一种以获得最短GC停顿为目标的收集器。适用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望停顿时间最短。是G1
收集器出来之前的首选收集器。使用标清算法。在GC的时候,会与用户线程并发执行,不会停顿用户线程。但是在标记的时候,仍然会STOP THE WORLD。
使用-XX:+UseConcMarkSweepGC
开启。开启过后,新生代默认使用ParNew
,同时老年代使用SerialOld
作为备用。
过程
- 初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,需要STW,很短暂。
- 并发标记:主要标记过程,标记全部对象,和用户线程一起工作,不需要STW。
- 重新标记:修正在并发标记阶段出现的变动,需要STW。
- 并发清除:和用户线程一起,清除垃圾,不需要STW。
优缺点
优点:停顿时间少,响应速度快,用户体验好。
缺点:
- 对CPU资源非常敏感:由于需要并发工作,多少会占用系统线程资源。
- 无法处理浮动垃圾:由于标记垃圾的时候,用户进程仍然在运行,无法有效处理新产生的垃圾。
- 产生内存碎片:由于使用标清算法,会产生内存碎片。
CMS垃圾收集器
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间, 和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。 最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。 CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
1)初始标记 只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
2)并发标记 进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
3)重新标记 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
4)并发清除 清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程一起并发工作, 所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
G1收集器
G1
收集器与之前垃圾收集器的一个显著区别就是——之前收集器都有三个区域,新、老两代和元空间。而G1收集器只有G1区和元空间。而G1区,不像之前的收集器,分为新、老两代,而是一个一个Region,每个Region既可能包含新生代,也可能包含老年代。
G1
收集器既可以提高吞吐量,又可以减少GC时间。最重要的是STW可控,增加了预测机制,让用户指定停顿时间。
使用-XX:+UseG1GC
开启,还有-XX:G1HeapRegionSize=n
、-XX:MaxGCPauseMillis=n
等参数可调。
特点
- 并行和并发:充分利用多核、多线程CPU,尽量缩短STW。
- 分代收集:虽然还保留着新、老两代的概念,但物理上不再隔离,而是融合在Region中。
- 空间整合:
G1
整体上看是标整算法,在局部看又是复制算法,不会产生内存碎片。 - 可预测停顿:用户可以指定一个GC停顿时间,
G1
收集器会尽量满足。
过程与CMS
类似。
- 初始标记。
- 并发标记。
- 最终标记。
- 筛选回收。
常见的垃圾回收算法
-
标记清除
-
拷贝
-
标记压缩
调整Java堆内存大小 -Xms,-Xmx
-Xms: 设置初始内存分配大小,默认为物理内存的1/64
-Xmx: 设置最大内存分配,默认为物理内存的1/4
JMM关于同步规定
-
线程解锁前,必须把共享变量刷回到主内存
-
线程加锁前,读取主内存中的最新值到线程的工作内存
-
加解锁是同一把锁
类加载器
负责加载class文件(cafe babe开头)到内存,并将class文件的内容转换成方法区中的运行时数据结构,且类加载器只负责加载,是否可以运行有执行引擎决定。
类加载器协同工作
实现机制: 双亲委派
双亲委派: 当类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是委派给自己的父类,找不到继续向上委派,到Bootstrap都加载不到,Bootstrap 就会向往下丢,向下找不到,知道最初发起加载的类加载去完成,若发起的这个加载类仍无法加载,丢ClassNotFounException!
沙箱安全: 用户定义的类不能污染JDK自带的类
用户自定义类加载器
-
继承ClassLoader
-
重写加载方法ClassLoader();
-
实例化class对象
虚线之上是虚拟机自带的加载器
-
Bootstrap 启动类加载器, C++实现
-
Extension 扩展类加载器, Java实现
-
Application 应用程序类加载器 Java 也叫 系统类加载器,加载当前应用的classpath下的所有类。
类一般成员——属性的初始化
在加载时进行默认初始化,在初始化时进行赋值
局部变量不会在加载时进行默认初始化,必须显示赋值
类加载(懒加载)
加载 ——> 连接 ——> 初始化 ——> 使用 ——> 卸载
连接阶段的主备阶段会给变量赋默认值
连接: 验证 ——> 准备 ——> 解析
验证: 文件格式、元数据、字节码 符号引用
准备: 正式为类变量分配内存并设置初始值;如果被final修饰,常量值同步指定为声明的值(一步到位)。
解析:常量池中的符号引用替换为直接引用
初始化:类加载的最后一步 执行clinit()方法
触发初始化
-
遇到new、getStatic、putStatic或invokeStatic四条字节码指令时,访问final变量除外,如果类没有进行初始化,触发;
-
对类反射调用时,未初始化,触发
-
初始化子类时,父类未初始化,触发
-
JVM启动时,用户需指明一个执行的主类,虚拟机会初始化这个主类
不被初始化
-
通过子类引用父类的静态字段
-
数组定义引用类 如:类[] c = new 类[10];
-
类的常量
本文针对JDK 1.8,仅作抛转引玉,因水平有限,文章或许有不足之处,望您不吝指出!