# 1 内存结构
> ### 1、简述一下JVM的内存结构?(高频)
JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大部分,线程私有
区和共享区。
| <img src="images/image-20220216222845410.png" alt="image-20220216222845410" style="zoom: 50%;" /> |
| ------------------------------------------------------------ |
**线程私有区**:
① 程序计数器
* 作用:是一块较小的内存空间,可以理解为是当前线程所执行程序的字节码文件的行号指示器,存储的是当前线程所执行的**行号**
* 特点:线程私有 ,唯一一个不会出现内存溢出的内存空间
② 虚拟机栈
* 作用:管理JAVA方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法中变量的变量表、操作数栈、动态链接方法、返回值、返回地址
等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)
| ![image-20220205133550896](images/image-20220205133550896.png) |
| ------------------------------------------------------------ |
* 特点:
1、线程私有
2、局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用(reference 类
型)
3、栈太小或者方法调用过深,都将抛出StackOverflowError异常
* 测试代码
```java
public class StackDemo02 {
// 记录调用了多少次出现了栈内存溢出
private static int count = 0 ;
// 入口方法
public static void main(String[] args) {
try {
show() ;
}catch (Throwable e) {
e.printStackTrace();
}
System.out.println("show方法被调用了:" + count + "次");
}
// 测试方法
public static void show() {
count++ ;
System.out.println("show方法执行了.....");
show();
}
}
```
配置虚拟机参数-Xss可以指定栈内存大小;例如:-Xss180k
栈内存的默认值问题:
```java
The default value depends on the platform:
* Linux/x64 (64-bit): 1024 KB
* macOS (64-bit): 1024 KB
* Oracle Solaris/x64 (64-bit): 1024 KB
* Windows: The default value depends on virtual memory
```
③ 本地方法栈:与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同。
**线程共享区**:
① 堆内存
* 作用:是Java内存区域中一块用来存放对象实例的区域,新创建的对象,数组都使用堆内存;【从Java7开始,常量池也会使用堆内存】
| <img src="images/image-20220216224408053.png" alt="image-20220216224408053" style="zoom:50%;" /> |
| ------------------------------------------------------------ |
Java 堆从GC的角度还可以细分为: 新生代( Eden区 、From Survivor区和 To Survivor区 )和老年代。
* 特点:
1、被线程共享,因此需要考虑线程安全问题
2、会产生内存溢出问题
* 测试代码:
```java
public class HeapDemo01 {
public static void main(String[] args) {
// 定义一个变量
int count = 0 ;
// 创建一个ArrayList对象
ArrayList arrayList = new ArrayList() ;
try {
while(true) {
arrayList.add(new Object()) ;
count++ ;
}
}catch (Throwable a) {
a.printStackTrace();
// 输出程序执行的次数
System.out.println("总共执行了:" + count + "次");
}
}
}
```
* 虚拟机参数:
-Xms 设置最小堆内存大小(不能小于1024K); -Xms 堆内存初始大小,可以通过jmap工具进行查看
-Xmx 设置最大堆内存大小(不能小于1024K); -Xmx 堆内存最大值,可以通过jmap工具进行查看
例如:-Xms1024K -Xmx2048K
注意:
| ![image-20220205135835292](images/image-20220205135835292.png) |
| ------------------------------------------------------------ |
② 方法区
* 作用:它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
* 特点:
1、方法区是一块线程共享的内存区域
2、方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误
3、jdk1.6和jdk1.7方法区也常常被称之为永久区(永久代),大小一般都是几百兆;
4、jdk1.8已经将方法区取消,替代的是元数据区(元空间),如果不指定大小,默认情况下,虚拟机会耗尽可用系统内存
5、jdk7以后就将方法区中的常量池移动至**堆内存**
| ![image-20220205140202778](images/image-20220205140202778.png) |
| ------------------------------------------------------------ |
变化的原因:
1、提高内存的回收效率(方法区内存的回收效率远远低于堆内存,因为方法去中存储的都是类信息,静态变量...这些信息不能被轻易回收)
2、字符串常量池在方法区,那么很容易产生内存溢出(因为方法区的垃圾回收效率比较低);
* 测试代码
```java
/**
jdk1.8的元数据区可以使用参数-XX:MaxMetaspaceSzie设定大小
* 演示元空间内存溢出
* -XX:-UseCompressedClassPointers -XX:MaxMetaspaceSize=10m
UseCompressedClassPointers使用指针压缩,如果不使用这个参数可能会出现: Compressed class space内存溢出
*/
public class MaxMetaspaceDemo extends ClassLoader { // 当前这个类就是一个类加载器
public static void main(String[] args) {
// 定义变量,记录程序产生类的个数
int j = 0;
try {
MaxMetaspaceDemo test = new MaxMetaspaceDemo();
for (int i = 0; i < 10000; i++, j++) {
// 字节码写入器
ClassWriter cw = new ClassWriter(0);
// 定义一个类版本为Opcodes.V1_1,它的访问域为public,名称为Class{i},父类为java.lang.Object,不实现任何接口
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] code = cw.toByteArray();
// 加载该类
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
```
> ### 2、堆和栈的区别?(高频)
① 功能不同:栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储
在堆内存中。
② 共享性不同:栈内存是线程私有的。堆内存是所有线程共有的。
③ 异常错误不同:如果栈内存或者堆内存不足都会抛出异常。栈空间不足:java.lang.StackOverFlowError。堆空间不足:
java.lang.OutOfMemoryError。
④ 空间大小:栈的空间大小远远小于堆的。
> ### 3、怎么获取Java程序使用的内存?堆使用的百分比?
可以通过java.lang.Runtime类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余
空间。
1、Runtime.freeMemory() 方法返回剩余空间的字节数
2、Runtime.totalMemory()方法总内存的字节数
> ### 4、栈帧都有哪些数据?
栈帧包含:局部变量表、操作数栈、动态连接、返回值、返回地址等。
> ### 5、如何启动系统的时候设置jvm的启动参数?
其实都很简单,比如说采用"java -jar"的方式启动一个jar包里面的系统,那么就可以才用类似下面的格式:
| ![image-20220205141640067](images/image-20220205141640067.png) |
| ------------------------------------------------------------ |
# 2 垃圾回收
> ### 6、如何判断一个对象是否为垃圾?(高频)
两种算法:
**① 引用计数法**:堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被
赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,
对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。
特点:简单、无法解决循环引用问题
定义学生类:
```java
public class Student {
// 定义成员变量
public Object instance ;
}
```
编写测试类:
```java
/*
jvm参数:-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-verbose:gc -XX:+PrintGCDetails:打印gc日志信息
-XX:+PrintGCTimeStamps: 打印gc日志的时间戳
*/
public class ReferenceCountGcDemo {
public static