双非本科准备秋招(8.2)——JVM1

第一天系统学习JVM!今天学了JVM是什么,学习JVM的作用,运行时的数据区域(重点),内存溢出。明天学GC。

运行时数据区域

整体认识

双非本科准备秋招(8.2)——JVM1_第1张图片 JDK1.7 双非本科准备秋招(8.2)——JVM1_第2张图片 JDK1.8

先写一下每个线程私有的三个数据区,分别是程序计数器,虚拟机栈,本地方法栈。

然后再写一下堆和方法区(概念,1.7的实现是永久代,1.8的实现是元空间)

程序计数器

作用:

1、记住下一条jvm指令的执行地址,一个线程的运行就是在它的程序计数器的变化下推动的。

2、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

3、多线程环境,线程来回切换时,线程自身的程序计数器能记住线程执行指令的位置。

特点:

1、是一块很小的内存空间,运行速度最快的存储区域。

2、线程私有,每个线程都有自己的程序计数器。

3、唯一的JVM规范中没有规定OutOfMemoryError的区域,因为它存储的是地址,占用内存小,几乎可以忽略不计。

虚拟机栈

概念

每个线程的创建的时候都会创建一个虚拟机栈,线程私有的,其实就是线程运行时需要的内存。

每个栈内由栈帧(Stack Frame)组成,实际上就是一个个java的方法,每个线程只能有一个活动栈帧(当前栈帧),就是只能执行当前一个方法。双非本科准备秋招(8.2)——JVM1_第3张图片

内存溢出(爆栈)

1、如果线程请求的栈深度大于JVM允许的深度,抛出StackOverFlowError。比如无限递归。

2、如果栈扩展时无法申请足够的内存,抛出OutOfMemoryError异常

栈帧及其结构

方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。return指令和抛出异常会使当前栈帧被弹出,继续执行下一个栈帧。

本地方法栈

本地方法在java中被native关键字修饰,可以看到本地方法没有方法体,它并不是java语言编写的,而本地方法栈就是为虚拟机使用Native方法服务的。HotSpot虚拟机(Oracle维护的java虚拟机,JVM只是一种规范,遵循JVM规范实现的java虚拟机有很多)中,本地方法栈和虚拟机栈合二为一。

我们用的Object类的clone、hashCode、notify、wait都是本地方法。

概念

内存最大的区域,用来存放对象实例,几乎所有对象实例和数组在此分配内存。

为什么说几乎?见以下引用:

Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。


著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/java/jvm/memory-area.html

java堆是GC管理的区域,垃圾会分为各种代,分代的目的是为了优化GC性能。

内存溢出

可以用-Xmx和-Xms控制堆的大小。

如果堆满了,会抛出OutOfMemoryError

方法区

概念

JVM并未规定方法区的实现,所以不同虚拟机中的实现都不相同,HotSopt虚拟机,简单来说,JDK1.7的实现是永久代,存在堆内存,JDK1.8彻底放弃了永久代,改为元空间,存在操作系统的内存中。

虚拟机要使用类的时候,会解析*.class文件获取信息,然后将信息存入方法区,方法区保存类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

常量池

java堆中的字符串常量池,用来保存字符串对象。

双非本科准备秋招(8.2)——JVM1_第4张图片

看一下test类反编译后的class文件,可以看到编译器给它加了个无参构造,并且合并了"a"+"b"

双非本科准备秋招(8.2)——JVM1_第5张图片

在终端执行java -v *.class命令,可以反编译,查看详细信息。

这个Constant Pool就是我们的常量池,#1等是每个字面量的地址

双非本科准备秋招(8.2)——JVM1_第6张图片

往下看可以看到JVM指令:

ldc命令判断常量池是否保存了对应字符串对象的引用,保存了就返回,没保存就在堆中创建字符串对象,并且将该字符串对象的引用保存到常量池中。

双非本科准备秋招(8.2)——JVM1_第7张图片

常量池中的信息会被加载到运行时常量池,但是一开始这些字符只是常量池的符号而已,还不是真正的java字符串对象,等到执行ldc #2这条命令的时候,才会创建字符串对象"a"。

运行时常量池

方法区的一部分,存放常量池表。

如下问题,继续加深对String的理解。

        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b";
        String s4 = s1+s2;
        String s5 = "ab";
        String s6 = s4.intern();

        System.out.println(s3 == s4);
        System.out.println(s3 == s5);
        System.out.println(s3 == s6);

        String x2 = new String("c") + new String("d");
        x2.intern();
        String x1 = "cd";
        
        System.out.println(x1 == x2);

答案是false;true;true;true;

s3==s4?

s1+s2做了什么?我们看看反编译后的jvm指令。

双非本科准备秋招(8.2)——JVM1_第8张图片

这里就能清晰地看出,new了一个StringBuilder对象,调用append方法,最后调用toString返回了String对象,并没有使用ldc指令,所以这个String对象创建在堆内存,不在常量池中。所以s3和s4指向不同的地址。

s3==s5?

很明显相等,都执行ldc指令,二者都是常量池中"ab"的地址。

s3==s6?

intern()方法做了什么?

这是个native方法,jdk1.8中,将这个字符串对象尝试放入常量池,如果有不放入;如果没有则放入,并且返回常量池中的对象。

所以s3和s6都指向常量池的对象,相等。

s1==x2?

    String x2 = new String("c") + new String("d");
    x2.intern();
    String x1 = "cd";

常量池一开始没有"cd",x2.intern在常量池中创建了"cd"并返回它的地址,所以x1==x2.

那如果颠倒一下呢?

    String x2 = new String("c") + new String("d");
    String x1 = "cd";
    x2.intern();

执行x2.intern,发现常量池中已经有了"cd",于是啥都不做,所以x1!=x2

但是注意JDK1.7的intern有所不同,再执行s.intern()时,会先拷贝一份s对象,然后放入常量池,返回常量池对象,如果没有也是啥也不干。

所以要是JDK1.7的话,那么下面就是false了,因为x2执行intern,intern返回的值不是给x2,而是拷贝出来的x2,原x2还是堆内存中的String。

    String x2 = new String("c") + new String("d");
    x2.intern();
    String x1 = "cd";

今天学习JVM让我对String的理解真的通透了!

本地内存和直接内存

可以这么表示:JAVA程序内存 = JVM内存+本地内存(元空间+直接内存)

本地内存(NativeMemory):并不属于虚拟机运行时数据区,而是本机的物理内存,只有申请内存超过本机物理内存才会抛出OutOfMemoryError异常。

直接内存(Direct Memory):JDK1.4加入了NIO(new input/output),可以通过存储在java堆里的DirectByteBuffer对象作为这块内存的引用操作,提高性能。

你可能感兴趣的:(jvm,求职招聘,java)