第一天系统学习JVM!今天学了JVM是什么,学习JVM的作用,运行时的数据区域(重点),内存溢出。明天学GC。
运行时数据区域
整体认识
先写一下每个线程私有的三个数据区,分别是程序计数器,虚拟机栈,本地方法栈。
然后再写一下堆和方法区(概念,1.7的实现是永久代,1.8的实现是元空间)
程序计数器
作用:
1、记住下一条jvm指令的执行地址,一个线程的运行就是在它的程序计数器的变化下推动的。
2、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
3、多线程环境,线程来回切换时,线程自身的程序计数器能记住线程执行指令的位置。
特点:
1、是一块很小的内存空间,运行速度最快的存储区域。
2、线程私有,每个线程都有自己的程序计数器。
3、唯一的JVM规范中没有规定OutOfMemoryError的区域,因为它存储的是地址,占用内存小,几乎可以忽略不计。
虚拟机栈
概念
每个线程的创建的时候都会创建一个虚拟机栈,线程私有的,其实就是线程运行时需要的内存。
每个栈内由栈帧(Stack Frame)组成,实际上就是一个个java的方法,每个线程只能有一个活动栈帧(当前栈帧),就是只能执行当前一个方法。
内存溢出(爆栈)
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堆中的字符串常量池,用来保存字符串对象。
看一下test类反编译后的class文件,可以看到编译器给它加了个无参构造,并且合并了"a"+"b"
在终端执行java -v *.class命令,可以反编译,查看详细信息。
这个Constant Pool就是我们的常量池,#1等是每个字面量的地址
往下看可以看到JVM指令:
ldc命令判断常量池是否保存了对应字符串对象的引用,保存了就返回,没保存就在堆中创建字符串对象,并且将该字符串对象的引用保存到常量池中。
常量池中的信息会被加载到运行时常量池,但是一开始这些字符只是常量池的符号而已,还不是真正的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指令。
这里就能清晰地看出,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对象作为这块内存的引用操作,提高性能。