1.程序计数器
全称:Program Counter Register(物理上是寄存器实现的,读取速度非常快)
作用
要了解程序计数器,可以看上图的代码。
右侧就是普通的java源代码,左侧的就是编译后的JVM指令(二进制字节码)。可以看到JVM每条指令左边都有一个数字,你可以理解为每条JVM指令对应的内存地址,根据内存地址就可以找到对应的指令去执行它。
执行每条JVM指令的同时,它也会把下一条JVM指令的内存地址存入程序计数器。当指令执行完后,解释器就会到程序计数器找到下一条指令的内存地址,去执行它。
综上,作用就是记住下一条指令的执行地址。
特点
- 是线程私有的
每一个线程都有自己的程序计数器
- 不会存在内存溢出
2.虚拟机栈
定义
从图中可以看出:
虚拟机栈就是线程运行时需要的内存空间。如果有多个线程的话,就会有多个虚拟机栈。
一个栈内又由多个栈帧(frame)组成。这里的栈帧是什么意思呢,其实,一个栈帧就对应着一次方法的调用。也就是说,栈帧就是每个方法运行时需要的内存。
每个线程只能有一个活动栈帧。它对应着当前正在执行的那个方法。
思考
1.垃圾回收是否涉及栈内存呢?
答:不涉及,因为虚拟机栈就是一次次的方法调用组成的,当chuchan方法执行结束后,栈帧内存 会随着出栈自动释放掉。
2.栈内存是否分配的越大越好?
答:不是的。栈内存分配的越大,反而会让线程的执行数变少。因为物理内存的大小是一定的。
3.方法内的局部变量是否是线程安全的?
答:先看下面的代码:
public class Demo01 {
static void m1() {
int x =0;
for(int i = 0; i < 5000; i++){
x++;
}
System.out.println(x);
}
}
有一个局部变量x,方法m1对x变量自增5000次。
如果这时有多个线程执行此方法。会产生线程安全问题吗?
答案是不会。
从图中可以看出,当两个线程都执行此方法时,分别会创建两个局部变量的栈帧入栈。相当于每个线程都有自己私有的变量x,互不干扰。(局部变量是线程私有的)
再来深入分析一下,看下面这段代码:
public class Demo02{
public static void main(String[] args) {
}
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
在m1方法里,sb是方法里的局部变量,是线程私有的,所以是线程安全的。
在m2方法里,sb作为了一个参数传递进来,这就意味着有其他的线程能访问到它,就不是线程私有的了,是共享的了,就会存在线程安全问题。可以用StringBuffer解决这个问题。
在m3方法里,sb对象作为一个返回值返回了,这意味着其他线程也能获取到这个对象并并发地修改它。所以也存在线程安全问题。
综上所述,要判断一个局部变量是否是线程安全的,就要看它有没有逃离该方法的作用范围。
栈内存溢出
java.lang.StackOverFlowError
什么情况会导致内存溢出呢?
本地方法就是值那些不是由Java编写的代码。有时候需要借助C语言等和操作系统的底层打交道。
4.堆
定义
特点
堆内存诊断
1.jps工具
- 查看当前系统中有哪些java进程
2.jmap工具
- 查看堆内存占用情况(命令行语句:jmap -heap 进程id)
3.jconsole工具
- 图形界面的多功能检测工具,可以连续检测
4.jvisualvm工具
- 可视化的虚拟机
5.方法区
定义
方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。比如成员变量、方法数据、成员方法以及构造器方法的代码部分。
综上,方法区存储的就是跟类相关的一些信息。
方法区是在java虚拟机启动时创建的。尽管逻辑上方法区是堆的一部分,但在实现上不同的虚拟机有各自的实现方式。
在jdk1.8以前,方法区的实现方式是永久代,在1.8以后,就把永久代移除了,换了个实现方式叫元空间。元空间用的就是本地内存,也就是操作系统的内存。
具体结构看下图:
这里的方法区只是一个概念,实现的方式是永久代。
1.8后,方法区的实现方式变成了元空间,元空间是由本地内存管理了,不在堆内存了。StringTable移到了堆内存中了。
方法区内存溢出
方法区如果内存不足了,也会抛出一个OutOfMemory的异常。
1.8以前会导致永久代内存溢出,1.8以后会导致元空间内存溢出
/*
* 演示元空间内存溢出
* -XX:MaxMetaSpaceSize=8m
*/
public class Demo03 extends ClassLoader{ //可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo03 test = new Demo03();
for(int i = 0; i < 10000; i++, j++){
//ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号, 修饰符public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//返回byte[]
byte[] code = cw.toByteArray();
//执行了类的加载
test.defineClass("Class" + i, code, 0, code.length);
}
} finally{
System.out.println(j);
}
}
}
看上图的代码,如果我们把虚拟机参数云空间最大容量改为8M。就会抛出内存溢出异常。
1.8以前,会显示永久代内存溢出:java.lang.OutOfMemory: Permgen space
运行时常量池
先来聊聊什么叫常量池?
常量池其实就是一张表,虚拟机指令会根据这张表来找到要执行的类名、方法名、参数类型、字面量(比如字符串之类的)等信息。
我们可以拿经典代码hello world为例子来学习常量池。如果我们要运行这个程序,肯定首先要把它编译成二进制字节码。
二进制字节码由以下几部分组成:
- 类的基本信息
- 常量池
- 类中的方法定义(类的方法中就包含了虚拟机指令)
首先我们要对二进制字节码文件进行反编译:javap -v HelloWorld.class
这几行就是方法中的虚拟机指令。比如第一行获取静态变量,那么我们要怎么获取呢?这时候我们就要根据后边的#7去常量池查表。
查到#7是fieldref——引用了成员变量。指向#8和#9,那么就继续找。#8是类名,我们要找哪个类的成员变量呢?继续找#10。接下来都是如此。。。
运行时常量池是方法区的一部分,是java虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中。
常量池是*.class文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池。并把里面的符号地址变为真实地址。
StringTable
中文名就是我们俗称的串池(字符串常量池),是运行时常量池的重要组成部分。它存在于堆中(JDK1.7之后)。
StringTable存在着hash表的特性,不存在两个相同的字符串,也不能扩容。
先看下面这个例子:
public class Demo04 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
常量池中的信息,都会被加载到运行时常量池中。这时a、b和ab都是常量池中的符号,还没有变为java字符串对象。
创建java字符串对象的时候,就会把该对象放入StringTable中。
看下面这道经典的面试题,搞懂了这道面试题,也就搞懂了StringTable。
public static void main(String[] args) {
String str1 = "abc";
String str2 = "ab" + "c";
String str3 = new String("ab") + "c";
String str4 = str3.intern();
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str1 == str4);
}
分别分析3个输出结果:
- 第一个输出为true。原因是JVM存在编译器优化的机制。在编译期(javac .java时)会将可以拼接的*字符串常量帮你自动拼接了,此时由于StringTable中已经存在了”abc”这个字符串对象,因此会让str2指向一个与str1相同的那块地址,因此为true。
- 第二个输出为false。由于使用了new String( ),运行过程中会在堆中重新开辟一个空间存储,与之前的常量字符串没啥关系,因此为false。
- 第三个输出为true,原因调用intern( )方法时会把在StringTable中查找是否存在与其值相等的(并不是地址相等)的字符串,发现里面恰好存在,因此返回该存在的引用,StringTable中的就是str1,因此为true。
6.直接内存
全称:Direct Memory定义
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。
- 常见于NIO操作时,用作数据缓冲区。
- 分配回收成本较高,但是读写性能较好。
- 不受JVM内存回收管理。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式。
基本使用
在文件读写过程中,java首先要调用操作系统内部的函数,它自己本身是不具备磁盘读写能力。首先辉仔啊系统内存中划出一块系统缓存区,但是在系统缓存区java代码是不能运行的,所以还要在堆里再划出一块java缓冲区。简洁读写磁盘文件。但是这里有两块缓冲区,从而会造成磁盘读写的效率不是很高。
在这张图里,也会在系统内存划出一块缓冲区,也就是我们讲的直接内存。但与上图不同的是,java代码也可以直接访问这块内存。所以直接内存是系统和java可以共享的一块区域。
内存溢出
直接内存也会存在内存溢出。异常是 java.lang.OutOfMemory
: Direct buffer memory