1 内存结构
1、简述一下JVM的内存结构?(高频)
JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大部分,线程私有区和共享区。

线程私有区:
① 程序计数器
- 作用:是一块较小的内存空间,可以理解为是当前线程所执行程序的字节码文件的行号指示器,存储的是当前线程所执行的行号
- 特点:线程私有 ,唯一一个不会出现内存溢出的内存空间
② 虚拟机栈
- 作用:管理JAVA方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法中变量的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)

- 特点:
1、线程私有
2、局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用(reference 类型)
3、栈太小或者方法调用过深,都将抛出StackOverflowError异常
- 测试代码
- 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
栈内存的默认值问题:- 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开始,常量池也会使用堆内存】
-------------------------------------------------------- |
Java 堆从GC的角度还可以细分为: 新生代( Eden区 、From Survivor区和 To Survivor区 )和老年代。
- 特点:
1、被线程共享,因此需要考虑线程安全问题
2、会产生内存溢出问题
- 测试代码:
- 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
注意:

② 方法区
- 作用:它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 特点:
1、方法区是一块线程共享的内存区域
2、方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误
3、jdk1.6和jdk1.7方法区也常常被称之为永久区(永久代),大小一般都是几百兆;
4、jdk1.8已经将方法区取消,替代的是元数据区(元空间),如果不指定大小,默认情况下,虚拟机会耗尽可用系统内存
5、jdk7以后就将方法区中的常量池移动至堆内存

