目录
1.什么情况下会发生栈内存溢出。
2.详解JVM内存模型
3.JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。
4. JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代
5.你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。
6.JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存。
7.简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。
8.说说你知道的几种主要的JVM参数
9.怎么打出线程栈信息。
10.强引用、软引用、弱引用、虚引用的区别?
參考与感谢
Q1:JVM管理的内存结构是怎样的?
Q2:不同的虚拟机在实现运行时内存的时候有什么区别?
Q3:运行时数据区中哪些区域是线程共享的?哪些是独享的?
Q4:除了JVM运行时内存以外,还有什么区域可以用吗?
Q5:堆和栈的区别是什么?
Q6:Java中的数组是存储在堆上还是栈上的?
Q7:Java中的对象创建有多少种方式?
Q8:Java中对象创建的过程是怎么样的?
Q9:Java中的对象一定在堆上分配内存吗?
Q10:怎么如何获取堆和栈的dump文件?
思路: 描述栈定义,再描述为什么会溢出,再说明一下相关配置参数,OK的话可以给面试官手写是一个栈溢出的demo。
我的答案:
思路: 给面试官画一下JVM内存模型图,并描述每个模块的定义,作用,以及可能会存在的问题,如栈溢出等。
我的答案:
思路: 先讲一下JAVA堆,新生代的划分,再谈谈它们之间的转化,相互之间一些参数的配置(如: –XX:NewRatio,–XX:SurvivorRatio等),再解释为什么要这样划分,最好加一点自己的理解。
我的答案:
1)共享内存区划分
2)一些参数的配置
3)为什么要分为Eden和Survivor?为什么要设置两个Survivor区?
思路: 先描述一下Java堆内存划分,再解释Minor GC,Major GC,full GC,描述它们之间转化流程。
我的答案:
思路: 一定要记住典型的垃圾收集器,尤其cms和G1,它们的原理与区别,涉及的垃圾回收算法。
我的答案:
1)几种垃圾收集器:
2)CMS收集器和G1收集器的区别:
CMS收集器 | G1收集器 | |
算法类型 | “标记-清除”算法 | “标记-整理”算法 |
收集范围 | 老年代 | 老年代和新生代 |
目标 | 最小的停顿时间为目标 | 可预测垃圾回收的停顿时间 |
思路: 先画出Java内存模型图,结合例子volatile ,说明什么是重排序,内存屏障,最好能给面试官写以下demo说明。
我的答案:
1)Java内存模型图:
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
2)指令重排序。
在这里,先看一段代码
public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();other.start();
one.join();other.join();
System.out.println(“(” + x + “,” + y + “)”);
}
运行结果可能为(1,0)、(0,1)或(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是指令重排。
3)内存屏障
内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。
4)happen-before原则
思路: 先说明一下什么是类加载器,可以给面试官画个图,再说一下类加载器存在的意义,说一下双亲委派模型,最后阐述怎么打破双亲委派模型。
我的答案:
1) 什么是类加载器?
类加载器 就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。
- 扩展类加载器(Extension ClassLoader):负责加载
\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。 - 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
2)双亲委派模型
双亲委派模型工作过程是:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
双亲委派模型图:
3)为什么需要双亲委派模型?
在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码
4)怎么打破双亲委派模型?
打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。
思路: 可以说一下堆栈配置相关的,垃圾收集器相关的,还有一下辅助信息相关的。
我的答案:
1)堆栈配置相关
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0
-Xmx3550m: 最大堆大小为3550m。
-Xms3550m: 设置初始堆大小为3550m。
-Xmn2g: 设置年轻代大小为2g。
-Xss128k: 每个线程的堆栈大小为128k。
-XX:MaxPermSize: 设置持久代大小为16m
-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。
-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
2)垃圾收集器相关
-XX:+UseParallelGC
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection:
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20: 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
3)辅助信息相关
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGC 输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
思路: 可以说一下jps,top ,jstack这几个命令,再配合一次排查线上问题进行解答。
我的答案:
思路: 先说一下四种引用的定义,可以结合代码讲一下,也可以扩展谈到ThreadLocalMap里弱引用用处。
我的答案:
我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
SoftReference softRef=new SoftReference(str); // 软引用
用处: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
如下代码:
Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
}else{
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
String str=new String("abc");
WeakReference abcWeakRef = new WeakReference(str);
str=null;
等价于
str = null;
System.gc();
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
转载自:Hollis
Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域。《Java虚拟机规范》中规定了JVM所管理的内存需要包括一下几个运行时区域:
Java虚拟机运行时数据区域主要包含了PC寄存器(程序计数器)、Java虚拟机栈、本地方法栈、Java堆、方法区以及运行时常量池。
各个区域有各自不同的作用,关于各个区域的作用就不在本文中相信介绍了。
但是,需要注意的是,上面的区域划分只是逻辑区域,规范对于有些区域的限制是比较松的,所以不同的虚拟机厂商在实现上,甚至是同一款虚拟机的不同版本也是不尽相同的。
前面提到过《Java虚拟机规范》定义的JVM运行时所需的内存区域,不同的虚拟机实现上有所不同,而在这么多区域中,规范对于方法区的管理是最宽松的,规范中关于这部分的描述如下:
方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集与压缩。本版本的规范也不限定实现方法区的内存位置和代码编译的管理策略。方法区的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多的空间时自行收缩。方法区在实际内存空间站可以是不连续的。
这一规定,可以说是给了虚拟机厂商很大的自由。
虚拟机规范对方法区实现的位置并没有明确要求,在最著名的HotSopt虚拟机实现中(在Java 8 之前),方法区仅是逻辑上的独立区域,在物理上并没有独立于堆而存在,而是位于永久代中。所以,这时候方法区也是可以被垃圾回收的。
实践证明,JVM中存在着大量的声明短暂的对象,还有一些生命周期比较长的对象。为了对他们采用不同的收集策略,采用了分代收集算法,所以HotSpot虚拟机把的根据对象的年龄不同,把堆分位新生代、老年代和永久代。
在Java 8中 ,HotSpot虚拟机移除了永久代,使用本地内存来存储类元数据信息并称之为:元空间(Metaspace)
在JVM运行时内存区域中,PC寄存器、虚拟机栈和本地方法栈是线程独享的。
而Java堆、方法区是线程共享的。但是值得注意的是,Java堆其实还未每一个线程单独分配了一块TLAB空间,这部分空间在分配时是线程独享的,在使用时是线程共享的。(TLAB介绍)
除了我们前面介绍的虚拟机运行时数据区以外,还有一部分内存也被频繁使用,他不是运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,他就是——直接内存。
直接内存的分配不受Java堆大小的限制,但是他还是会收到服务器总内存的影响。
在JDK 1.4中引入的NIO中,引入了一种基于Channel和Buffer的I/O方式,他可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的应用进行操作。

堆和栈(虚拟机栈)是完全不同的两块内存区域,一个是线程独享的,一个是线程共享的,二者之间最大的区别就是存储的内容不同:
堆中主要存放对象实例。
栈(局部变量表)中主要存放各种基本数据类型、对象的引用。
在Java中,数组同样是一个对象,所以对象在内存中如何存放同样适用于数组;
所以,数组的实例是保存在堆中,而数组的引用是保存在栈上的。

Java中共有5种方式可以创建一个对象。
最简单的方式就是使用new关键字。
User user = new User();
除此以外,还可以使用反射机制创建对象:
User user = User.class.newInstance();
或者使用Constructor类的newInstance:
Constructor<User> constructor = User.class.getConstructor();
User user = constructor.newInstance();
除此之外还可以使用clone方法和反序列化的方式,这两种方式不常用并且代码比较复杂,就不在这里展示了,感兴趣的可以自行了解下。
对于一个普通的Java对象的创建,大致过程如下:
前面我们说过,Java堆中主要保存了对象实例,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
其实,在编译期间,JIT会对代码做很多优化。其中有一部分优化的目的就是减少内存堆分配压力,其中一种重要的技术叫做逃逸分析。
如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。(关于逃逸分析和栈上分配可以参考:深入理解Java中的逃逸分析、对象并不一定都是在堆上分配内存的)
Java Dump,Java虚拟机的运行时快照。将Java虚拟机运行时的状态和信息保存到文件。
可以使用在服务器上使用jmap命令来获取堆dump,使用jstack命令来获取线程的调用栈dump。(关于jmap和jstack可以参考:Java命令学习系列(三)——Jmap、Java命令学习系列(二)——Jstack)