关于java:干货图文并茂深入理解JVM

2次阅读

共计 5618 个字符,预计需要花费 15 分钟才能阅读完成。

联合字节码指令了解 Java 虚拟机栈和栈帧

栈帧:每个栈帧对应一个被调用的办法,能够了解为一个办法的运行空间

每个栈帧中包含局部变量表(Local Variables),操作数栈(Operand Stack) 执行运行时常量池的援用(A reference to the run-time constant pool),办法返回地址(Return Address)和附加信息

局部变量表:办法中定义的局部变量以及办法的参数寄存在这张表中

局部变量表中的变量不可间接应用,如须要应用的话,必须通过相干指令将其加载至操作数栈中作为操作数应用

操作数栈:以压栈和出栈的形式存储操作数

动静链接:每个栈帧都包好一个指向运行时常量池中该栈帧所属的办法援用,持有这个援用是为了反对办法调用过程中的动静链接(Dynamic Linking)

办法返回地址:当一个办法开始执行后,只有两种形式退出,一种是遇到办法返回的字节码指令,一种是遇见异样,并且这个异样没有在办法体内失去解决。

1 class Person{ 

private String name="Jack"; 

private int age; 

private final double salary=100; 

private static String address; 

private final static String hobby="Programming";
 public void say(){System.out.println("person say...");
 }

public static int calc(int op1,int op2){op1=3; int result=op1+op2; return result;}

public static void order(){}

public static void main(String[] args){calc(1,2); order();} 
}

class Person {

public static int calc(int, int);

Code:

0: iconst\_3 // 将 int 类型常量 3 压入[操作数栈]

1: istore\_0 // 将 int 类型值存入[局部变量 0]

2: iload\_0 // 从 [局部变量 0] 中装载 int 类型值入栈

3: iload\_1 // 从 [局部变量 1] 中装载 int 类型值入栈

4: iadd // 将栈顶元素弹出栈,执行 int 类型的加法,后果入栈

5: istore\_2 // 将栈顶 int 类型值保留到 [局部变量 2] 中

6: iload\_2 // 从 [局部变量 2] 中装载 int 类型值入栈

7: ireturn // 从办法中返回 int 类型的数据

}

栈指向堆

如果在栈帧中有一个变量,类型为援用类型,比方 Object object=new Object(),这个时候就是典型的栈中元素指向堆中的对象。

办法区指向堆

办法区会寄存动态变量,常量等数据,如果上面的这种状况,就是典型的办法区中元素执行堆中的对象

private static Object obj=new Object();

堆指向办法区

办法区会蕴含类的信息,堆中会有对象,那怎么晓得对象是哪个类创立的呢?

思考:一个对象怎么晓得它是由哪个类创立进去的呢?怎么记录呢?这就须要理解一个 Java 对象的具体信息了

Java 对象内存布局

一个对象在内存中包含 3 个局部:对象头,实例数据和对齐填充

内存模型:

一块是非堆区,一块是堆区。

堆辨别为两大块:一个是 old 区,一个是 Young 区,

Young 辨别为两个大块,一个是 Survivor 区(s0+s1),一块是 Eden 区,Eden:s0:s1=8:1:1

s0 和 s1 一样大, 也能够叫 From 和 to

依据之前的对于 Heap 的介绍能够晓得,个别对象和数组的创立会在堆中分配内存空间,要害是堆中有那么多区域,那一个对象的创立到底在哪个区域呢?

对象创立所在的区域

个别状况下,新创建的对象都会调配到 Eden 区,一些非凡的大的对象会间接调配到 old 区

比方 有对象 A,B,C 等创立 Eden 区,然而 Eden 区的内存空间必定无限,比方有 100M,如果曾经应用了 100M 或者达到一个设定的临界值,这时候就须要对 Eden 内存空间进行清理,即垃圾收集,这样的 GC 咱们称之为 Minor GC,Minor GC 指得是 Young 区的 GC

通过 GC 之后,有些对象就会被清理掉,有些对象可能还在存活着,对于存活的对象须要将其复制到 Survivor 区,而后再清空 Eden 区中的这些对象

Survivor 区详解

survivor 辨别为 so 和 s1,也能够叫 From to 在同一个工夫点上,so 和 s1 只能有一个区有数据,另一个是空的

接着下面的 GC 来说,比方一开始只有 Eden 区和 From 中有对象,To 中是空的。此时进行一次 GC 操作,From 区中对象的年龄就会 +1,咱们晓得 Eden 区中所有存活的对象会被复制到 To 区,

From 区中还能存活的对象会有两个去处。若对象年龄达到之前设置好的年龄阈值,此时对象会被挪动到 Old 区,如果 Eden 区和 From 区没有达到阈值的对象会被复制到 To 区。

此时 Eden 区和 From 区曾经被清空(被 GC 的对象必定没了,没有被 GC 的对象都有了各自的去处)。这时候 From 和 To 替换角色,之前的 From 变成了 To,之前的 To 变成了 From。

也就是说无论如何都要保障名为 To 的 Survivor 区域是空的。Minor GC 会始终反复这样的过程,晓得 To 区被填满,而后会将所有对象复制到老年代中。

Old 区详解

从下面的剖析能够看出,个别 Old 区都是年龄比拟大的对象,或者绝对超过了某个阈值的对象。在 Old 区也会有 GC 的操作,Old 区的 GC 咱们称作为 Major GC

对象的一辈子了解

我是一个一般的 Java 对象, 我出世在 Eden 区, 在 Eden 区我还看到和我长的很像的小兄弟, 咱们在 Eden 区中玩了挺长时间。有一天 Eden 区中的人切实是太多了, 我就被迫去了 Survivor 区的“From”区, 自从去了 Survivor 区, 我就开始漂了, 有时候在