变化的原因:
1、提高内存的回收效率(方法区内存的回收效率远远低于堆内存,因为方法去中存储的都是类信息,静态变量...这些信息不能被轻易回收)
2、字符串常量池在方法区,那么很容易产生内存溢出(因为方法区的垃圾回收效率比较低);
- /**
- 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包里面的系统,那么就可以才用类似下面的格式:

2 垃圾回收
6、如何判断一个对象是否为垃圾?(高频)
两种算法:
① 引用计数法:堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。
特点:简单、无法解决循环引用问题
定义学生类:- public class Student {
- // 定义成员变量
- public Object instance ;
- }
复制代码 编写测试类:- /*
- jvm参数:-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
- -verbose:gc -XX:+PrintGCDetails:打印gc日志信息
- -XX:+PrintGCTimeStamps: 打印gc日志的时间戳
- */
- public class ReferenceCountGcDemo {
- public static void main(String[] args) {
- // 创建Student对象
- Student a = new Student() ;
- Student b = new Student() ;
- // 进行循环引用
- a.instance = b ;
- b.instance = a ;
- // 将a对象和b对象设置为null
- a = null ;
- b = null ;
- // 调用System.gc进行垃圾回收
- System.gc(); // 如果没有触发垃圾回收说明Hotspot的jvm使用的就是引用计数法来判断对象是否为垃圾
- }
- }
复制代码 控制台输出gc日志:- 0.076: [GC (System.gc()) [PSYoungGen: 7802K->856K(151552K)] 7802K->864K(498688K), 0.0008493 secs] [Times: user=0.17 sys=0.02, real=0.00 secs]
- 0.077: [Full GC (System.gc()) [PSYoungGen: 856K->0K(151552K)] [ParOldGen: 8K->620K(347136K)] 864K->620K(498688K), [Metaspace: 3356K->3356K(1056768K)], 0.0044768 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- Heap
- PSYoungGen total 151552K, used 3901K [0x0000000716c00000, 0x0000000721500000, 0x00000007c0000000)
- eden space 130048K, 3% used [0x0000000716c00000,0x0000000716fcf748,0x000000071eb00000)
- from space 21504K, 0% used [0x000000071eb00000,0x000000071eb00000,0x0000000720000000)
- to space 21504K, 0% used [0x0000000720000000,0x0000000720000000,0x0000000721500000)
- ParOldGen total 347136K, used 620K [0x00000005c4400000, 0x00000005d9700000, 0x0000000716c00000)
- object space 347136K, 0% used [0x00000005c4400000,0x00000005c449b318,0x00000005d9700000)
- Metaspace used 3365K, capacity 4496K, committed 4864K, reserved 1056768K
- class space used 370K, capacity 388K, committed 512K, reserved 1048576K
复制代码- ① 0.076: 代表gc发生的时间,从jvm启动以来经过的秒数
- ② [GC和[Full Gc: 说明这次垃圾收集器的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有"Full",说明此次GC发生了stop-the-world。System.gc()是说明显示的调用了 System.gc方法进行垃圾回收
- ③ [PSYoungGen:表示GC发生的区域, 不同的垃圾收集器展示的区域名称不一样,PSYoungGen表示的是新生代,这里默认使用的是Parallel Scavenge收集器 (-XX:+UseSerialGC)
- ④ 7802K->856K(151552K):GC前该区域已使用容量 -> GC后该区域已使用容量(该区域的总容量)
- ⑤ 7802K->864K(498688K):GC前Java堆已使用容量 -> GC后Java堆已使用容量(Java堆总容量)
- ⑥ 0.0008493 secs:该区域GC所占用的时间
- ⑦ [Times: user=0.17 sys=0.02, real=0.00 secs]: 分别表示用户态消耗的CPU时间、内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间(墙钟时间包括非运算的等待耗时)。多线程操作会叠加这些CPU时间,所以user、sys时间超过real时间是完全正常的。
复制代码 ② 可达性分析算法 : 可达性分析算法又叫做跟搜索法,就是通过一系列的称之为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的
路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
(似于葡萄串);

7、可达性算法中,哪些对象可作为GC Roots对象?(高频)
可以作为GC ROOTS对象的情况:
1、虚拟机栈中引用的对象
2、方法区静态成员引用的对象
3、方法区常量引用对象
4、本地方法栈引用的对象
8、Java中都有哪些引用类型?(高频)
① 强引用
Java中默认声明的就是强引用,比如:- Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
- obj = null; //手动置null
复制代码 只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
示例:- /**
- * JVM参数:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
- */
- public class StrongReferenceDemo01 {
- private static List<Object> list = new ArrayList<Object>() ;
- public static void main(String[] args) {
- // 创建对象
- for(int x = 0 ; x < 10 ; x++) {
- byte[] buff = new byte[1024 * 1024 * 1];
- list.add(buff);
- }
- }
- }
复制代码 ② 软引用
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
示例代码:- /**
- * JVM参数:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
- */
- public class SoftReferenceDemo01 {
- private static List<Object> list = new ArrayList<>();
- public static void main(String[] args) {
- // 创建数组对象
- for(int x = 0 ; x < 10 ; x++) {
- SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024 * 1024 * 1]) ;
- list.add(softReference) ;
- }
- System.gc(); // 主动通知垃圾回收器进行垃圾回收
-
- for(int i=0; i < list.size(); i++){
- Object obj = ((SoftReference) list.get(i)).get();
- System.out.println(obj);
- }
-
- }
- }
复制代码 我们发现无论循环创建多少个软引用对象,打印结果总是有一些为null,这里就说明了在内存不足的情况下,软引用将会被自动回收。
③ 弱引用
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2之后,用
java.lang.ref.WeakReference来表示弱引用。
示例代码:- /**
- * JVM参数:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
- */
- public class WeakReferenceDemo01 {
- private static List<Object> list = new ArrayList<>();
- public static void main(String[] args) {
- // 创建数组对象
- for(int x = 0 ; x < 10 ; x++) {
- WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1024 * 1024 * 1]) ;
- list.add(weakReference) ;
- }
- System.gc(); // 主动通知垃圾回收器进行垃圾回收
- for(int i=0; i < list.size(); i++){
- Object obj = ((WeakReference) list.get(i)).get();
- System.out.println(obj);
- }
-
- }
- }
复制代码 ④ 虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
[code]public class PhantomReference extends Reference { /** * Returns this reference object's referent. Because the referent of a * phantom reference is always inaccessible, this method always returns * null. * * @return null */ public T get() { return null; } /** * Creates a new phantom reference that refers to the given object and * is registered with the given queue. * *
It is possible to create a phantom reference with a null * queue, but such a reference is completely useless: Its get * method will always return null and, since it does not have a queue, it * will never be enqueued. * * @param referent the object the new phantom reference will refer to * @param q the queue with which the reference is to be registered, * or null if registration is not required */ public PhantomReference(T referent, ReferenceQueue |