JVM原理
一、运行时数据区域
- 线程私有:程序计数器、java虚拟机栈、本地方法栈
- 线程共有:方法区(运行时常量池)、堆、直接内存
1、程序计数器
作用:可以看成是当前线程所执行的字节码的行号指示器(即表征程序运行到何处)。如果执行java方法,则记录正在执行的虚拟字节码指令的地址;如果执行本地(native)方法,则为空(undefined)
特点:
- 线程私有
- 不会存在内存溢出
2、java虚拟机栈
2.1、定义
虚拟机栈:每一个线程运行时所需的内存空间。一个线程中,会包含多个java方法,那么每执行一个java方法,都会创建一个栈帧
栈帧:每个java方法运行时需要的内存空间。用于存储局部变量表、操作数栈等信息。
局部变量表:存放编译时期可知的各种基本数据类型,引用类型等。表的大小在编译时期就已经确定,并且运行时期不可改变。
2.2、栈的大小
栈的大小:可以通过**-Xss**这个虚拟机参数来指定每一个线程的java虚拟机栈的大小。
**栈内存溢出:**抛出StackOverFlowError异常
- 栈帧过多导致栈内存溢出(递归方法中常常出现)
- 栈帧过大导致栈内存溢出(不常见)
异常:该区域可能引起以下两种异常:
- 当线程请求的栈深度超过最大值,会引发StackOverFlowError异常
- 栈进行动态扩展时,如果无法申请到足够的内存,会抛出OutofMemoryError异常
2.3、特点:
- 每个栈由多个栈帧组成
- 每个线程只能有一个活动栈帧,对应当前正在执行的java方法(栈顶部的栈帧是活动栈帧)
- 线程私有
3、本地方法栈
为本地方法提供内存空间。
本地方法:一般指用其他语言(c/c++/汇编)编写的,并且被编译为基于本机硬件和操作系统的程序,这些方法需要特别处理。
4、堆
- 是java虚拟机中最大的内存区域,存放对象实例,是垃圾收集的主要区域。
- 通过new关键字创建对象都会使用堆内存
4.1、特点
- 线程共享的,堆内存中的对象都需要考虑线程安全问题
- 有垃圾回收机制
4.2、堆的大小
大小:堆不需要连续的内存,并且可以动态扩展其大小,扩展失败会抛出OutofMemoryError异常。可以通过-Xms和-Xmx这两个虚拟机参数来指定一个程序的堆内存大小,-Xms设置初始值、-Xmx设置替代值。
堆内存溢出:抛出OutofMemoryError异常
4.3、堆内存诊断
- jps工具:查看当前系统中有哪些java进程(查询进程id)
- jmap工具:jmap -head 进程id 查看堆内存占用情况
- jconsole工具:图形界面形式,多功能检测工具,可以连续监测
5、方法区
5.1、定义
用于存放已被加载的类信息、常量、静态常量等信息。不需要连续的内存,可以进行动态扩展,扩展失败会抛出OutofMemoryError异常。
对这块区域进行垃圾回收的主要目标是常量池的回收和类的卸载,但是一般比较难实现。
5.2、实现方式
方法区是一个概念,其有具体的实现方式:
- 在JKD1.8之前,HotSpot方法区的实现是永久代,采用的是堆内存的一部分作为方法区
- 在JDK1.8之后,HotSpot将永久代移除,方法区的实现是元空间,采用的是本地内存(操作系统内存)的一部分作为方法区
5.3、内存溢出
抛出OutofMemoryError异常,通常是因为加载的类太多
- JDK1.8之前,永久代内存溢出
- JDK1.8以后,元空间内存溢出
5.4、运行时常量池
运行时常量池是方法区的一部分,主要为了存放程序中的常量值。
除了在编译期间生成常量,还运行动态生成常量,使用String类的intern()
public class JvmTest {
public static void main(String[] args) {
String s1="abc"; //放入运行时常量池的StringTable中
String s2="abc";
String s3=new String("abc"); //new出来的对象都在堆内存中,只是这个对象的值是“abc”
System.out.println(s1==s3); //false
System.out.println(s1==s3.intern()); //true,将堆内存中的"abc"字符串转换成常量放入StringTable中
}
}
public class JvmTest {
public static void main(String[] args) {
String s=new String("a")+new String("b"); //["a","b"]
String s2=s.intern(); //["a","b","ab"]
String s3="a"+"b"; //直接在串池中寻找字符串"ab"
System.out.println(s2==s3); //true
}
}
执行第一句代码时,在串池中放入了常量"a"、“b”。此时s相当于s=new String(“ab”),这个new出来的对象放在堆内存中,其值是”ab“。
执行第二句代码时,会尝试将"ab"字符串放入串池,如果有则不放入,如果没有则放入串池,并把串池的对象返回
6、直接内存
- 是操作系统的内存,不属于java虚拟机的内存。
- 在JDK1.4中新发布了NIO类,使用Native函数库直接分配堆外内存,避免了在堆内和堆外来回拷贝数据的过程,显著提高性能。
- 不受JVM内存回收管理
注意
-
如何在堆中给对象分配内存???
两种分配方式:指针碰撞和空闲列表
-
对象的访问定位???
两种访问方式:句柄访问和指针直接访问
句柄访问:java堆中会划分一块内存作为句柄池,引用变量存储的是句柄地址,而句柄中包含了两个地址,一个是对象实例数据,一个是对象类型数据(存放于方法区中)
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIyVGduV2YfNWawNCM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TP350dNpmTxUlaNBDOsJGcohVYsR2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLwgTM3QTNxUTM4AzNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
**直接指针访问:**引用变量存储的直接是对象地址,堆中不会分句柄池,对象地址中包含了对象类型数据的地址
二、垃圾回收
1、判断一个对象是否可回收
1.1引用计数算法
在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就加1,当引用失效的时候(变量记为null),计数器的值就减1。但Java虚拟机中没有使用这种算法,这是由于如果堆内的对象之间相互引用,就始终不会发生计数器-1,那么就不会回收。
1.2可达性分析算法:
通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何的引用链相连时,证明此对象不可用。
可作为GC Roots的对象:
- 虚拟机栈中局部变量表引用的对象
- 本地方法栈中引用的对象
- 方法区的类属性所引用的对象
- 方法区中常量所引用的对象
引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判断对象是否可被回收都与引用有关。
-
强引用
使用new关键字来创建一个强引用。被强引用关联的对象不会被回收。
-
软引用
使用SoftReference类来创建软引用。被软引用的对象只有在内存不足的情况下才会被回收
Object obj=new Object(); SoftReference<Object> sf=new SoftReference<Object>(obj); obj=null; //使得第一行创建的new Object()对象只被sf这个软引用对象关联
-
弱引用
使用WeakReference类来创建弱引用。被弱引用的对象一定会被回收,它值存活到下一次垃圾回收发生之前
Object obj=new Object(); WeakReference<Object> wf=new WeakReference<Object>(obj); obj=null;
-
虚引用
使用PhantomReference来创建虚引用。
虚引用又被称为幽灵引用或幻影引用,无法通过一个虚引用得到一个对象,其设置的唯一目的是能在这个对象被回收时收到一个系统通知。
Object obj=new Object(); PhantomReference<Object> pf=new PhantomReference<Object>(obj); obj=null;
2、垃圾回收算法
2.1 标记-清除算法
先标记出要回收的对象(一般使用可达性分析算法),再去清除,但会有效率问题和空间问题。标记的空间被清除后,会造成我的内存中出现越来越多的不连续空间,当要分配一个大对象的时候,再次进行寻址的要花费很多时间,可能会再一次触发垃圾回收。
2.2 标记-整理算法
与标记-清除算法类似,只是清除对象后,还要将所有存活的对象都向一端移动,并更新引用其对象的指针。
- 优点:不会产生内存碎片
- 缺点:需要移动大量对象,处理效率比较低
2.3 复制算法
将内存划分为大小不同的2块,每次只使用其中一块,当这一块内存用完了就将仍存活的对象复制到另一块上面,然后把使用过的那一块全部清除。
缺点:只用了内存的一半
目前常用的虚拟机都采用这种复制算法来回收新生代,主要将新生代分为一块较大的Eden和2块较小的Survivor空间,每次只使用Eden和一块Survivor,当回收时,将Eden和Survivor中还活着的对象一次性复制到另一块Survivor上,最后清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认,的Eden和Survivor的大小比例是8:1,这样每次新声嗲中可用的内存为整个新生代容量的90%(80%Eden+10%Survivor),只有10%的内存会“浪费”。
2.4 分代收集算法
将内存分为新生代与老年代,不同的代使用不同的收集算法
- 新声代使用:复制算法
- 老年代使用:标记-清除算法或标记-整理算法
3、垃圾回收器
根据要回收的内存区域的不同,可以使用不同的垃圾收集器。
3.1、串行回收器
- 单线程。只有一个线程(一个垃圾回收器)进行垃圾回收,在回收的期间,所有的用户线程都被阻塞,。
- 适合堆内存较小的情况下使用,适合个人电脑
- Serial收集器
开启方式:-XX:+UseSerialGC=Serial(新生代)+SerialOld(老年代)
新生代使用复制收集算法,老年代使用标记-整理算法。
3.2、吞吐量优先回收器
- 多线程。多个线程(可理解为有多个垃圾回收器)进行垃圾回收,在回收期间,所有的用户线程被阻塞。
- 适合堆内存较大,多核CPU
- 让单位时间内STW的时间最短(在单位时间内,让垃圾回收时间所占比例越小)
- Parallel收集器,并行执行
开启方式:-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
3.3、响应时间优先回收器
- 多线程。多线程进行垃圾回收,在回收期间,一定阶段内,用户线程继续执行,不被阻塞
- 适合堆内存较大,多核CPU
- 尽可能让STW的单次时间最短(让一次垃圾回收的时间越小)
- CMS收集器,并发执行,适用于老年代,采用标记-清除收集算法
开启方式:-XX:+UseConcMarkSweepGC (老年代)~ -XX:+UseParNewGC(新生代)、
如果CMS并发失败,会退化到SerialOld回收器
-
初始标记
标记老年代中所有的GC Roots对象和年轻代中活着的对象引用到的老年代的对象,时间短;
-
并发标记
从“初始标记”阶段标记的对象开始找出所有存活的对象;
-
重新标记
用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象,时间短;
-
并发清理
清除那些没有标记的对象并且回收空间。
缺点:占用大量的cpu资源、无法处理浮点垃圾、出现ConcurrentMarkFailure、空间碎片。
3.4、G1回收器
JDK9废弃了CMS回收器,默认使用Garbage First回收器。
特点:
- G1可以直接对新生代和老年代一起回收,其他回收器的范围都是整个新生代或老年代
- G1将堆划分为多个大小不同的独立区域(Region),这些Region就分为了Eden、Survivor、Old区域
使用场景:
- 同时注重吞吐量和低延迟,默认的暂停目标是200ms
- 超大堆内存,将堆分为多个大小相等的Region
- 整体上采用标记-整理算法,两个区域之间采用复制算法
相关参数:
- -XX:+UseG1GC 开启方式(JKD9默认开启)
- -XX:G1HeapRegionSize=Size 设置堆中Region的大小
- -XX:MaxGCPauseMillis=time 设置暂停时间(垃圾回收的时间)
回收过程:循环进行
- Young Collection新生代回收:采用标记-整理算法,在该过程中会进行GC Root的初始标记
- Young Collection+CM新生代回收与并发标记:老年代占用堆空间比例达到一定阈值,进行并发标记(不会STW)。默认的阈值是45%
- Mixed Collection混合回收:对E、S、O区域进行一次全面的垃圾回收(Full GC)
三、内存分配与回收策略
- Minor GC:回收新生代。因为新生代对象存活时间很短,所以Minor GC会持续执行,执行的速度一般也会比较快。
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此Full GC很少执行,执行速度会比Minor GC慢很多。
1、内存分配原则
- 优先分配到Eden。当Eden不够时,发起Minor GC
- 大对象直接分配到老年代。-XX:PretenureSizeThreshold 大于该值的对象直接分配在老年代
- 长期存活的对象直接分配到老年代。为对象定义年龄计数器,对象在Eden出生并经过Minor GC依然存活,将移动到Survivor中,年龄就增加1岁,增加到一定年龄则移动到老年代中。-XX:MaxTenuringThreshold用作定义年龄的阈值。
- 动态对象年龄判断。虚拟机并非永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
-
空间分配担保。
(1)在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么Minor GC确认是安全的,可以执行。
(2)如果不成立的话,虚拟机会查看HandlePromotionFailure的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC ;如果小于,或者HandlePromotionFailure的值表示冒险,那么就要进行一次Full GC。
2、Full GC触发条件
- 调用System.gc():只是建议虚拟机执行Full GC,但虚拟机不一定真正去执行
- 老年代空间不足:常见的是大对象直接进入老年代、长期存活的对象进入老年代等,为了避免:
(1)尽量不要创建过大的对象
(2)通过-Xmn虚拟机参数将新生代内存调大,让对象试图在新生代被回收掉,不进入老年代。
(3)通过-XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多幸存。
- 空间分配担保失败:使用复制算法的Minor GC需要老年代的内存空间作担保,如果担保失败会执行一次Full GC
- JDK1.7之前的永久代空间不足
- 并发模式故障:执行CMS GC的过程中同时有对象要加入老年代,而此时老年代空间不足(可能是GC过程中浮动垃圾过多导致暂时性的空间不足),便会报并发模式失败错误,并触发Full GC。
四、类加载机制
类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存
1、类的生命周期
类从class文件---->进入java虚拟机------->最终卸载的整个过程,分为7个阶段:加载、验证、准备、解析、初始化、使用、卸载
2、类的加载过程
包括类的生命周期中的5个部分:
- 加载:查找类的二进制文件(class文件)
|-方法区:存放类信息
|-堆:存放class文件对应的类实例
- 验证:确保Class的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 准备:为类的静态变量进行初始化,分配空间并赋予初始值(使用方法区内存)
public static int value=123; //准备阶段value赋予的初始值为0 public static final int value=123; //如果类变量是常量,即用final修饰,则准备阶段value赋予的初始值为123
- 解析:将常量池的符号引用替换为直接引用
- 初始化:JVM对类进行初始化,对类的静态变量赋予正确的值
3、类初始化时机
-
遇到new、getstatic、putstatic、invokestatic四条指令代码时,如果没有进行类初始化,则必须先触发其初始化。
|–常见情况:使用new关键字创建类实例、读取或设置一个类的类静态变量值、调用一个类的类方法
- 使用java.lang.reflect包方法对类进行反射调用时
- 当初始化一个类时,发现其父类还没有被初始化,则需要先触发父类初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(含main方法的类),虚拟机会初始化这个主类
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且此方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
4、类加载器
从java虚拟机角度分类:
- 启动类加载器:使用C++实现,是虚拟机本身的一部分
- 所有其他类的加载器:使用java实现,独立于虚拟机,继承自抽象类java.lang.ClassLoader
开发角度分类:
-
启动类加载器:BootStrapClassLoader(C语音写的)
|-(JDK/JRE/LIB java.*)所有类的类加载器
-
扩展类加载器:ExtClassLoader
|-(JDK/JRE/LIB javax.*)所有类的类加载器
-
应用程序类记载器:AppClassLoader(如果用户没有自定义加载器,那么用户定义的类默认使用该加载器)
|-(用户自己定义的类)类加载器
-
用户自定义类加载器
|-文件流、网络、数据库
***双亲委派模型:***如果一个类收到了类加载请求,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中。只有当父类加载器在它的搜索范围内没有找到所需的类时,即无法完成加载时,子类加载器才会尝试自己去加载这个类(先依次向上,然后在向下)、
优点:
- 防止重复加载一个类,保证数据安全
- 防止java核心API库被随意篡改
打破双亲委派机制:用户自定义类加载器
参考文章:
https://blog.csdn.net/TJtulong/article/details/89598598
[https://cyc2018.github.io/CS-Notes/#/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机)