Survivor 的“From”区, 有时候在 Survivor 的“To”区, 居无定所。直到我 18 岁的时候, 爸爸说我成人了, 该去社会上闯闯了。于是我就去了年轻代那边, 年轻代里, 人很多, 并且年龄都挺大的, 我在这里也意识了很多人。在年轻代里, 我生存了 20 年(每次

GC 加一岁), 而后被回收。

常见问题

1. 如何了解 Minor GC,Major GC,Full GC

Minor GC:新生代

Major GC:老年代

Full GC:新生代 + 老年代

2. 为什么须要 Survivor 区?只有 Eden 不行吗?

如果没有 Survivor,Eden 区每进行一次 Minor GC, 并且没有年龄限度的话,存活的对象就会被送到老年代。这样一来,老年代很快被填满, 触发 Major GC(因为 Major GC 个别随同着 Minor GC, 也能够看做触发了 Full GC)。

老年代的内存空间远大于新生代, 进行一次 Full GC 耗费的工夫比 Minor GC 长得多。执行工夫长有什么害处? 频发的 Full GC 耗费的工夫很长, 会影响大型程序的执行和响应速度。

可能你会说,那就对老年代的空间进行减少或者较少咯。如果减少老年代空间,更多存活对象能力填满老年代。尽管升高 Full GC 频率,然而随着老年代空间加大, 一旦产生 Full

GC, 执行所须要的工夫更长。如果缩小老年代空间,尽管 Full GC 所需工夫缩小,然而老年代很快被存活对象填满,Full GC 频率减少。

所以 Survivor 的存在意义, 就是缩小被送到老年代的对象, 进而缩小 Full GC 的产生,Survivor 的预筛选保障, 只有经验 16 次 Minor GC 还能在新生代中存活的对象, 才会被送到老年代。

3 为什么须要两个 Survivor 区?

最大的益处就是解决了碎片化。也就是说为什么一个 Survivor 区不行? 第一局部中, 咱们晓得了必须设置 Survivor 区。假如当初只有一个 Survivor 区, 咱们来模仿一下流程:

刚刚新建的对象在 Eden 中, 一旦 Eden 满了, 触发一次 Minor GC,Eden 中的存活对象就会被挪动到 Survivor 区。这样持续循环上来, 下一次 Eden 满了的时候, 问题来了, 此时进行 Minor GC,Eden 和 Survivor 各有一些存活对象, 如果此时把 Eden 区的

存活对象硬放到 Survivor 区, 很显著这两局部对象所占有的内存是不间断的, 也就导致了内存碎片化。永远有一个 Survivor space 是空的, 另一个非空的 Survivor space 无碎片。

4 新生代中 Eden:s1:s0 为什么是 8:1:1?

新生代中的可用内存:复制算法用来担保的内容为 9:1

可用内存中 Eden:s1 区为 8:1

即新生代中 Eden:s1:s2=8:1:1

体验与验证

1)堆内存溢出

@RestController 

public class HeapController {List<Person> list=new ArrayList<Person>();
 @GetMapping("/heap") 

public String heap() throws Exception{while(true){list.add(new Person()); 
Thread.sleep(1); 
} 
} 
}

记得设置参数比方 -Xmx20M -Xms20M

运行后果:

Exception in thread “http-nio-8080-exec-2” java.lang.OutOfMemoryError: GC overhead limit

exceeded

2)办法区内存溢出

比方向办法区中增加 class 的信息

<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
public class MyMetaspace extends ClassLoader {public static List<Class<?>> createClasses() {List<Class<?>> classes = new ArrayList<Class<?>>(); 

for (int i = 0; i < 10000000; ++i) {ClassWriter cw = new ClassWriter(0); 
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); 
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
 mw.visitVarInsn(Opcodes.ALOAD, 0); 
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); 
mw.visitInsn(Opcodes.RETURN); 
mw.visitMaxs(1, 1);
 mw.visitEnd(); 
Metaspace test = new Metaspace();
 byte[] code = cw.toByteArray(); 
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); 
classes.add(exampleClass); 
}

return classes;
 }
 }
@RestController public class NonHeapController {List<Class<?>> list=new ArrayList<Class<?>>(); 
@GetMapping("/nonheap") public String nonheap() throws Exception{while(true){list.addAll(MyMetaspace.createClasses()); 
Thread.sleep(5); } } }

设置 Metaspace 的大小,比方 -XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M

运行后果:

java.lang.OutOfMemoryError: Metaspace

at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0\_191]

at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0\_191]

虚拟机栈

public class StackDemo { 

public static long count=0; 

public static void method(long i){System.out.println(count++); 
method(i); }

public static void main(String[] args) {method(1); } }

了解和阐明:

Stack Space 用来做办法的递归调用时压入 Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽 StackSpace,爆出 StackOverflow 的谬误。

-Xss128k:设置每个线程的堆栈大小。JDK 5 当前每个线程堆栈大小为 1M,以前每个线程堆栈大小为 256K。依据利用的线程所需内存大小进行调整。在雷同物理内存下,减小这个值能生成更多的线程。然而操作系统对一个过程内的线程数还是有

限度的,不能有限生成,经验值在 3000~5000 左右。线程栈的大小是个双刃剑,如果设置过小,可能会呈现栈溢出,特地是在该线程内有递归、大的循环时呈现溢出的可能性更

大,如果该值设置过大,就有影响到创立栈的数量,如果是多线程的利用,就会呈现内存溢出的谬误。

关注公众号:java 宝典

正文完
 0