文章目录
学习路线
内存结构----->垃圾回收—>字节码文件---->类加载器---->运行期的及时编译器
程序计数器
作用: 记住下一条令的执行地址;当前线程所执行字节码的行号指示器
特点:
线程私有
: 线程 不会存在内存溢出
由于Java虚拟机的多线程是通过线程轮流切换
并分配处理器执行时间的方式实现的,在任何一个时间,一个处理器(对于多核处理器来说就是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序技术器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为 线程私有
虚拟机栈
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
Java方法执行的内存模型
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
每个方法在执行的同时都会创建一个
栈帧
(Stack Frame)💛 用于存储局部变量表
,操作数栈
,动态连接
,方法出口
等信息。每一个方法从调用执行直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
局部变量表
存放了再编译期可知的各种基本数据类型
(boolean,byte,short,int,long),对象引用
(reference类型,它不等同于对象本身,可能是对象起始地址的指针,也可能是指向一个代表对象句柄或其他与此对象相关的位置)
局部变量表
所需要的内存空间在编译期间分配完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行就期间不会改变局部变量表的大小
反编译之后查看局部变量表
/**
`局部变量表`所需要的内存空间在编译期间分配完成分配,==当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行就期间不会改变局部变量表的大小==
**/
LocalVariableTable: //局部变量表
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
演示栈帧
./**
* 演示栈帧
*/
public class Demo1_1 {
public static void main(String[] args) throws InterruptedException {
method1();
}
private static void method1() {
method2(1, 2);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
/
一些问题
垃圾回收是否牵扯到栈?
答:不需要,垃圾回收只回收堆内存
栈内存越大越好吗?
答:不一定,划分的大通常能进行多次递归调用
方法内局部变量是否为线程安全?
答: 不会。如果方法内局部变量没有逃离方法作用范围则为线程安全,反之。
/**
* 局部变量的线程安全问题
*/
public class Demo1_17 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
//线程安全
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 StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
栈内存溢出
什么情况下会导致占内存溢出
1、栈帧过多;例如递归过多
2、栈帧过大
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k
*/
public class Demo1_2 {
private static int count;
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
private static void method1() {
count++;
method1();
}
}
java.lang.StackOverflowError
at cn.itcast.jvm.t1.stack.Demo1_2.method1(Demo1_2.java:20)
at cn.itcast.jvm.t1.stack.Demo1_2.method1(Demo1_2.java:21)
at cn.itcast.jvm.t1.stack.Demo1_2.method1(Demo1_2.java:21)
本地方法栈
本地方法使用到的内存。
虚拟机栈为虚拟机执行
Java方法
本地方法栈则为虚拟机使用到的
Native
方法
堆
堆定义
Heap堆
通过new关键字,创建的对象都会使用堆内存
此内存区域的唯一目的就是存放对象实例
虚拟机规范中描述:所有对象实例以及数组都要在堆上分配
特点:
它是
线程共享
的,堆中对象都需要考虑线程安全的问题 它由垃圾回收机制,因此很多时候也被称为“GC堆”
堆内存溢出
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m
*/
public class Demo1_5 {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
堆内存诊断
- jps工具:查看当前系统中有哪些java进程
- jmap工具:查看堆内存占用情况
- jconsole工具:图形界面,多功能的和检测工具,可以连续监测(监测线程、cpu)
/**
* 演示堆内存
*/
public class Demo1_4 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}
使用jmap监测
使用jconsole监测
方法区
存储类数据信息;
用于存储已被虚拟机加载的
类信息
,常量
,静态变量
,即时编译后的代码
等数据。特点:
线程共享
g)]
方法区内存溢出
JDK 1.8元空间内存溢出
package cn.itcast.jvm.t1.metaspace;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
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); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
常量池
作用:给指令提供常量符号,以查表的方式找到它
就是一张表,虚拟机指令根据这张常量表找到要执行 的 类名、方法名、参数类型、字面量的信息(基本数据类型、boolean类型)
public class HelloWorld{
//二进制字节码:
//类基本信息、
//常量池、
//类方法定义包含了虚拟机指令
public static void main(String[] args){
System.out.println("hello world");
}
//javap -v HelloWorld.class 反编译
}
使用反编译获取的字节码信息
javap -v HelloWorld.class
Classfile /F:/jvm/src/cn/itcast/jvm/HelloWorld.class
Last modified 2020-1-17; size 567 bytes
MD5 checksum 8efebdac91aa496515fa1c161184e354
Compiled from "HelloWorld.java"
//以上为类的描述信息:类基本信息
public class cn.itcast.jvm.t5.HelloWorld//类的修饰符
SourceFile: "HelloWorld.java" //
minor version: 0 //类的版本
major version: 52 //对应jdk1.8
flags: ACC_PUBLIC, ACC_SUPER
//常量池
//常量池
//常量池
//常量池
//结合下面的
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object.
"<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/Prin
tStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStre
am
#25 = NameAndType #32:#33 // println:(Ljava/la
ng/String;)V
#26 = Utf8 cn/itcast/jvm/t5/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
//
/
//类的方法定义
{
public cn.itcast.jvm.t5.HelloWorld(); //类的构造方法:默认构造方法
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/la
ng/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorl
d;
public static void main(java.lang.String[]); //main方法
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
//从这里开始就是虚拟机的指令
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println(Ljava/lang/String;)V;注意这里的 #号 :需要从常量池中查找 对应的#指代
8: return //方法执行
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
运行时常量池
常量池是.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池;并把里面的符号地址变为真实地址。
运行期间的常量池;这时常量池中的数据仅仅是符号,还没有称为对象,也就是没有称为字符串对象等
字符串池(String Table)
常量池与串池关系:
运行期间的常量池;这时常量池中的数据仅仅是符号,还没有称为对象,也就是没有称为字符串对象等
何时才会成为对象: ldc #2 执行完才会把"a"字符变为"a"字符串对象;==若串池中没有该对象,==会把生成的对象放入串池中 StringTable[“a”]
何时才会成为对象: ldc #3 执行完才会把"b"字符变为"b"字符串对象 : ==若串池中没有该对象,==会把生成的对象放入串池中 StringTable[“a”,“b”]
Demon1:懒惰的常量池
String s1 = "a"; // 懒惰的;用到才会把字符创建为字符串对象,r==若串池中没有该对象,==会把生成的对象放入串池中 StringTable["a"]
String s2 = "b";
String s3 = "ab";
String s4 = new String("ab");//在堆上创建一对象
反编译结果
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a;读取 #2位置的数据
2: astore_1 //将数据存入到 1 s1 变量位置
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 6
line 21: 9
LocalVariableTable: //局部变量表
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
Demon2:变量的拼接原理
String s4 = s1 + s2; //结合下面的反编译,可以看出首先执行 创建一个StringBuilder对象
//new StringBuilder.append("a").append("b").toString() 等价于 创建 newString("ab")
System.out.println(s3==s4);//false;
//分析:s1与s2,s3在串池中;而 s4是new 出来的在堆中
反编译
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
//这里开始才对应String s4 = s1+s2;
9: new #5 // class java/lang/StringBuilder:创建StringBuilder对象
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V:构造方法
16: aload_1 //将s1参数准备好
17: invokevirtual #7 // Method java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; stringBuilder.append(a)
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4 //变量存储在4
29: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 6
line 14: 9
line 21: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
//常量池
//常量池 常量池 常量池
//常量池
Constant pool:
#1 = Methodref #10.#29 // java/lang/Object."<init>":()V
#2 = String #30 // a
#3 = String #31 // b
#4 = String #32 // ab
#5 = Class #33 // java/lang/StringBuilder
#6 = Methodref #5.#29 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilde
r;
#8 = Methodref #5.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #36 // cn/itcast/jvm/t1/stringtable/Demo1_22
#10 = Class #37 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcn/itcast/jvm/t1/stringtable/Demo1_22;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 s1
#23 = Utf8 Ljava/lang/String;
#24 = Utf8 s2
#25 = Utf8 s3
#26 = Utf8 s4
#27 = Utf8 SourceFile
#28 = Utf8 Demo1_22.java
#29 = NameAndType #11:#12 // "<init>":()V
#30 = Utf8 a
#31 = Utf8 b
#32 = Utf8 ab
#33 = Utf8 java/lang/StringBuilder
#34 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #40:#41 // toString:()Ljava/lang/String;
#36 = Utf8 cn/itcast/jvm/t1/stringtable/Demo1_22
#37 = Utf8 java/lang/Object
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 toString
#41 = Utf8 ()Ljava/lang/String;
Demon03:常量拼接原理
String s5 = "a" + "b"; // javac 在编译期间的优化,已经在编译期确定为ab
字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab 从常量池中取 ab
8: astore_3
9: ldc #4 // String ab 从常量池中取ab
11: astore 4 //存储到 s3 s5 的变量都是串池中的值
13: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 6
line 15: 9
line 21: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
3 11 1 s1 Ljava/lang/String;
6 8 2 s2 Ljava/lang/String;
9 5 3 s3 Ljava/lang/String;
13 1 4 s5 Ljava/lang/String;
Demon04:itern方法
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == x); //true 同时引用字符串常量
System.out.println( s == x );//false x为字符串常量 ,s为对象:
}
反编译
stack=4, locals=4, args_size=1
0: ldc #2 // String ab /
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: new #5 // class java/lang/String
13: dup
14: ldc #6 // String a
16: invokespecial #7 // Method java/lang/String."<init>":(Ljava/lang/String;)V
19: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: new #5 // class java/lang/String
25: dup
26: ldc #9 // String b
28: invokespecial #7 // Method java/lang/String."<init>":(Ljava/lang/String;)V
31: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
37: astore_2
38: aload_2
39: invokevirtual #11 // Method java/lang/String.intern:()Ljava/lang/String;
42: astore_3
43: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_3
47: aload_1
48: if_acmpne 55
51: iconst_1
52: goto 56
55: iconst_0
56: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
59: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
62: aload_2
63: aload_1
64: if_acmpne 71
67: iconst_1
68: goto 72
71: iconst_0
72: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
75: return
一道面试题
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b"; //变量拼接 "ab";
String s4 = s1+s2; //变量拼接 使用到StringBuilder生成一个新的String对象 :数据存储在堆中
String s5 = "ab"; //串池中数据
String s6 = s4.intern(); //串池中数据 ab
print(s3==s4) //false
print(s3==s5) //true
print(s3==s6) //true
//
/
String x2 = new String("c")+new String("d");//堆中对象 new String("cd")
String x1 = "cd"; //常量池中对象
x2.intern();
x1==x2 //false
//将上卖弄的x2.intern()与String x1 = "cd";交换顺序
// 则结果为true
小结:
-
常量池中的字符串仅是符号,第一用到才变为对象
-
利用串池机制,来避免重复创建字符串对象(串池中对象只有一份)
-
字符串变量的拼接原理是StringBuilder
-
字符串常量的拼接原理是编译期优化
-
可以使用intern方法,主动将串池中还没有的字符串对象放入串池
5.1、1.8将字符串对象尝试放入串池,如果有则并不会放入,如果没有则会放入串池;会把串池对象返回
2.2、1.6将字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份放入串池,会把串池中的对象返回;会把串池中对象返回
String Table的位置
在Jdk 1.6中 字符串池中的数据存储在 永久区中
在Jdk 1.8中 字符串池中的数据存储在 堆中
### 测试常量池内存溢出
/**
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
* 在jdk6下设置 -XX:MaxPermSize=10m
*/
public class Demo1_6 {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());//存入字符串;intern
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}