如何优雅的学习JVM,进行篇(二)

zhouxihua0 2020-05-30

一、理解Java虚拟机栈和栈帧

1.1 什么是栈帧呢?

每个栈帧被认为对应是一个被调用的方法,可以理解为一个方法的运行空间。

官方地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6

栈帧的组成部分:

1、局部变量表(Local Variables):方法中的局部变量以及方法的参数存放在这张表中,局部变量中的变量不可以直接使用,如果需要使用的话,必须通过相关指令将其加载到操作数栈中作为操作数使用。

2、操作数栈(Operand Stack):以压栈和出栈的方式存储操作数。

3、动态链接(Dynamic Linking):每个栈帧都包含指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。

4、方法返回地址(Return Address):当一个方法执行时,只有两种方式可以退出,一种是遇到方法返回的字节码指令,一种是遇见异常,并且这个异常没有在方法体内得到处理。

5、附加信息

如何优雅的学习JVM,进行篇(二)

如何优雅的学习JVM,进行篇(二)

Person.java

public class Person {
    private String name = "Kevin";
    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();
    }
}

反编译指令宝典,oracle官网:

https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

Compiled from "Person.java"
public 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类型的加法,结果入栈
   【For example, the iadd instruction (§iadd) adds two int values together. It
requires that the int values to be added be the top two values of the operand stack, pushed
there by previous instructions. Both of the int values are popped from the operand stack.
They are added, and their sum is pushed back onto the operand stack. Subcomputations may be
nested on the operand stack, resulting in values that can be used by the encompassing
computation.】
   5: istore_2   //将栈顶int类型值保存到[局部变量2]中
   6: iload_2    //从[局部变量2]中装载int类型值入栈
   7: ireturn    //从方法中返回int类型的数据
...
}

例子中的Java虚拟机和栈帧,如图:

如何优雅的学习JVM,进行篇(二)

1.2 栈指向堆

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

如何优雅的学习JVM,进行篇(二)

1.3 方法区指向堆

方法区会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中的元素指向堆中的对象。

private static Object obj = new Object();

如何优雅的学习JVM,进行篇(二)

1.4 堆指向方法区

方法区中会包含类的信息,堆中会有对象,那么一个对象是由哪个类创建出来的呢?它是如何记录信息的呢?我们需要了解一个Java对象的具体信息。

如何优雅的学习JVM,进行篇(二)

1.5 Java对象内存布局

一个Java对象在内存中的布局分为3个部分:

1、对象头:一系列的标记位(Mark Word)、指向对象对应的类元数据的内存地址(Class Pointer)、数组长度(Length)

2、实例数据:包含各对象所有的成员变量,其大小由变量类型决定

3、对齐填充:为了对象的大小为8字节的整数倍

如何优雅的学习JVM,进行篇(二)

二、JVM内存模型

2.1 内存模型结构

内存模型结构分为2个部分:

1、非堆区:

2、堆区:一个是Old区,另一个是Young区

另:Young区分为2个部分,一个是Survivor区(S0+S1),另一个是Eden区。

Eden : S0 : S1 = 8 : 1 : 1,S0和S1一样大,也可以叫做From和To。

如何优雅的学习JVM,进行篇(二)

2.2 对象创建所在区域

在一般情况下:

1、新创建的对象都会分配到Eden区

2、一些特殊的大对象都会分配到Old区

例如:有对象A、B、C等创建在Eden区,但是由于Eden区的内存空间有限,其大小只有100M,假如意见使用了100M或达到了一个设定的临界值,这个时候就需要对Eden区的内存空间进行清理,即垃圾回收(Garbage Collect),这样的GC也被称为Minor GC, Minor GC指的是Young区的GC。

2.3 Survivor区

Survivor分为两块,即S0和S1,也可以叫做From和To。

在同一个时间点上,S0和S1只能有一区有数据,另一区只能空着。

例如:接着上面示例的GC来说,一开始只有Eden区和S0中有对象,S1是空的。此时进行一次GC操作,S0区中的对象年龄就会+1,Eden区中的所有存活的对象会被复制到S1区,S0区中还能存活的对象会有两个去处。

若对象年龄达到之前设置好的年龄阈值,此时对象会被移动到Old区,没有达到阈值的对象会被复制到S1区。此时Eden区和S0区已经被清空(被GC的对象肯定是没有了,没有被GC的对象都有各自的去处了)。

这个时候S0和S1交换角色,之前的S0变成S1,之前的S1变成S0了。也就是说无论如何都要保证名为S1的Survivor区域都是空的。

Minor GC会一直重复这样的过程,直到S1区被填满,然后会将所有的对象复制到Old区中。

