JDK、JRE、JVM区别?
类加载过程
装载 验证 准备 解析 初始化
类加载器分类
双亲委派模型
如何打破双亲委派模型?
自定义类加载器,集成ClassLoader类重写loadClass,如Tomcat
JVM内存模型
JVM 需要使用计算机的内存,Java 程序运行中所处理的对象或者算法都会使用 JVM 的内
存空间,JVM 将内存区划分为 5 块,这样的结构称之为 JVM 内存模型。
JVM为什么进行内存区域的划分?
随着对象数量的增加,JVM 内存使用率也在增加,如果 JVM 内存使用率达到 100%,
则无法继续运行程序。为了让 JVM 内存可以被重复使用,我们需要进行垃圾回收。为了提高垃圾回收的效率,JVM 将内存区域进行了划分。
JVM内存划分
JVM 按照线程是否共享将内存首先分成两大类
线程独享区
当有当前线程能访问数据的区域,线程之间不能共享,随着线程的创建而创建,随着线程的销毁而被回收。线程存在的时间较短,一般不涉及垃圾回收
线程共享区
所有的线程都可以访问的区域,当线程被销毁的时候,共享区的数据不会被立即回收,需要等待达到垃圾回收的阈值才会进行回收。垃圾回收的优化,主要是优化的线程共享区。
程序计数器
程序计数器会记录当前线程执行指令的内存地址,只占用一小部分区域,只记录一个地址,故我们认为程序计数器不会出现内存溢出的问题。
本地方法栈
Java中有些代码的实现是依赖于其他非Java语言的(C++),本地方法栈存储的是非Java语句执行中产生的数据。一般我们认为本地方法栈不会出现内存溢出的问题。
虚拟机栈
存放当前线程中所声明的变量,包括基本数据类型的数据和引用数据类型的引用。
基本数据类型和引用数据类型划分的标准:
-
基本数据类型:
-
四类八种(byte【1字节】、short 【2】、int【4】、 long【8】 、 char【2~3根据编码】、 boolean【1】 、float【4】 、double【8】)
变量在声明的时候就能确认占用内存的大小。
引用数据类型将值的引用存放在虚拟机栈中,对象存放在堆内存中,引用数据类型占4个字节
-
引用数据类型:
(对象、数组、接口)
变量在声明的时候,不能取人占用内存的大小。
栈帧
每个线程都会对应一个虚拟机栈,线程的每个方法都会创建一个栈帧,存放本次方法执行过程中所需要的数据。
如果一个线程中有多个方法嵌套调用,虚拟机栈会对栈帧进行压栈和出栈操作,正在执行的方法一定在栈顶,我们只能获取栈顶的栈帧,栈帧在虚拟机栈中先进后出。
栈帧的数据结构
- 1、局部变量
存放当前方法的局部变量,基本数据类型存值,引用数据类型存内存地址。 - 2、操作数栈
对方法中的变量提供计算的区域 - 3、常量数据的引用
常量数据会存放到方法区的常量池中,不管是基本数据类型还是引用数据类型。 - 4、方法返回值的地址
方法返回数据会存到计算机内存的寄存器中。
虚拟机栈溢出异常
修改虚拟机栈的内存大小设置栈帧的最大深度:-Xss 虚拟机栈内存大小。
一般栈帧深度达到3000~5000即可。
太小虚拟机栈容易溢出,太大导致每个线程内存占用过大,影响线程数量。
方法区
在 java8 之后,我们把方法区称之为元空间(MetaSpace),方法区在逻辑上属于堆的一部分,但一些具体机制和堆有所区别,如:一些 JVM 的方法区是可以不进行垃圾回收的,关闭 JVM 时才会释放方法区内存。所以方法区还有一个别名叫非堆,目的是和堆分开。
方法区会存储类信息、静态变量、常量(JDK8 之后不存放字符串常量)、本地机器指令。
如果加载大量 class 文件,也会造成方法区内存溢出,如一个 tomcat 运行 20~30 个项目。
堆内存模型
JAVA 对象内存布局
对象头
MarkWord :一系列标记位(哈希码、分代年龄、锁状态标记等),在 64 位系统中占8 字节;
ClassPoint :对象对应的类信息的内存地址,在 64 位系统中占 8 字节。
Length:数组对象特有,表示数组长度,占 4 字节。
实例数据:
包含了对象的所有成员变量,大小由变量类型决定。
byte、boolean:1 字节
short:2 字节
char:2~3 字节
int、float:4 字节
long、double、引用数据类型:8 字节
对其填充
将对象大小填充为 8 字节的整数倍
JVM 内存溢出和垃圾回收机制
为什么要进行堆内存分区:
1、提高垃圾搜索效率;
2、垃圾回收后可以更好的利用内存空间,存放大对象;
3、尽可能减少GC次数
JVM 堆内存的划分
老年代:
对象会优先分配到新生代内存中,每次 GC 后没有回收的对象年龄加 1,年龄到15 还没有被回收,对象会存放到老年代内存中;如果对象较大,超过新生代内存的一半,对象也会存放到老年代区域。
新生代:
为了减少young区垃圾回收后的空间碎片,新生代又分为Eden区和两个Survivor区,且始终有一个 Suvivor 区保持闲置。对象会先存放到 Eden 区当中,Eden 区空间满了之后会进行 young 区的垃圾回收,之后将 young 区所有存活的对象复制到闲置的 Suvivor 区中,并清空 Eden 区和正在使用的 Survivor 区。
YoungGC 和 OldGC
YoungGC
新生代区域的垃圾回收称之为 YoungGC,也叫 MinorGC,Eden 区满后会触发YoungGC
OldGC
老年代区域的垃圾回收称之为 OldGC,也叫 MajorGC,OldGC 非常浪费性能,
所以我们的 JVM 调优要尽可能减少 OldGC 的次数, OldGC 往往伴随着 YoungGC。YoungGC+OldGC = FullGC
问题:
Survivor 区空间并不大,如果满了怎么办?
1、一般情况下 GC 会回收 95%的对象,且超过 15 次 GC 的对象会存放到 old区,所以 Survivor 区不容易满。
2、如果 Survivor 区满了,会触发担保机制,提前将对象存入 Old 区。
为什么需要 Survivor 区?
为了减少垃圾回收带来的空间碎片,空间碎片过多会频繁触发YoungGC。
为什么需要两块 Survivor 区?
为了减少 Survivor 区的空间碎片。
垃圾回收机制
如何判断一个对象是垃圾
引用计数法:
如果要操作对象,必须通过引用来进行。如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到。那么这个对象就成为可被回收的对象了。这种方式实现简单,效率较高,但是它无法解决循环引用的问题,因此在 Java 中并没有采用这种方式(Python 采用的是引用计数法)。
达性分析:
以一个GC Root对象作为起点进行搜索,如果在GC Roots和对象之间没有可达路径,以一个GC根对象作为起点进行搜索,如果在GC根和对象之间没有可达路径则称该对象是不可达的。
GC ROOT对象∶
- 栈帧中的本地变量表中引用的对象。
我们正在执行的方法是在顶部栈帧中,正在执行的方法所引用的对象必定是很重要的对象 - 方法区中静态属性引用的对象。
类中的变量,整个程序都要用的东西,很重要 - 方法区中常量引用的对象。
也是要一直使用的对象 - 本地方法栈中引用的对象.
存放的是一些非Java语言的对象
常见面试题补充
内存泄漏和内存溢出是一样的概念吗?
不一样,内存泄漏指不再使用的对象无法得到及时的回收,持续占用内存空间,造成内存空间的浪费。内存溢出指程序运行要用到的内存大于能提供的最大内存。内存无法内存泄漏很容易导致内存溢出,内存溢出不一定是内存泄漏导致的。