JVM虚拟机-02、JVM运行时数据区解析

JVM虚拟机-02、JVM运行时数据区解析

运行时数据区(Run-Time Data Areas)

官网定义

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

The Java Virtual Machine defines various run-time data areas that are used

during execution of a program. Some of these data areas are created on Java

Virtual Machine start-up and are destroyed only when the Java Virtual Machineexits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

JVM整体结构及内存模型图解

import com.bloom.common.vo.UserTaskVo;

public class Math {

    public static int initData = 666;
    public static UserTaskVo userTaskVo = new UserTaskVo();


    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }


    public static void main(String[] args) {
        Math math = new Math();
        math.compute();


    }

}

针对以上代码,JVM运行时的整体结构如图所示

运行时数据区(内存结构)

1 方法区(Method Area)

类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在这里定义。简单来说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是为了和Java的堆区分开(jdk1.8以前hotspot虚拟机叫永久代、持久代,jdk1.8时叫元空间)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

The Java Virtual Machine has a method area that is shared among all Java Virtual  Machine threads.The method area is created on virtual machine start-up.Although the method area is logically part of the heap,......

If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

此时回看装载阶段的第2步:(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放.

2 堆(Heap)

虚拟机启动时自动分配创建,用于存放对象的实例,几乎所有对象都在堆上分配内存,当对象无法在该空间申请到内存是将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.The heap is created on virtual machine start-up.

2.1 新生代(Young Generation)

类出生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,

结束生命。新生代分为两部分:伊甸区(Eden space)和幸存者区(Survivor space),所有的类都是在伊甸区被new出来的。幸存区又分为From和To区。当Eden区的空间用完是,程序又需要创建对象,JVM的垃圾回收器将Eden区进行垃圾回收(Minor GC),将Eden区中的不再被其它对象应用的对象进行销毁。然后将Eden区中剩余的对象移到From Survivor区。若From、Survivor区也满了,再对该区进行垃圾回收,然后移动到To Survivor区。

2.2 老年代(Old Generation)

新生代经过多次GC仍然存货的对象移动到老年区。若老年代也满了,这时候将发生Major GC(也可以叫FullGC),进行老年区的内存清理。若老年区执行了FullGC之后发现依然无法进行对象的保存就会抛出OOM异常

2.3元空间(Meta Space)

在JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。

2.4为什么移除了永久代?

移除永久代是为融合HotSpot与JRockit而做出的努力,因为JRockit没有永久代,不需要配置永久代。

3 栈(Stack)

虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

图解

4 本地方法栈(Native Method Stacks)

和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行native方法服务。登记native方法,在Execution Engine执行时加载本地方法库

5 程序计数器(The PC Register)

程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,则这个计数器为空。

对象的生命周期理解

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次C加一岁),然后被回收。

常见问题

  • 为什么需要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还能在新生代中存活的对象,才会被送到老年代。

  • 为什么需要两个Survivor区

最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设

现在只有一个Survivor区,我们来模拟一下流程:

刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循

环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的

存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

体验与验证

1 使用jvisualvm查看

visualgc插件下载链接 https://visualvm.github.io/pluginscenters.html

2 栈内存溢出

代码

// JVM设置  -Xss128k(默认1M)
public class StackOverflowTest {

    static int count = 0;

    static void redo() {
        count++;
        redo();
    }

    public static void main(String[] args) {
        try {
            redo();
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println(count);
        }
    }
}
// JVM设置  -Xss128k(默认1M)
public class StackOverflowTest {

    static int count = 0;

    static void redo() {
        count++;
        redo();
    }

    public static void main(String[] args) {
        try {
            redo();
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println(count);
        }
    }
}
运行结果:
java.lang.StackOverflowError
	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:12)
	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)

3 堆内存溢出

设置运行参数比如-Xmx20M -Xms20M

import java.util.ArrayList;

public class HeapTest {

    byte[] a = new byte[1024*100];

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

        ArrayList<HeapTest> objects = new ArrayList<>();
        while (true){
            objects.add(new HeapTest());
            Thread.sleep(30);
        }


    }
}
Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit
exceeded
posted @ 2020-04-14 15:50  teago  阅读(205)  评论(0)    收藏  举报