JVM运行时数据区域
上图:
堆(JAVA HEAP)
堆主要管存储,主要用于存储对象以及数组,堆主要有以下几个特点
- 存储的是我们使用new指令创建的对象,不存放基本类型以及对象引用
- 垃圾回收的主要区域,可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。(幸存者)
- 线程共享区域,会参生线程安全问题
- 内存溢出时会发生outOfMemoryError错误
堆内存结构:
虚拟机栈(JVM Stacks)
为虚拟机执行Java方法(字节码)服务。特点:
- 线程私有,生命周期与线程相同
- 描述java方法执行的内存模型,每个方法被执行的时候会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每个方法被调用直至执行完成的过程就对应一个栈帧在虚拟机中入栈到出栈的过程
- 存放基本数据类型以及对象的引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
- 内存溢出时可能会产生两种错误,当线程请求的栈深度大于虚拟机所允许的最大深度会抛出StackOverFlowError异常,如果虚拟机栈可以动态扩展,当扩展时无法申请到足够内存时会抛出OutOfMemoryError异常
本地方法栈(Native Method Stacks)
本地方法栈是为使用到的Native方法服务
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
方法区(Method Area)
方法区(有时也称为永久代(Permanent Generation)或元空间(Metaspace,自Java 8起))用于存储类的元数据、静态变量、编译后的代码和常量池等信息。方法区是所有线程共享的,因此也需要关注线程安全问题。特点:
- 线程共享
- 存储类的元数据、静态变量、编译后的代码和常量池等信息
- 内存溢出时会抛出OutOfMemoryError异常
程序计数器(Program Counter Register)
程序计数器是一个较小的内存区域,用于存储当前线程正在执行的字节码指令的地址
。每个线程都有自己的程序计数器,线程之间互相独立。当线程执行一个方法时,程序计数器会跟踪该方法中的字节码指令流。
垃圾回收(GC)
垃圾回收类型
Minor GC(Young GC)
针对新生代的垃圾收集
原理:
Minor GC使用复制算法,将存活的对象从Eden区(对象刚被创建的区域)和Survivor区(存活时间较长的区域)复制到另一个Survivor区,同时清理掉不再存活的对象。
步骤:
- 将Eden区和一个Survivor区(称为From区)中的存活对象复制到另一个Survivor区(称为To区)。
- 清空From区。
- 将From区和To区交换角色,保证下次GC时可以复制到正确的区域。
每次垃圾回收后处于From区的对象如果还存活那么将会移动到TO区若此时TO区没有足够的空间则直接将该对象移动至老年代。若TO区有足够空间则将该对象移动至TO区同时年龄加一,若年龄达到老年代的界限也会被移动至老年代默认老年代年龄为15
Major GC(Old GC)
针对老年代的垃圾收集
原理:
Major GC使用标记-整理算法,首先标记所有存活对象,然后将存活对象向内存一端移动,然后清理掉其余的内存。
步骤:
- 标记所有存活的对象。
- 将存活的对象向内存一端移动,保证存活对象之间没有间隙。
- 清理掉不再存活的对象。
Full GC
针对整个java堆以及方法区(元空间)的垃圾收集
原理:
Full GC会同时清理新生代和老年代,以及可能的永久代(如果有的话),可以回收更多的内存,但会造成更长的停顿时间。
步骤与Major GC一样只是针对的范围更加的广泛
如何判定回收
判定对象是否销毁一般使用两种方法:
-
引用计数法
为对象添加一个引用计数器,每当对象在一个地方被引用,则该计数器加1;每当对象引用失效时,计数器减1。但计数器为0的时候,就表白该对象没有被引用。
-
可达性分析算法
通过一系列被称之为“GC Roots”的根节点开始,沿着引用链进行搜索,凡是在引用链上的对象都不会被回收。
回收算法
标记清除算法
对无效的对象进行标记然后清除
- 优点:
- 实现简单,容易理解。
- 不需要连续的内存空间,可以处理非连续分布的对象。
- 缺点:
- 回收后会产生大量的内存碎片,可能导致后续分配较大对象时出现空间不足的情况。
- 回收过程中需要停顿整个程序,造成较长的暂停时间,影响程序的响应性能。
标记复制算法
标记复制算法会将堆分为两块,每次垃圾回收只回收其中的一块,回收后将存活的对象移动至另一块
- 优点:
- 回收效率高,不会产生内存碎片。
- 内存分配时,只需移动指针,非常高效。
- 可以并行进行标记和复制过程,降低了停顿时间。
- 缺点:
- 需要两个相同大小的内存空间,会造成内存利用率下降。
标记整理算法
标记整理算法结合了标记清除与标记整理的有点实现的一个算法,标记过程与标记清除一样但是回收时会将存活的对象移动到堆的一端。避免了内存碎片的产生。
- 优点:
- 解决了标记清除算法产生的内存碎片问题,回收后可以整理出连续的内存空间。
- 回收后,分配大对象时不容易出现空间不足的情况。
- 缺点:
- 需要移动存活对象,回收效率相对较低,特别是存活对象较多时。
- 每次垃圾回收时会暂停所有用户线程
垃圾收集器
Serial
Serial收集器是一个很基础的垃圾收集器也是最老的一个收集器,单线程工作。使用该收集器工作无论是minor gc还是Full Gc清理堆空间时所有的应用线程都会被暂停
ParNew
parNew实际上与Serial收集器差不多,区别为parNew是可以多线程并行工作的。
Parallel Scavenge
Parallel Scavenge收集器也是一款新生代收集器,基于标记——复制算法实现,能够并行收集的多线程收集器和 ParNew 非常相似。
Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
Serial Old
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
CMS
CMS 收集器设计的初衷是为了消除 Parallel 收集器和 Serial 收集器 Full gc 周期中的长时间停顿。CMS收集器在 Minor gc 时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。
类加载
类加载过程
类加载过程是将编译后的class文件搬运到JVM虚拟机中的过程
包括:
-
加载
查找并加载类的二进制数据,在java堆中创建一个java.lang.Class对象
-
验证
文件格式,元数据,字节码,符号引用验证
-
准备
为类的静态变量分配内存,并将其初始化为默认值
-
解析
将类中的符号应用变为直接引用
-
初始化
为类的静态变量赋予正确的值。初始化的触发条件主要有:
- 创建类的实例,通过new创建对象如果此时类没有被初始化那就执行初始化过程
- 访问类的静态资源时如果该类还没有被初始化就执行初始化
- 使用反射来动态加载类时会触发初始化
- 当程序启动时主类(包含main方法的类)会被初始化
- 子类初始化,如果一个类是另一个类的子类,当初始化子类时父类也会进行初始化
- 使用jvm参数初始化如:(-Djava.starup.class)也会触发初始化
在上面五个过程中加载,验证,准备,初始化的顺序是确定的。但是解析不确定在某些情况下会在初始化后开始
类的生命周期
类的生命周期包含了类加载的过程,只是在类加载的基础上多了两个步骤
-
加载
查找并加载类的二进制数据,在java堆中创建一个java.lang.Class对象
-
验证
文件格式,元数据,字节码,符号引用验证
-
准备
为类的静态变量分配内存,并将其初始化为默认值
-
解析
将类中的符号应用变为直接引用
-
初始化
为类的静态变量赋予正确的值。
-
使用
new出对象并在程序中使用
-
卸载
执行垃圾回收
类加载器
作用
- 将class字节码内容加载到内存中,并将这些静态数据转换成方法区运行时数据结构,然后在堆中形成代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
- 类缓存:标准的JavaSE类加载器按照要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间,不过JVM垃圾回收机制也会回收这些Class对象。
分类
-
引导类加载器(扩展类加载器的父类)
用c++编写,是JVM自带的类加载器,负责java平台核心库,用来装载核心类库,该加载器无法直接获取
-
扩展类加载器(系统类加载器的父类)
负责jre/lib/ext目录下的jar包或-Djava.ext.dirs指定的jar包装入工作库
-
系统类下加载器 (自定义类加载器的父类)
负责java-classpath或者 -D java.class.path所指的目录下的类与jar包装入工作,是最常用的加载器
-
自定义类加载器
双亲委派机制
Java类随着它的加载器一起具备了一种带有优先级的层次关系(当一个类加载的过程中,它首先不会去加载,而是委托给自己的父类去加载,父类又委托给自己的父类)
解决的问题:
- 避免重复加载
- 避免核心类被不同的的类加载器加载到内存中造成冲突和混乱,保证了java核心库的安全
反双亲委派机制
每个中间件先从自己的classpath 路径下加载类,加载不到时才委派给父加载器加载,保证加载到的三方jar中的类是自己应用lib目录下依赖的版本,解决不同中间件依赖同一个库的不同版本冲突问题(pandora容器隔离原理)
应用场景
- 解决依赖冲突
- 热加载/热部署
- jar包的加密
Java类随着它的加载器一起具备了一种带有优先级的层次关系(当一个类加载的过程中,它首先不会去加载,而是委托给自己的父类去加载,父类又委托给自己的父类)
解决的问题:
- 避免重复加载
- 避免核心类被不同的的类加载器加载到内存中造成冲突和混乱,保证了java核心库的安全
反双亲委派机制
每个中间件先从自己的classpath 路径下加载类,加载不到时才委派给父加载器加载,保证加载到的三方jar中的类是自己应用lib目录下依赖的版本,解决不同中间件依赖同一个库的不同版本冲突问题(pandora容器隔离原理)
应用场景
- 解决依赖冲突
- 热加载/热部署
- jar包的加密