JVM组成
JVM包括哪些区域
- 程序计数器
- 堆
- 虚拟机栈
- 方法区
- 直接内存
什么是程序计数器
程序计数器是线程私有的,每个线程中存在一份,内部保存字节码的行号,用于记录正在执行中的字节码指令地址。
Java堆
Java堆是线程共享的区域,主要用来保存对象实例、数组等,当内存不够时抛出OutOfMemoryError异常。
Java堆中包含年轻代和老年代。
- 年轻代包括Eden区和两个大小严格相同的Survivor区
- 老年代中保存生命周期长的对象,一般是一些老的对象
Java 1.7 和1.8中堆的区别
- JDK1.7的堆中有一个永久代,存储的是类信息、静态变量、常量以及编译后的代码
- JDK1.8的堆中移除了永久代,把数据存储到内存的元空间中,防止内存的溢出
什么是虚拟机栈
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用所占用的内存(包括参数、局部变量、返回地址等)
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
垃圾回收是否涉及栈内存
垃圾回收主要是指堆内存,当栈内存弹帧后,内存就会释放
栈内存分配越大越好吗?
否,栈内存默认1024K,栈帧过大会导致线程数变少
方法内的局部变量是否线程安全
- 如果方法内的局部变量没有逃离方法的作用范围,它是线程安全的
- 如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
什么情况下会导致栈内存溢出
- 栈内存过多导致栈内存溢出,典型:递归调用
- 栈帧过大导致栈内存溢出
堆栈的区别是什么
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。堆会有垃圾回收,栈不会
- 栈内存是线程私有的,而堆内存是线程共有的
- 两者的异常错误不同:
栈空间不足:java.lang.StackOverFlowError
堆空间不足:java.lang.OutOfMemoryError
如何解释方法区
- 方法区是各个线程的共享内存
- 主要存储类的信息、运行时常量池
- 虚拟机启动时创建,关闭虚拟机时释放
- 如果方法区的内存无法满足分配请求,则会抛出OutOfMemory: MetaSpace
介绍一下运行时的常量池
- 常量池:可以看作一张表,虚拟机指令根据表中信息找到要执行的类名、方法名、参数类型、字面量等信息
- 当类被加载,它的常量池信息就会放入运行时常量池,并将其中符号地址变为真实地址
直接内存
- 并不属于JVM的内存结构,不由JVM进行管理,是虚拟机的系统内存
- 常见于NIO操作,用于数据缓冲区,分配回收成本高,但读写性能高,不受JVM内存回收管理。
类加载器
什么是类加载器
JVM只会运行二进制文件,类加载器的作用是将字节码文件加载到JVM中,从而让Java程序能够启动起来
类加载器有哪些
- 启动类加载器 (BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库
- 扩展类加载器 (ExtClassLoader):主要加载JAVA_HOME/jre/lib/ext目录中的类
- 应用类加载器 (AppClassLoader):用于加载classPath下的类
- 自定义类加载器 (CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。
什么是双亲委派模型
加载某一个类,先委托上一级的加载器进行加载,如果上级也有上级,则会继续向上委托。如果该类委托没有被加载,自家在其尝试加载该类。
JVM为什么采用双亲委派机制
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后无需重复加载,保证唯一性。
- 为了安全,保证类库APP不会被修改
类装载的执行过程
- 加载:查找和导入class 文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:把类中符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:JVM开始从入口方法执行用户的程序代码
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。
垃圾回收
对象什么时候可以被垃圾回收器回收
如果一个对象没有任何的引用指向它,那么这个对象就是垃圾。如果定位了垃圾,就可能被垃圾回收器回收。
定位垃圾的方法有两种:
引用计数法
给对象添加一个引用计数器:
- 每当有一个地方引用它,计数器就加1;
- 当引用失效时,计数器减1;
- 任何时候计数器为0的对象都不能再被使用
实现简单,效率高,但是主流不使用。由于难以解决对象之间循环引用的问题
可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
GC Roots包括:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- JNI(Java Native Interface)引用的对象
JVM的垃圾回收算法有哪些
标记清除算法
将垃圾回收分为两个阶段
- (标记)根据可达性分析算法得出的垃圾进行标记
- (清除)对标记为可回收的内容仅小垃圾回收
会产生大量磁盘碎片,内存不连续
标记整理算法
和标记清除算法一样,将存活对象向内存另一端移动,然后清理边界以外的垃圾
复制算法
将原有的内存空间一分为二,每次只用其中的一块。清楚时,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
该方法不易产生碎片,但内存使用率低。
JVM中的分代回收
堆区域的划分
- 堆被分为了新生代和老年代两部分【1:2】
- 对于新生代,内部又分为了三个区域。Eden区,幸存者区survivor(存在两个)【8:1:1】
对象回收分代回收策略
- 新创建的对象,都会先分配到eden区
- 当eden区内存不足时,标记eden区与From中的存货对象
- 将存活的对象移动到To区中,然后将eden与from区的内存释放,同时互换to区与from区标记
- 当幸存者区中对象经历多次回收依然存活,移动到老年代(幸存者区内存不足或有大对象回提前移动)
MinorGC、Mixed GC、FullGC的区别是什么
- MinorGC 发生在新生代的垃圾回收,暂停时间短
- MixedGC 新生代+老年代部分区域的垃圾回收,G1收集器特有
- FullGC:新生代+老年代完整垃圾回收,暂停时间长
STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成。
JVM有哪些垃圾回收器
串行垃圾收集器
Serial 收集器 / Serial Old 收集 ,一种单线程收集器,在进行垃圾收集工作的时候必须暂停其它所有的工作线程,直到收集结束。
新生代使用标记-复制算法,老年代使用标记-整理算法
并行垃圾收集器
ParNew GC / Parral Old GC /Parallel Scavenge 收集器
ParNew收集器是Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
Parallel Scavenge 收集器 / Parral Old 收集器
在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(并发)垃圾收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS是基于**“标记-清除”算法**实现的,运作过程相比于前面几种垃圾收集器更加复杂,过程分为四个步骤:
- **初始标记:**暂停所有的其他线程,并记录下与root相连的对象;
- **并发标记:**同时开启GC和用户线程,用一个闭包结构记录可传达对象。同时记录标记期间发生引用更新的地方。
- **重新标记:**修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
- **并发清除:**开启用户线程,同时GC线程开始对未标记区域做清扫
G1垃圾收集器
面向服务器的垃圾收集器,主要针对多核大容量内存,GC停顿时间短且吞吐量高。
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专门为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分为三个阶段:新生代回收、并发标记、混合收集
- 如果并发失败(回收速度赶不上创建新对象的速度),会触发Full GC
包括以下特点: - 并行与并发
- 分代收集
- 空间整合:整体上基于标记-整理,局部通过标记-复制实现
- 可预测的停顿:可以建立起可预测的停顿时间模型,让使用者指定在长度为M毫秒的时间段内,消耗在垃圾收集上的时间不超过N毫秒
Yong Collection(新生代垃圾回收)
- 初始,所有区域处于空闲状态
- 创建对象时挑选一部分空闲区域作为eden区存储对象
- eden区需要垃圾回收时,挑选一个空闲区域作为幸存区,利用复制算法复制存活对象,暂停用户线程
- eden区再次不足时,将eden区和之前幸存区的存活对象复制到新的幸存区,其中较老对象转移到老年代
Yong Collection + Concurrent Mark(年轻代垃圾回收+并发标记)
- 当老年代占用内存超过阈值(45%),触发并发标记,无需暂停用户。
- 并发标记后,重新标记解决漏标问题,此时需要暂停用户进程
混合收集阶段
参与复制的有eden、survivor、old
eden+survivor --》survivor
survivor(较老对象)+old(高价值对象)–>old
强引用、软引用、弱引用、虚引用的区别
- 强引用:只要GC Root是能找到,就不会被回收
- 软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够的时候,会回收软引用对象
- 弱引用:需要配合WeekReference使用,只要进行了垃圾回收,就会把弱引用对象回收
- 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由Reference Handeler线程调用虚引用相关方法释放内存资源。