1、能说一下JVM运行时的内存区域划分吗?(大家需要把这个和JVM模型搞乱,最好问清楚点,有时候模型是指进程/线程模型)
简单来说 JVM 运行时的内存区域分为了五大块:程序计数器,虚拟机栈,本地方法栈,堆,方法区。
- 其中程序计数器用来记录各个线程执行的字节码地址,通常在线程上下文切换时用来保存当前线程的执行信息。
- 虚拟机栈用于保存方法的局部变量,操作树并参与方法的调用和返回。每个线程在创建时都会创建一个虚拟机栈,每次方法调用都会创建一个栈帧并压入虚拟机栈中。栈帧中保存的就是方法信息,如操作数栈,局部变量表等等。
- 本地方法栈就是用于管理 native 方法的调用,一般是由 C 语言实现的。
- 堆是线程共享的区域,几乎类的实例和数组分配的内存都来自于它。堆被划分为 “新生代” 和 “老年代”,新生代又被进一步划分为 Eden 和 Survivor 区,这些都主要跟垃圾回收机制有关。
- 首先方法区只是 JVM 的规范,具体的实现可能各个厂商不一样,在 HotSpot 虚拟机中,JDK 8 以前是用 “永久代” 实现的 “方法区”,在 JDK 8 中用 “元空间” 代替了 “永久代” 作为方法区的实现。那么方法区主要是用来存放虚拟机加载的 “类相关信息”,比如类信息,常量池等等。其中类信息包括类中的字段,方法,父类等等。常量池又包括静态常量池和动态常量池,静态常量池用来保存字面量以及符号引用等信息,动态常量池用来存储类加载时生成的 “直接引用” 等信息。
2、 JVM 进程与线程模型?
在操作系统层面,一个 JVM 实例被视为一个独立的进程,这个进程拥有自己的内存空间和执行环境。
在单个 JVM 进程内,可以同时运行多个 Java 应用程序,它们之间相互独立。
JVM 的线程模型在 Java 规范中并没有要求用哪种方法实现,在 JDK 1.2 以前,是使用一种叫 “绿色线程” 的用户线程实现的。绿色线程是指由虚拟机调度,而不是本地的操作系统调度的线程,它可以在本来不支持多线程的操作系统上实现多线程。在 JDK 1.2 以后,采用的是内核线程(Kernel-Level Thread, KLT)来实现 Java 线程。内核线程是操作系统内核直接支持的线程,它由内核的线程调度器对内核线程进行控制和分配,程序一般不直接使用内核线程,而是使用它的高级接口:轻量级线程(Light Weight Process, LWP)。每个轻量级线程都由一个内核线程与其对应,所以也叫做 1:1 的线程模型。
3、堆和栈有啥区别?
主要有存储内容,内存分配和生命周期三个方面的不同。
存储内容方面,Java 堆主要用于存储对象实例和数组等动态分配的内容。同时堆中分配的内存由垃圾回收器自动回收,防止内存泄漏;虚拟机栈主要用于存储方法调用时的方法信息,方法调用上下文等等。这块内存的管理是自动进行的,当方法调用结束时,虚拟机栈上有关方法的内存会被自动清除。
内存分配方面,Java 堆主要由 JVM 动态分配和管理,对象实例一般通过 new 关键字来在堆中分配内存。虚拟机栈则是在方法调用时动态创建栈帧,压入栈中。
生命周期方面,对象在 Java 堆上分配内存后,其生命周期可以贯穿整个 Java 程序的运行,而虚拟机栈中的栈帧则取决于方法何时调用和返回。
4、什么时候会出现堆栈溢出呢?如何排查?
堆溢出也就是内存溢出 OOM,一般是由于创建的对象太多,导致超过了堆的最大容量。排查时可以通过性能检测工具如 jconsole,获取堆内存快照,然后观察溢出的对象是否是必要的,是的话就需要检查代码中对象的生命周期是否过长,或者优化算法,实在不行可以调整 JVM 的堆参数设置 -Xmx 和 -Xms,增加 JVM 最大内存和启动初始内存;如果溢出的对象不是必要的,就表明发生了内存泄漏,这时可以查看泄漏对象的 GC Roots 引用链,找到具体泄漏的位置。
如果是虚拟机栈溢出,很可能就是存在死循环,过多的递归调用导致把栈撑满了,此时可以直接看控制台的堆栈信息,比较容易定位。
5、对象一定是在堆上分配的吗?
不一定,JVM 会通过 “逃逸分析” 技术,对于逃不出方法的对象,会直接在栈空间上分配内存。这样可以直接在栈上快速创建和销毁对象,不用再将对象分配到堆中,减轻 JVM 垃圾回收的压力。
逃逸分析技术就是用来判断对象是否逃逸的技术。对象逃逸分为方法逃逸和线程逃逸。方法逃逸就是指,当一个对象在方法中被定义后,它被外部方法引用了,例如作为调用参数传递到其他方法中。线程逃逸是指一个对象被外部线程访问到了,比如赋值给可以在其他线程中访问的实例变量。
6、常量池有啥用?
Java 8 以前常量池存在于 JVM 方法区的永久代中,Java 8 及以后存在于元空间中,它主要用来存储编译期生成的各种字面量和符号引用等等。而这些字面量可以在程序运行时直接使用,不需要再创建,提高运行效率。对于符号引用,其中包括类的全限定名,方法名等等,主要用在类加载,动态绑定等阶段,来定位具体的类,方法等等。