摘要
- 自从java面世以来,声势浩大,提出“Write Once,Run Anywhere";Java相比于其他C/C++语言的优势:在JVM内存管理之下,不再需要为每一个new操作去手动分配内存和free/delete的内存释放;不容易出现内存泄漏和内存溢出等问题。
- 本节主要讲解Java运行时数据区:线程共享数据区:方法区、堆 线程隔离数据去:虚拟机栈、本地方法栈、程序计数器
思维导图
内容
1、运行时数据区包含哪几部分?
java虚拟机运行时数据区主要包含以下几个模块?
- 线程共享数据区:方法区、堆
- 线程隔离数据区:虚拟机栈、本地⽅法栈、堆、程序计数器
程序计数器:用来记录字节指令的行号;我们将.java文件编译成.class文件后,交由JVM去执行的时候,程序一行一行执行就是交给程序计数器去做的
Java虚拟机栈:比如我们写一个方法,JVM执行这个方法的时候,类似于创建了一个栈针;入栈到出栈就是这个方法调用的整个过程;对应的就是一个方法一个栈。
本地方法栈:就是JVM虚拟机执行一些本地方法库;我们在进行一个CAS操作的时候:通过unsafe的compareAndSwapInt调到本地方法库里面的native方法。那么这些native方法就是在本地方法栈里面运行的。
方法区:存放类信息,类变量,静态变量。
堆:几乎所有数组跟对象的创建都是在堆里面。
2、程序计数器
是什么?
程序计数器(Program Counter Register)是一块较小的内存空间,是当前线程所执行的字节码的行号指示器。
为什么?
- 字节码解释器工作时候通过改变程序计数器的值来选取下一条要执行的字节码指令;线程的各个基础功能都需要依赖这个计数器来完成。
- java虚拟机多线程是通过线程轮流切换并分配处理器执行的时间实现,在任意一个确定的时刻,一个处理器都只会执行一条线程指令,因此为了线程切换后能恢复到正确的执行位置。每个线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。我们称这类内存区域为“线程私有”的内存。
特点?
内存区域中唯⼀⼀个在Java虚拟机规范中没有规定任OutOfMemoryError 情况的区域。因为程序计数器本身不需要我们程序员去操作,所以不会出现OOM。
实战演练
我们创建一个Person类;拥有属性age,提供getter/setter方法。
public class Person {
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private int age;
}
我们使用javac进行编译源代码为字节码,然后使用javap查看字节码,如下:
我们通过javap -l可以看到字节码的内容;我们看到里面有两个方法:setAge;getAge;然后我们看到getAge方法在第5行。setAge在8,9行。现在如果我们需要执行Person里面的getAge方法;我们说到,这个时候我们可能多线程来执行这个方法;所以这块运行时数据库是一块独立的内存;我们知道程序计数器是一块线程隔离的数据库,所以每块线程有自己独立的程序计数器。这块内存是在我们的线程里面单独隔离开来的,不同的线程维护了自己不同的程序计数器。
3、java虚拟机栈
上一节我们讲解了程序计数器;程序计数器是线程私有的一块小内存,线程私有的内存除了程序计数器之外还有两块:java虚拟机栈,本地方法栈。方法区跟堆相对的就是线程共享的内存。
是什么?
Java方法执行的一块内存区域;随着线程方法执行时候压栈出栈,此内存区域也会销毁,他是跟线程的生命周期相同的。
为什么?
方法的执行是按照栈数据结构;每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存放局部变量表、操作数栈、动态链接、⽅法出⼝等信息。每⼀个⽅法从调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在虚拟机栈中⼊栈到出栈的过程。
特点?
- 局部变量表存放了编译期可知(指代我们这些基础数据类型他所对应的数据的大小,大小是可知道的)的各种基本数据类型(boolean、byte、char、short、int、 float、long、double)以及对象引⽤(reference 类型)
- 如果线程请求的栈深度⼤于虚拟机所允许的深度,将抛出 StackOverflowError异常。
实战
public class StackDemo {
public static void a(){
System.out.println("method a executed");
}
public static void b(){
//a();
b();
System.out.println("method b executed");
}
public static void main(String[] args){
b();
System.out.println("method main executed");
}
}
我们修改b()的调用为自己。上面进行了递归调用:b()方法执行不断入栈操作,而没有出栈,导致所分配的栈内存不够,从而出现栈内存溢出。栈长度超过制定长度大小。结果如下:
4、本地⽅法栈
java运行时数据区里面的java虚拟机栈的讲解;JVM运行时数据区里面java虚拟机栈、本地方法栈、程序计数器这3快的内存是线程私有的。他的生命周期跟线程的生命周期是一样的。java虚拟机栈跟本地方法栈是很相似的。他们的区别无非就是:java虚拟机栈他执行的是java方法,他会被编译成字节码。本地方法栈执行的是native方法。那么什么是native方法?native是一个修饰符。native方法比较特殊,他不允许有方法体。我们native方法执行的时候,他会调用CAS(CAS是cpu的原子指令)。
是什么?
native方法执行的一块内存区域。
为什么?
我们java程序需要调用native方法从而调用cpu指令。满足CAS原子性操作
特点?
- 与java虚拟机栈一样也会抛出Stackoverflow.
- java虚拟机栈跟本地方法栈差不多,Hotshot将Java虚拟机栈和本地⽅法栈合⼆为⼀;我们上面讲解的java运行时数据区是jvm标准,而Hotshot只是jvm虚拟机的实现。
我们通过java -version可以查看使用的JVM类型。
实战
本地方法实战,在我们juc里面会有一些原子类:Atomic相关类,比如:以AtomicInteger为例子。
我们可以看到上面demo;原子类进行CAS操作时候会调用底层native方法。
native方法就是调用java语言之外的语言,比如c语言/c++语言;我们调用其他语言的话,我们将这个方法用关键字:native来修饰。java里面原子类都是基于native方法调用cpu指令的。
5、java堆
上一节我们讲解了java运行时数据区里面本地方法栈的讲解;我们讲解了java虚拟机栈、本地方法栈、程序计数器的讲解,这三块是java运行时候数据区里面的线程独占区内存。那么除了线程独占区内存之外。我们图示左边是线程共享区:方法区跟堆。我们先讲解java运行时数据区里面的线程共享区:java堆。
是什么?
Java中⽤来存放对象实例的最大一块内存区域,【⼏乎所有的对象实例都在这⾥分配内
存】
为什么?
- 内存区域的唯⼀⽬的就是存放对象实例。
- Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最⼤的⼀块;Java 堆是被所有线程共享的⼀块内存区域。
特点?
- Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage Cash)。
- 通过:-Xmx -Xms。(最大堆内存、最小堆内存)。
- Java堆可以分成新⽣代和⽼年代 新⽣代可分为To Survivor、From Survivor、Eden。
实战
我们运行一个SpringBoot项目;通过ps或者jps查看其对应的pid。
然后通过指令:jmap -heap 13027;
Heap Configuration:
//对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)
MinHeapFreeRatio = 0
//对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
MaxHeapFreeRatio = 100
//对应jvm启动参数-XX:MaxHeapSize=设置JVM堆的最大大小
MaxHeapSize = 16848519168 (16068.0MB)
//对应jvm启动参数-XX:NewSize=设置JVM堆的‘新生代’的默认大小
NewSize = 351272960 (335.0MB)
//对应jvm启动参数-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小
MaxNewSize = 5616173056 (5356.0MB)
//对应jvm启动参数-XX:OldSize=<value>:设置JVM堆的‘老生代’的大小
OldSize = 703594496 (671.0MB)
//对应jvm启动参数-XX:NewRatio=:‘新生代’和‘老生代’的大小比率
NewRatio = 2
//对应jvm启动参数-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage://堆内存分步
PS Young Generation
Eden Space://Eden区内存分布
capacity = 117964800 (112.5MB) //Eden区总容量
used = 27181312 (25.922119140625MB) //Eden区已使用
free = 90783488 (86.577880859375MB) //Eden区剩余容量
23.041883680555557% used //Eden区使用比率
From Space: //其中一个Survivor区的内存分布
capacity = 1572864 (1.5MB)
used = 950272 (0.90625MB)
free = 622592 (0.59375MB)
60.416666666666664% used
To Space://另一个Survivor区的内存分布
capacity = 1572864 (1.5MB)
used = 0 (0.0MB)
free = 1572864 (1.5MB)
0.0% used
PS Old Generation //当前的Old区内存分布
capacity = 1331167232 (1269.5MB)
used = 115252040 (109.91291046142578MB)
free = 1215915192 (1159.5870895385742MB)
8.657968527879147% used
61594 interned Strings occupying 6891128 bytes.
6、⽅法区
上一节我们讲解了java运行时数据区块里面线程私有的:java虚拟机栈、本地方法栈、程序计数器;以及线程间共享的数据区:java堆。这一节我们讲解线程共享的数据区方法区:
是什么?
线程共享的用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的内存区域。
为什么?
为了存放长久存在的极少被垃圾回收的常量。Hotspot使⽤永久代来实现⽅法区 JRockit、IBM J9VM Java堆⼀样为了管理这部分内存。
特点?
- 并⾮数据进⼊了⽅法区就如永久代的名字⼀样“永久”存在了。这区域的内存回收⽬标主要是针对常量池的回收和对数据类型的卸载。---所以我们把它叫做部分永久。
- ⽅法区也会抛出OutofMemoryError,当它⽆法满⾜内存分配需求时(当我们的方法区无法满足内存分配时候),虽然我们的方法区的内存没有堆内存那么大,但是当我们的方法去里面的类信息比较庞大时候就会出现OOM。
7、常量池
是什么?
运⾏时常量池是⽅法区的⼀部分,Class⽂件除了有类的版本、字段、⽅法、接⼝等描述信息外,还有⼀项信息是常量池,⽤于存放编译器⽣成的各种字⾯量和符号引⽤,这部分内容将在类被加载后进⼊⽅法区的运⾏时常量池中存放。
什么是类信息:类版本号、⽅法、接⼝。
我们打开class文件时候:我们可以看到开头是cafe babe(因为我们的java的图标是一杯咖啡),里面就是一些类版本号、⽅法、接⼝信息。
为什么?
用户存放共有的常量数据。
特点?
运⾏时常量池是⽅法区的⼀部分,受到⽅法区内存的限制,当常量池再申请到内存时会抛出OutOfMemoryError异常。
实例
我们通过代码发现:a跟b是相等的,如果说a跟b是存在在堆内存,那么将会创建不同的对象,开辟不同的空间,那么这个时候a跟b肯定是不相等的。所以我们知道a跟b是存放在方法区。并且通过源码发现String类型的都是常量,常量是存放在方法区的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。