1. jvm运行时数据区的划分?
运行时数据区(Run-Time Data Areas)
Java 虚拟机定义了若干种在程序执行期间会使用到的运行时数据区域。
其中一些数据区域在 Java 虚拟机启动时被创建,随着虚拟机退出而销毁。也就是线程间共享的区域:堆、方法区、运行时常量池。
另外一些数据区域是按线程划分的,这些数据区域在线程创建时创建,在线程退出时销毁。也就是线程间隔离的区域:程序计数器、Java虚拟机栈、本地方法栈。
1)程序计数器(Program Counter Register)
Java 虚拟机可以支持多个线程同时执行,每个线程都有自己的程序计数器。在任何时刻,每个线程都只会执行一个方法的代码,这个方法称为该线程的当前方法(current method)。
如果线程正在执行的是 Java 方法(不是 native 的),则程序计数器记录的是正在执行的 Java 虚拟机字节码指令的地址。如果正在执行的是本地(native)方法,那么计数器的值是空的(undefined)。
2)Java虚拟机栈(Java Virtual Machine Stacks)
每个 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,它与线程同时创建,用于存储栈帧。
Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
3)本地方法栈(Native Method Stacks)
本地方法栈与 Java 虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是 Java 虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地(Native)方法服务。
4)堆(Heap)
堆是被各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。
堆在虚拟机启动时创建,堆存储的对象不会被显示释放,而是由垃圾收集器进行统一管理和回收
5)方法区(Method Area)
方法区是被各个线程共享的运行时内存区域。方法区类似于传统语言的编译代码的存储区。它存储了每一个类的结构信息,例如:运行时常量池、字段和方法数据,构造函数和普通方法的字节码内容,还包括一些用于类、实例、接口初始化用到的特殊方法。
6)运行时常量池(Run-Time Constant Pool)
运行时常量池是 class 文件中每一个类或接口的常量池表(constant_pool table)的运行时表示形式。
它包含了若干种常量,从编译时已知的数值字面量到必须在运行时解析后才能获得的方法和字段引用。运行时常量池的功能类似于传统编程语言的符号表(symbol table),不过它包含的数据范围比通常意义上的符号表要更为广泛。
2. 根据jvm规范,这些数据区中哪些会出现 内存溢出异常,分别是什么场景下出现?
**
实战:OutOfMemoryError异常
**
内存异常是我们工作当中经常会遇到问题,但如果仅仅会通过加大内存参数来解决问题显然是不够的,应该通过一定的手段定位问题,到底是因为参数问题,还是程序问题(无限创建,内存泄露)。定位问题后才能采取合适的解决方案,而不是一内存溢出就查找相关参数加大。
概念
内存泄露:代码中的某个对象本应该被虚拟机回收,但因为拥有GCRoot引用而没有被回收。
内存溢出:虚拟机由于堆中拥有太多不可回收对象没有回收,导致无法继续创建新对象。
在分析问题之前先给大家讲一讲排查内存溢出问题的方法,内存溢出时JVM虚拟机会退出,那么我们怎么知道JVM运行时的各种信息呢,Dump机制会帮助我们,可以通过加上VM参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现内存溢出异常时生成dump文件,然后通过外部工具(VisualVM)来具体分析异常的原因。
除了程序计数器外,Java虚拟机的其他运行时区域都有可能发生OutOfMemoryError的异常,下面分别给出验证:
一、 Java堆溢出
Java堆用来存储对象,因此只要不断创建对象,并保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清楚这些对象,那么当对象数量达到最大堆容量时就会产生 OOM。
/**
* java堆内存溢出测试
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7164.hprof …
Heap dump file created [27880921 bytes in 0.193 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:440)
at com.jvm.oom.HeapOOM.main(HeapOOM.java:17)
堆内存 OOM 是经常会出现的问题,异常信息会进一步提示 Java heap space
二、 虚拟机栈和本地方法栈溢出
在 HotSpot 虚拟机中不区分虚拟机栈和本地方法栈,栈容量只由 -Xss 参数设定。关于虚拟机栈和本地方法栈,在 Java 虚拟机规范中描述了两种异常:
**
1.如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。
2.如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。**
/**
* 虚拟机栈和本地方法栈内存溢出测试,抛出stackoverflow exception
* VM ARGS: -Xss128k 减少栈内存容量
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak () {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length = " + oom.stackLength);
throw e;
}
}
}
运行结果:
stack length = 11420
Exception in thread “main” java.lang.StackOverflowError
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
以上代码在单线程环境下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时,抛出的都是 StackOverflowError 异常。
如果测试环境是多线程环境,通过不断建立线程的方式可以产生内存溢出异常,代码如下所示。但是这样产生的 OOM 与栈空间是否足够大不存在任何联系,在这种情况下,为每个线程的栈分配的内存足够大,反而越容易产生OOM 异常。这点不难理解,每个线程分配到的栈容量越大,可以建立的线程数就变少,建立多线程时就越容易把剩下的内存耗尽。这点在开发多线程的应用时要特别注意。如果建立过多线程导致内存溢出,在不能减少线程数或更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。
/**
* JVM 虚拟机栈内存溢出测试, 注意在windows平台运行时可能会导致操作系统假死
* VM Args: -Xss2M -XX:+HeapDumpOnOutOfMemoryError
*/
public class JVMStackOOM {
private void dontStop() {
while (true) {}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JVMStackOOM oom = new JVMStackOOM();
oom.stackLeakByThread();
}
}
三、方法区和运行时常量池溢出
方法区用于存放Class的相关信息,对这个区域的测试,基本思路是运行时产生大量的类去填满方法区,直到溢出。使用CGLib实现。
方法区溢出也是一种常见的内存溢出异常,在经常生成大量Class的应用中,需要特别注意类的回收情况,这类场景除了使用了CGLib字节码增强和动态语言外,常见的还有JSP文件的应用(JSP第一次运行时要编译为Java类)、基于OSGI的应用等。
/**
* 测试JVM方法区内存溢出
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class MethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject{}
}
四、本机直接内存溢出
DirectMemory 容量可通过 -XX:MaxDirectMemorySize 指定,如不指定,则默认与Java堆最大值一样。测试代码使用了 Unsafe 实例进行内存分配。
由 DirectMemory 导致的内存溢出,一个明显的特征是在Heap Dump 文件中不会看见明显的异常,如果发现 OOM 之后 Dump 文件很小,而程序直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。
/**
* 测试本地直接内存溢出
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
输出结果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.netty.netty.OutOfMemoryError.DirectMemoryOOM.main(DirectMemoryOOM.java:21)
3. 这些数据区哪些是线程独有的,哪些是线程共享区?
JVM 内存模型主要指运行时的数据区,包括 5 个部分,如下图所示
线程独占
栈也叫方法栈,是线程私有的,线程在执行每个方法时都会同时创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。
本地方法栈与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行 Java 方法使用栈,而执行 native 方法使用本地方法栈。
程序计数器保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行 Java 方法服务,执行 native 方法时,程序计数器为空。
栈、本地方法栈、程序计数器这三个部分都是线程独占的。
线程共享
堆是JVM管理的内存中最大的一块,堆被所有线程共享,目的是为了存放对象实例,几乎所有的对象实例都在这里分配。当堆内存没有可用的空间时,会抛出OOM异常。根据对象存活的周期不同,JVM把堆内存…
方法区也是各个线程共享的内存区域,又叫非堆区。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,JDK1.7中的永久代和JDK1.8中的Metaspace都是…
4. 每个区存储的数据的特点?
jvm内存结构划分
2.jvm运行时数据区 程序计数器的特点及作用:
1.程序计数器是一块较小的内存空间,几乎可以忽略;
2.是当前线程所执行的字节码的行号指示器;
3.Java多线程执行时,每条线程都会有一个独立的程序计数器,各条线程之间互不影响;
4.该区域是“线程私有”的内存,每个线程独立存储;
5.该区域不存在OOM;
6.无GC回收
3.jvm运行时数据区 虚拟机栈的特点及作用:
1.线程私有
2.方法执行会创建栈帧,存储局部变量表等信息;
3.方法执行遵循(先进后出)FILO;
4.栈深度大于虚拟机所允许会抛出栈溢出 StackOverflowError
5.栈需要扩展而无法申请空间OutOfMemoryError;—hostspot虚拟机没有
6.栈里面运行方法,存放方法的局部变量名,变量名所指向的值都存在堆上;
7.栈一般不设置大小,栈所占的空间很小,可通过-Xss1M设置,如果不设置,默认为1M
8.随线程而生,随线程而灭
9.该区域不会GC
4.jvm运行时数据区 Java堆的特点及作用:
1.线程共享的一块区域
2.虚拟机启动时创建
3.虚拟机管理的最大的一块内存区域
4.存放所有实例对象级数组
5.GC垃圾收集器的主要管理区域
6.分新生代 老年代
7.新生代:eden-s0 -s1(堆/3),老年代(2堆/3)
8.可通过-Xmx、-Xms调节堆得大小;
9.无法再扩展
10.从分配内存角度看。所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(TLAB)以提升对象分配时的效率
5.jvm运行时数据区 元空间的特点及作用:
1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代; 2、元空间与Java堆类似,是线程共享的内存区域;
3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据;
4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设 置元空间大小;
5. 程序计数器是什么,它是线程独有的吗? 它是否有内存溢出问题.
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。- - 摘自《深入理解Java虚拟机》
特 点
1.如果线程正在执行的是Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址
2.如果正在执行的是Native 方法,则这个技术器值为空(Undefined)
3.此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域 即:不会出现内存溢出的情况
4.程序计数器保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行 Java 方法服务,执行 native 方法时,程序计数器为空。
一、程序计数器会随着线程的启动而创建,先来直观的看下计数器中会存哪些内容
有代码如下
package com.xnccs.cn.share;
public class ShareCal {
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return ( a + b ) * c;
}
}
这是一段非常简单的计算代码,我们先编译成Class 文件再使用 javap 反汇编工具看下class 文件中数据格式,如下图
图中使用红框框起来的就是字节码指令的偏移地址,偏移地址对应的bipush 等等是jvm 中的操作指令,这是入栈指令。这里不作详细分析,有机会再分享。 当执行到方法calc()时在当前的线程中会创建相应的程序计数器,在计数器中为存放执行地址 (红框中的)0 2 3…等等
这也说明在我们程序运行过程中计数器中改变的只是值,而不会随着程序的运行需要更大的空间,也就不会发生溢出情况。–特点3
三、为什么执行的是native 方法时,为undefined
由上我们知道计数器记录的字节码指令地址,但是native 本地(如:System.currentTimeMillis()/ public static native long currentTimeMillis();)方法是大多是通过C实现并未编译成需要执行的字节码指令所以在计数器中当然是空(undefined).
问: 那native 方法的多线程是如何实现的呢?
答: native 方法是通过调用系统指令来实现的,那系统是如何实现多线程的则 native 就是如何实现的
6. 虚拟机栈上保存哪些数据?怎么放?虚拟机栈是线程独有的吗,它是否有内存溢出问题?虚拟机栈的优点?
- 虚拟机栈的大小是否可动?是否会有异常出现?
1.虚拟机常见异常
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个StackOverflowError异常;
如果Java虚拟机栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常;
2.测试栈的大小
/**
测试栈大小
* JDK 1.8
*/
public class Stack_Test01 {
public static int i=1;
public static void main(String[] args) {
//我的电脑默认测试JVM栈的大小为9832左右
//添加JVM命令行参数:-Xss1024k 之后为9777左右
//添加JVM命令行参数:-Xss1m 之后为9789左右
System.out.println(i++);
main(args);
}
}
输出结果:
8. 如何设置虚拟机栈大小?
3.设置栈内存大小
我们可以使用虚拟机参数-Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度;
-Xss size
设置线程堆栈大小(以字节为单位)。附加字母k或K表示KB,m或M表示MB,和g或G表示GB。默认值取决于平台:Linux / x64(64位):1024 KB macOS(64位):1024 KB Oracle Solaris /
x64(64位):1024 KB Windows:默认值取决于虚拟内存
下面的示例以不同的单位将线程堆栈大小设置为1024 KB:
复制-Xss1m (1mb)
-Xss1024k (1024kb)
-Xss1048576
设置方式:
9. 什么叫本地方法? 是否可以写一个例子来实现本地方法,以输出一个hello world?
**
10.什么叫本地方法栈?有什么作用?它是线程私有的吗? 它是否有可能抛出异常?
Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
本地方法是使用C语言实现的。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。 它甚至可以直接使用本地处理器中的寄存器 直接从本地内存的堆中分配任意数量的内存。
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
11. jvm规范一定强制要求实现本地方法栈吗?
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
12. 方法区是线程独有的吗?它是否有异常?它的作用?
方法区(Method Area)
方法区是被各个线程共享的运行时内存区域。方法区类似于传统语言的编译代码的存储区。它存储了每一个类的结构信息,例如:运行时常量池、字段和方法数据,构造函数和普通方法的字节码内容,还包括一些用于类、实例、接口初始化用到的特殊方法。
三、方法区和运行时常量池溢出
方法区用于存放Class的相关信息,对这个区域的测试,基本思路是运行时产生大量的类去填满方法区,直到溢出。使用CGLib实现。
方法区溢出也是一种常见的内存溢出异常,在经常生成大量Class的应用中,需要特别注意类的回收情况,这类场景除了使用了CGLib字节码增强和动态语言外,常见的还有JSP文件的应用(JSP第一次运行时要编译为Java类)、基于OSGI的应用等。
/**
* 测试JVM方法区内存溢出
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class MethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject{}
}
13. 方法区的演进, jdk7及以前,它叫什么? jdk8开始,这又叫什么.
在 JDK 7 及以前,习惯上把方法区,称为永久代。从 JDK8 开始,使用元空间取代了永久代。
JDK 8 后,元空间存放在堆外内存中。
本质上,方法区和永久代并不等价。仅是对 hotspot 而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念。
现在来看,当年使用永久代,不是好的 idea。导致 Java 程序更容易 OOM(超过-XX:MaxPermSize上限)。
而到了 JDK 8,终于完全废弃了永久代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间(Metaspace)来代替。
元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
永久代、元空间二者并不只是名字变了,内部结构也调整了。
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OOM 异常。
14. 方法区或永久代的大小如何设置?
1.方法区内存大小设置
1.方法区的大小可以不是固定的,JVM可以根据应用需要自动调整。
a)JDK7及以前(了解):-XX:PermSize设置永久代初始大小。-XX:MaxPermSize设置永久代最大可分配空间。(JDK7目前已经很少用了,这两个参数在JDK8及以后已经没有了,所以不必掌握,了解一下)
b)
JDK8及以后:可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置元空间初始大小以及最大可分配大小。
例子:设置初始大小是100M,最大可分配空间也是100M。-XX:MetaspaceSize=100m
-XX:MaxMetaspaceSize=100m。
1.如果不指定元空间的大小,默认情况下,元空间最大的大小是系统内存的大小,元空间一直扩大,虚拟机可能会消耗完所有的可用系统内存。
2.如果元空间内存不够用,就会报OOM。
3.默认情况下,对应一个64位的服务端JVM来说,其默认的-XX:MetaspaceSize值为21MB,这就是初始的高水位线,一旦元空间的大小触及这个高水位线,就会触发Full
GC并会卸载没有用的类,然后高水位线的值将会被重置。
4.从第3点可以知道,如果初始化的高水位线设置过低,会频繁的触发Full GC,高水位线会被多次调整。所以为了避免频繁GC以及调整高水位线,建议将-XX:MetaspaceSize设置为较高的值,而-XX:MaxMetaspaceSize不进行设置。