2.4 Old区

一般Old区都是年龄较大的对象,或者相对超过了某个阈值的对象。在Old区也会有GC操作,Old区的GC操作我们被称为Major GC,每次GC之后还能存活的对象年龄也会+1,如果超过了某个阈值,也会被回收的。

2.5 如何理解对象的一辈子

如何优雅的学习JVM,进行篇(二)

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到了跟我长得很像的兄弟,我们在Eden区玩了挺长时间。有一天Eden区的人实在太多了,我就被迫去了Survivor区的S0区,自从去了Survivor区,我就开始漂了,有时候在Survivor区的S0区,有时候在Survivor区的S1区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯荡了。

于是我就去Old区了,老年代里,人很多,并且年龄都挺大的,我在这里认识了很多人。在Old区里我生活了20年(每次GC加一岁),最后就被回收了。

2.6 常见的问题

1、如何理解Minor/Major/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区,在刚刚新建的对象在Eden中,一单Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区中,这样循环下去,下一次Eden满了的时候,那么这样问题就来了,此时进行Minor GC,Eden和Survivor各有一些存活的对象,如果此时把Eden区中存活的对象硬是放到Survivor区,很明显这两部分对象所占的内存是不连续的,也就导致了内存碎片化。永远只有一个Survivor 空间是空的,而另一个Survivor 空间无碎片。

4、新生代中的Eden:S0:S1为什么是8:1:1?

新生代中的可用内存:复制算法用来担保的内存为9:1 可用内存中的Eden:S0为8:1 即新生代中的Eden:S0:S1位8:1:1

三、使用与验证

3.1 使用VisualVM

使用JDK自带的VisualVM工具进行查看:

如何优雅的学习JVM,进行篇(二)

3.2 堆内存溢出

示例代码:

import java.util.List;

public class HeapOut {

    public static void main(String[] args) throws Exception {

        List<person> personList = new ArrayList&lt;&gt;();
        while (true) {
            personList.add(new Person());
            Thread.sleep(1);
        }
    }
}

设置启动参数:-Xmx20M -Xms20M,我们启动main方法后,让程序持续运行一段时间可以看到控制台出现如下信息:

com.gooagoo.dop.trans.test.json.HeapOut
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	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 com.gooagoo.dop.trans.test.json.HeapOut.main(HeapOut.java:12)

使用visualVM查看:

如何优雅的学习JVM,进行篇(二)

3.3 方法区内存溢出

示例:向方法区中添加class信息

asm依赖

<dependency>
&nbsp;&nbsp;<groupid>asm</groupid>
&nbsp;&nbsp;<artifactid>asm</artifactid>
&nbsp;&nbsp;<version>3.3.1</version>
</dependency>

代码

package com.gooagoo.dop.trans.test.json;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.ArrayList;
import java.util.List;
public class MyMetaspace extends ClassLoader {
    public static List<class<?>&gt; createClasses() {
    List<class<?>&gt; classes = new ArrayList<class<?>&gt;();
    for (int i = 0; i &lt; 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();
        MyMetaspace test = new MyMetaspace();
        byte[] code = cw.toByteArray();
        Class<!--?--> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
        classes.add(exampleClass);
    }
    return classes;
    }
    
    
     public static void main(String[] args) throws Exception {
        List<class<?>&gt; list=new ArrayList<class<?>&gt;();
        while(true){
            list.addAll(MyMetaspace.createClasses());
            Thread.sleep(5); 
        }
    }
}

设置启动参数Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M,我们启动main方法后,让程序持续运行一段时间可以看到控制台出现如下信息:

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at com.gooagoo.dop.trans.test.json.MyMetaspace.createClasses(MyMetaspace.java:21)
	at com.gooagoo.dop.trans.test.json.MyMetaspace.main(MyMetaspace.java:30)

3.4 虚拟机栈StackOvewFlow

代码示例:

public class JvmStack {

    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);
    }
}

启动main方法后,让程序持续运行一段时间可以看到控制台出现如下信息:

如何优雅的学习JVM,进行篇(二)

3.5 理解与说明

Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,所以出现StackOverFlow的错误。

线程栈的大小是个双刃剑,如果设置过小,可能会出现溢出,特别是在该线程递归、大的循环时出现溢出的可能性更大;如果设置过大,就有影响到创建栈的数量,如果是多线程应用,就会出现内存溢出的错误。

-Xss128k:设置每个线程的堆栈大小。JDK 5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线 程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有 限制的,不能无限生成,经验值在3000~5000左右。

未完待续......

</class<?></class<?></init></init></class<?></class<?></class<?></person>

来源:亳州网站优化

相关推荐