1.运行时内存区域的划分
java
虚拟机在执行 java
程序的过程中,会将它管理的内存区域划分为若干个不同的数据区域,这些区域有各自不同的用途,以及创建和销毁时间。
HotSpot
虚拟机并不区分虚拟机栈和本地方法栈,所以下面统称这两种内存区域叫Java 栈
。
2.每个内存区域的作用以及服务的对象
-
程序计数器
程序计数器是一块较小的内存空间,它可以看做是当前线程所执行字节码的行号指示器。如果线程正在执行的是一个java
方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是Native
方法,这个计数器的的值为空(Undefined
)。 -
Java
虚拟机栈
虚拟机栈描述的是Java
方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame
)用于存储局部变量表、操作栈、动态链接、方法出口等。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈。
局部变量表存放了编译期可知的各种基本数据类型以及对象的引用。其中64
位长度的long
和double
类型的数据会占用2
个局部变量空间,其余数据类型只占用1
个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 -
本地方法栈
本地方法栈和虚拟机栈所发挥的作用是非常类似的,只不过虚拟机栈是为虚拟机执行java
方法服务的,而本地方法栈是为虚拟机使用到的Native
方法服务的。 -
堆
堆存放的是对象的实例,也是垃圾收集器管理的主要区域。根据Java
虚拟机规范规定,Java
堆可以处于物理上不连续的内存空间,只要在逻辑上连续即可。 -
方法区
它用于存储被虚拟机加载的类信息,常量,静态变量以及即时编译器编译后的代码等。类似的,Java
虚拟机规范规定,方法区可以处于物理上不连续的内存空间,只要逻辑上连续就可以。另外,方法区可以选择不实现垃圾收集器,因为垃圾收集行为在这个区域比较少见,这个区域的内存回收目标主要是针对常量池和类型卸载。 -
直接内存
直接内存并不是虚拟机运行时数据区域,也不是Java
虚拟机规范定义的内存区域,也叫做堆外内存。
3.可能抛出的异常
-
程序计数器
唯一一个在Java
虚拟机规范中没有规定任何OutOfMemoryError
情况的区域。 -
Java 栈(虚拟机栈和本地方法栈)
有两种可能抛出的异常:
OutOfMemoryError
:如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时。(如:创建的线程太多)
StackOverflowError
:如果线程请求的栈深度大于虚拟机所允许的深度时。(如:没有终止条件的递归调用) -
堆
OutOfMemoryError
:如果堆中没有内存完成实例分配,并且堆也无法再扩展时。 -
方法区
OutOfMemoryError
:当方法区无法满足内存分配需求时。
4.控制的参数
-
Java 堆
-Xms
最小堆大小
-Xmx
最大堆大小
如果两个值相等,堆不能自动扩展,是固定大小。 -
Java 栈(虚拟机栈和本地方法栈)
-Xss
栈大小
-方法区
-XX:PermSize
最小方法区大小
-XX:MaxPermSize
最大方法区大小
5.模拟异常
- 堆
OutOfMemoryError
/**
* -Xms10m
* -Xmx10m
* */
public class HeapOOM {
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
static class OOMObject {
}
}
运行结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at jvm.exception.HeapOOM.main(HeapOOM.java:13)
解决这种溢出的思路是:首先确定是内存泄露还是内存溢出导致的异常。可以通过堆转储快照进行分析,如果是内存泄露导致的,就需要检查泄露的对象为什么不能被垃圾回收,也即寻找泄露对象与 GC Roots
的可达路径。如果是内存溢出查看
- 栈
OutOfMemoryError
:
/**
* -Xss128k
*/
public class StackOOM {
public static void main(String[] args) {
while (true) {
new Thread(() -> {
while (true){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//...
}
}).start();
}
}
}
执行结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at jvm.exception.StackOverflow.main(StackOverflow.java:23)
StackOverflowError
:
/**
* -Xss128k
*/
public class StackOverflow {
public static void main(String[] args) {
test();
}
public static void test(){
test();
}
}
执行结果:
Exception in thread "main" java.lang.StackOverflowError
at jvm.exception.StackOverflow.test(StackOverflow.java:11)
at jvm.exception.StackOverflow.test(StackOverflow.java:11)
at jvm.exception.StackOverflow.test(StackOverflow.java:11)
at jvm.exception.StackOverflow.test(StackOverflow.java:11)
....
- 方法区
OutOfMemoryError
:可以用两种方式导致这种异常,增加更多的常量,创建更多的代理类。
/**
* -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> values = new ArrayList<>();
int i=0;
while (true){
values.add(String.valueOf(i++).intern());
}
}
}
上面代码中的
intern
方法的作用是:判断是否存在需要的常量,如果存在就返回,如果不存在,则将当前值加入到常量池中。
执行结果:
发现设置了 -XX:PermSize和-XX:MaxPermSize
并不会导致方法区溢出。原来在 java8
中移除了这两个参数,原来的永久代(PermGen
)概念也没有了,取而代之的是元空间(Metaspace
)。不过它们也有一些区别:原先永生代中类的元信息会被放入本地内存(元数据区),将类的静态变量和内部字符串放入到java堆中。
新的元空间设置参数:
-XX:MetaspaceSize
,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对改值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize
,最大空间,默认是没有限制的。
-XX:MinMetaspaceFreeRatio
,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。
-XX:MaxMetaspaceFreeRatio
,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。
总结起来,java8中对方法区的实现做了如下的变化:
- 移除了永久代(
PermGen
),替换为元空间(Metaspace
); - 永久代中的
class metadata
转移到了native memory
(本地内存,而不是虚拟机); - 永久代中的
interned Strings
和class static variables
转移到了Java heap
; - 永久代参数 (
PermSize MaxPermSize
) -> 元空间参数(MetaspaceSize MaxMetaspaceSize
)
那么,很明显上面的代码中的字符串常量是被放到了堆中了,增加参数-Xms10M和-Xmx10M
,验证效果如下:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
Disconnected from the target VM, address: '127.0.0.1:53648', transport: 'socket'
at java.nio.CharBuffer.wrap(CharBuffer.java:373)
at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:265)
at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
at java.io.PrintStream.newLine(PrintStream.java:545)
at java.io.PrintStream.println(PrintStream.java:807)
at jvm.exception.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:17)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize1=10M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0
在上面的结果中我们不仅看到了 OutOfMemoryError
,还有一句 GC overhead limit exceeded
,这句是什么意思的?
GC overhead limt exceed
检查是 Hotspot VM 1.6
定义的一个策略,通过统计 GC
时间来预测是否要 OOM
了,提前抛出异常,防止 OOM
发生。Sun
官方对此的定义是:并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError
。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存
。用来避免内存过小造成应用不能正常工作。“
/**
* //-XX:PermSize=128k -XX:MaxPermSize=128k
* -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create();
}
}
static class OOMObject {
}
}
执行结果:
Error occurred during initialization of VM
OutOfMemoryError: Metaspace
6.对象访问
Object obj = new Object();
上面这行代码涉及到的内存区域有:方法区、堆、栈。
Object obj
这部分的语义将反映到 Java 栈的本地变量表
中。
new Object()
这部分的语义将反映到 Java 堆
中。
另外,还需要类型数据(对象类型、父类、实现的接口、方法等),这部分数据储存在 方法区
。
obj
中的引用代表的含义在不同的虚拟机中有不同的实现,主流的有两种:句柄和指针。
- 句柄
Java 堆
中将会划分出一块内存来作为句柄池,obj
中存储的是句柄地址,而句柄中包含对象的实例数据和类型数据的具体地址。
image.png - 指针
直接指针访问的方式中,类型数据的信息时存储在对象中,obj
中直接存储的是对象的地址。
image.png
这两种方式都有各自的优缺点
在对象被移动式,只要修改句柄中的对象对象地址即可,而 obj
中的句柄地址不用改变。但是直接指针方法的速度更快,它节省的一次指针定位的时间开销。
参考:
https://blog.csdn.net/laomo_bible/article/details/83067810
https://www.cnblogs.com/hucn/p/3572384.html
网友评论