运行时数据区
组成概述
java运行时数据区,不同的虚拟机实现可能略微有所不同,但都会遵从java虚拟机规范,Java8虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个运行区域。
堆、方法区(元空间)主要用来存放数据 是线程共享的
程序计数器、本地方法栈、虚拟机栈是运行程序的,是线程私有的。
- 程序计数器
JVM中的程序计数器不是CPU中的程序计数器,可以简单理解为计数器。
是一块非常小的内存空间,运行速度是最快的,不会出现内存溢出情况。线程私有的。
作用:记录当前线程中的方法执行到的位置。以便于CPU在切换执行其他任务时,记录程序执行的位置,回来继续执行。
在运行时数据区中唯一一个不会出现内存溢出的区域。
- 本地方法栈
当我们在程序中调用本地方法时,会将本地方法加载到本地方法栈中执行。
也是线程私有的,如果空间不够,也会出现栈溢出错误。
- 虚拟机栈
出现背景:Java为了移植性好(跨平台)所以将运行程序的设计架构为栈结构运行,而不是依赖于CPU的寄存器架构。也正是因此现,其性能下降,实现同样功能需要更过的指令集.
栈是运行时的单位(加载方法运行)
堆是存储的单位 (存储对象的)
作用:运行方法,一个方法就是一个栈帧,栈帧中包含(局部变量(基本类型、引用地址)方法地址、返回地址)
栈的特点:是一种快速有效的分配存储模式,访问熟读仅次于程序计数器。
JVM直接对Java栈的操作只有两个:调用方法,(进栈) 执行结束后(出栈)。
对于栈来说不存在垃圾回收问题。
栈的异常:StackOverflowError(栈溢出)
递归调用方法太多。
栈中存储方法运行时需要的数据
栈的运行原理:第一个方法被加载 入栈 在方法中调用了其他方法,其他方法入栈,方法运行结束后出战,把结果返回给下一个要运行的方法。
栈帧的结构:
- 局部变量表:方法参数、定义的局部变量、基本类型直接存值,引用类型存地址。
- 操作数栈:栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。程序中的所有计算过程都是在借助于操作数栈来完成的。
- 动态链接:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
- 方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址
- java堆
概述
- 堆是JVM内存中核心的区域,用来存储创建出来的对象。
- 堆空间在JVM启动时被创建,大小可以设置。
- 物理上是不连续的,逻辑上是连续的。
- 堆中会发生垃圾回收。
堆内存的区域划分
- 新生代(新生区)
新生代分为:伊甸园区:幸存者0:幸存者1=8:1:1
- 伊甸园区(新生成的对象存储)
- 幸存者0(from)
- 幸存者1 (to)
- 老年代(老年区)
为什么要分区?
把不同的生命周期的对象存储在不同的区域,这样不同的区域可以使用不同的垃圾回收算法。可以提高垃圾回收的效率。
对象在堆内存中存放的过程:
新建的对象 存放在伊甸园区,第一次垃圾回收时,垃圾对象直接被回收掉,存活下来的对象会被存放到幸存者0区或幸存者1区
再次垃圾回收时会把幸存者0区(1区)存活的对象移动到幸存者1区(0区),然后将幸存者0区(1区)清空,依次交替执行。
每次保证有一个幸存者区域是空的,内存是完整的。
当对象经过15(阈值)次垃圾回收后,依然存活的对象将会被移动到老年区(老年区垃圾回收的频率会比较低)
这里说明一下最大为15次的原因:
- 在对象头中,它是由4位数据来多GC年龄进行保存的,所以最大值为1111也就是15。
堆各区域的占比:
新生代默认占整堆的1/3
新生代中的 伊甸园区:幸存者0区:幸存者1区=8:1:1
对象经过15次垃圾回收后,依然存活的,将被移动到老年区。
一般所说的JVM优化 就是调整JVM相关各区的参数。
堆中的参数设置参考文献
分代收集思想Minor GC、Major GC、Full GC
一般情况下收集新生代 Minor GC/Yong GC
当老年代的空间不足时 会触发Major GC/Old GC
整堆收集 Full GC
整堆收集触发的条件:
- System.gc();时
- 老年区空间不足
- 方法区空间不足
开发期间应该避免整堆收集。(在垃圾回收时,会有STW stop the world 回收时停止其他的线程的运行)
TLAB机制
- TLAB线程本地分配缓存区
- 在多线程情况下,可以在堆空间中通过-XX:UseTLAB设置 在堆空间中为线程开辟一块空间,用来存储线程中产生的一些对象,避免了空间竞争,提高了分配效率。
字符串常量池
JDK7之前字符串常量池在方法区(永久代)中存储。
JDK7及以后的版本将字符串常量池放到了堆空间。因为方法区只有触发了Full GC时才会回收,而在程序中需要大量的使用字符串,所以将字符串常量池的位置改变到了堆中,可以及时的回收无效的字符串。
- 方法区
概述
- 方法区也是一块内存空间,逻辑上属于堆,为了区分,称为元空间(JDK8之后)。
- 主要用来存储类的信息。
- 在JVM启动时创建,大小可以分配。
- 如果加载的类太多,也会报内存溢出错误。
- 是线程共享的
public class Demo2 {
public static void main(String[] args) {
String temp = "world";//字符串是常量,值存储在字符串常量池中
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = temp + temp;
temp = str;
str.intern();//将字符串存储到字符串常量池中
}
}
- 方法区的大小可以通过-XX:MetaspaceSize设置。
- 方法区在windows中默认的大小是21MB。
- 如果达到21MB会触发Full GC 因此可以将其值设置大一些,减少Full GC的触发。
- 方法区中主要回收运行时常量池类的信息
- 类的信息卸载(回收)条件是比较苛刻的。要满足三个条件:
- 该类以及子类的对象没有被引用。
- 该类的类加载器被卸载。
- 该类的Class对象也没有被引用。
方法区的内部结构
本地方法接口
- 什么是本地方法接口
- 一个Native Method 就是一个Java调用非Java代码的接口。
- 非Java语言实现的,例如C/C++。
- 为什么要使用本地方法?
- 我们的Java程序,需要与外部(计算机硬件)进行数据交互(例如:hashCode read() start() )
- 可以直接调用外部的本地方法实现。
- JVM解释器是用C写的,可以更好的与本地方法进行交互。
执行引擎
概述
- 前端编译(.java---->.class)
- 字节码 不等于 机器码。
- 需要JVM将字节码加载到内存中。
- 需要通过执行引擎将字节码解释/编译成机器码。
- 后端编译(.class—>机器码)
执行引擎机制:
- 解释器:将字节码逐行解释执行。
- JIT编译器(即时编译器):将字节码整体编译为机器码执行。
为什么JVM的执行引擎设计为半解释型,半编译型?
- 逐行解释执行效率低。
- JVM会针对使用频率较高的热点代码进行编译,并缓存起来。这样执行效率就会提高。
- 虽然编译型执行效率高,但是编译需要消耗时间,所以JVM刚刚启动后可以通过解释器去解释执行代码,(编译器开始编译)之后再使用编译器编译执行,两者结合在一起,效果更好。
一张图片让你明白
垃圾回收
概述
- 垃圾收集机制并不是Java语言首创的,但是又是Java的招牌,Java可以自动垃圾回收。
垃圾回收:
什么是垃圾?
- 垃圾是指运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾。
为什么要GC?
- 垃圾如果不及时清理就会越积越多,可能会导致内存溢出。
- 垃圾多了,内存碎片较多。(例如数组,就需要连续的内存空间)
回收哪些区域:
- 频繁回收堆内存。
- 较少回收方法区。
- 栈、本地方法栈、程序计数器没有垃圾回收。
早期是手动回收不被使用的对象,例如C++。
Java语言是自动垃圾收集的。
垃圾回收机制:
自动内存管理:
- 无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险,将程序员从繁重的内存管理中释放出来,可以个更加专注于业务开发。
自动收集的担忧
- 自动回收方便了程序员的开发,但是降低了程序员处理内存问题的能力。
- 自动虽好,但还是应该了解并掌 握一些相关内存管理的知识。
java堆是垃圾回收的重点:
从次数上讲:
- 频繁收集新生区。
- 较少收集Old区。
- 基本不收集原空间(原方法)。
内存溢出与内存泄漏
- 内存溢出:内存不够用了。
- 内存泄漏:有些对象已经在程序中不被使用了,但是垃圾回收机制不能判断其为垃圾对象,因此不能将其回收掉,这样的对象越积越多,长久也会导致内存不够用。
例如:
- 与数据库连接完成之后,需要关闭连接通道,但是没有关闭
- IO读取完成后,没有关闭。
垃圾收集算法分为两大类:
- 垃圾标记阶段算法:
- 主要判定哪些对象已经不再被使用,然后标记为垃圾对象。
- 判断对象为垃圾的标准:不被任何引用指向的对象
- 垃圾回收阶段的算法:
- 引用计数算法(在JVM中不被使用)
- 如果有一个引用指向此对象,那么计数器加1,如果没有引用指向这个对象,那么计数器为0,此时就判定为垃圾。
- 优点:方便使用,设计简介。
- 缺点:增加了计数器的存储空间,计数需要消耗时间,会导致循环引用问题。(好几个对象之间,相互引用,但是没有其他的引用指向它们,此时垃圾回收不能回收它们,但是也没有引用指向,这样就造成了内存泄漏)。
- 可达性分析算法/跟搜索算法(Java目前所使用的垃圾标记算法)
- 可以解决循环引用问题,设计简单,运行高效防止内存泄漏。
- 思路:
- 从一些活跃引用(GCRoots)根开始查找,如果对象被根直接或间接引用,那么此对象不是垃圾,否则标记为垃圾对象。
- 哪些引用被用来当作根:
- 虚拟机栈中引用的对象(方法中引用的对象)。
- 本地方法栈中引用的对象。
- 静态变量所引用的对象。
- 方法区中常量引用的对象(例如:字符串常量池里面的引用)。
- 所有被同步锁synchronized持有的对象。
- java虚拟机内部的引用。
- 总结:
栈中引用的(正在使用的)方法区,常量池中(生命周期较长的),被synchronized当作锁的对象。
final、finally、finalize() 三者的区别?
- final:关键字
- finally:代码块
- finalize() 是一个方法,它是Object类中的一个方法,在对象被最终回收之前调用,且只调用一次。
finalization机制
Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
finalize()方法机制
- java允许对象在销毁前去调用finalize()方法去处理一些逻辑,一般不用(不建议使用)。
- 不要自己显示的去调用finalize()方法,在里面写代码一定要慎重。
- .在 finalize()时可能会导致对象复活。
- finalize()是由垃圾回收器调用的,没有固定的时间。
- 一个糟糕的finalize()会严重的影响GC的性能。比如finalize()是一个死循环。
对象状态
- 可触及的:从根节点开始,可以到达这个对象。(没有被标记为垃圾)
- 可复活的:对象的所有引用都被诠释,但是对象有可能在finalize()方法中复活。确定为垃圾了,但是没有调用finalize()方法。
- 不可触及的:对象的finalize()方法已经被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
代码演示
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于GC Root
//此方法只能被调用一次.
@Override
protected void finalize() throws Throwable {
// super.finalize();
System.out.println("调用当前类重写的finalize()方法");
//当前待回收对象在finalize()方法中引用链上的一个对象obj建立了联系.
obj=this;
}
public static void main(String[] args) {
try {
obj=new CanReliveObj();
//对象第一次拯救自己
obj=null;
//调用垃圾回收器,触发full GC 也不是调用后立刻就回收的,因为线程的执行权在操作系统
System.gc();
System.out.println("第一次GC");
//因为finalizer线程的优先级很低,暂停两秒,以等待它
Thread.sleep(2000);
if (obj==null){
System.out.println("obj is deal");
} else{
System.out.println("obj is still alive");
}
System.out.println("第二次 GC");
//下面这段代码与上面的完全相同,但这次自救失败了.
obj=null;
System.gc();
//因为Finalizer线程优先级很低,暂停两秒,以等待它
Thread.sleep(2000);
if (obj==null){
System.out.println("obj is deal");
} else{
System.out.println("obj is still alive");
}
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
效果展示:
-
垃圾回收算法
1. 标记清除算法
分为两个阶段:
- 标记:标记可以从根可达的对象,标记的是引用的对象。
- 清除:此清除并非直接讲垃圾对象清除掉,而是将垃圾对象的地址维护到一个空闲列表中,之后如果有新的对象产生,判断空闲列表中的对象空间能否存放的下新的对象。如果能放的下就覆盖垃圾对象。
- 优点:简单,容易理解。
- 缺点:效率低,会产生STW(在回收时,停止整个应用程序),会产生内存碎片。
2. 复制算法:
- 将内存分为大小相等的两块,每次只使用其中的一块儿区域即可。
- 当回收时,将不是垃圾的对象复制到另一块内存中,排放整齐,然后再将原来的内存块清空。
- 使用场景:在新生代中的幸存者0区与幸存者1区使用这种算法。
- 优点:没有标记和清除过程,实现简单,运行高效。 复制过去之后保证了空间的连续性,不会出现内存碎片问题。
- 缺点: 需要两倍的内存空间。
3.标记-压缩算法
背景:
- 复制算法需要移动对象,移动对象数量如果多的情况下,效率低。对于年轻代来说还是不错的。但是老年代中大量的对象是存活的,如果移动就比较麻烦,效率低。
实现:
- 将存活对象标记出来,重新在本内存空间中排放位置。
- 清除其他空间的垃圾对象。
标记-清除与标记-压缩对比:
- 标记-清除:不移动对象,不会把垃圾对象清除掉(维护在一个空闲列表中)
- 标记-压缩:要移动对象,要清除掉垃圾对象
- 优点:不会像标记-清除算法那样产生内存碎片,也不会像复制算法那样需要两倍的内存空间。
- 缺点:效率相对较低,对象位置移动后需要重新设置对象的地址。也会有STW(在回收时,停止整个应用程序)。
4.小结
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
5.分代/分区收集
为什么分代收集?
- 由于对象的生命周期的长短不同,讲不通的对象存储在不同的区域,针对不同的区域进行分区收集,提高收集效率。
年轻代里面:
- 年轻代区域相对老年代小,对象生命周期短、存活率低,回收频繁。【一般使用复制算法速度最快】
老年代里面:
- 老年代区域较大,对象生命周期较长、存活率高,回收没有新生代频繁。【复制算法明显不合适,一般由标记-清除算法或标记-压缩算法混合实现】。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
垃圾回收中相关概念
System.gc()的理解
- 调用System.gc()方法,会触发FULL GC(整堆收集)。但是不一定调用后回立刻生效,因为垃圾回收是自动的。
- 一般情况下,不要在项目中显示的去调用。
内存溢出:内存不够用
内存泄漏:垃圾对象无法回收。
Stop the World
- Stop the World—>STW 在垃圾回收时,会导致整个应用程序停止。
- 在标记垃圾对象时,需要以某一个时间节点上内存的情况进行分析(拍照、快照)。
- 因为如果不停顿的话,内存中的对象不停的变化,会导致结果分析不准确。
- 停顿是不可避免的,优秀的垃圾回收器尽可能减少停顿的时间。
对象引用
JDK1.2版之后,将对象分为四个等级:强引用、软引用、弱引用、虚引用。
- 强引用(有引用指向的对象):
Object obj = new Object();
obj引用创建的对象,那么此对象就是被强引用的。
这种情况下即使内存不够用了,报内存溢出,也不会回收。软引用
- 当内存足够使用时,先不回收这一类对象,当虚拟机内存不够用时,要回收此类对象。
弱引用
- 此类对象只能生存到下次垃圾回收之前,只要发生垃圾回收,就会回收此类对象。
虚引用
- 发现即回收。
垃圾回收器
- 比较底层,了解垃圾回收器的一些种类及实现。
- 垃圾回收器(具体实现垃圾回收的收集器名称)
垃圾收集器分类
按照线程数区分,可以分为串行垃圾回收器和并行垃圾回收器
按照工作模式区分,可以分为并发式垃圾回收器和独占式垃圾回收器。
按照工作内存区间区分,可以分为年轻代垃圾回收器与老年代垃圾回收器。
垃圾收集器的性能指标
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用:Java 堆区所占的内存大小。
快速:一个对象从诞生到被回收所经历的时间。
常见的垃圾收集器
-
Serial 垃圾收集器(单线程)
特点:单线程、简单高效,采用复制算法,对于限定单个 CPU 的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:适用于 Client 模式下的虚拟机 -
Serial Old 垃圾收集器(单线程)
Serial Old 是 Serial 收集器的老年代版本。
特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在 Client 模式下的虚拟机中。也可在 Server 模式下使
用。 -
ParNew 垃圾收集器(多线程)
ParNew 收集器其实就是 Serial 收集器的多线程版本。
除了使用多线程外其余行为均和 Serial 收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
特点:多线程、ParNew 收集器默认开启的收集线程数与 CPU 的数量相同,在CPU 非常多的环境中,可以使用-XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题。
应用场景:ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,因为它是除了 Serial 收集器外,唯一一个能与 CMS 收集器配合工作的。 -
Parallel Scavenge 垃圾收集器(多线程)
Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。
但是两者有巨大的不同点:
Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算,。 -
Parallel Old 垃圾收集器(多线程)
是 Parallel Scavenge 收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及 CPU 资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器