目录
一.引言
1.什么是JVM
2.学习路线
二.内存结构
1.程序计数器
1.1定义
1.2作用
2.虚拟机栈
2.1定义
2.2栈内存溢出(StackOverflowError)
2.3线程运行诊断
3.本地方法栈
4.堆
4.1定义
4.2堆内存溢出(OutOfMemoryError)
4.3堆内存诊断
5.方法区
5.1定义
5.2组成
5.3方法区内存溢出
5.4运行时常量池
一.引言
1.什么是JVM
JVM:java virtual machine(java虚拟机),Java程序的运行环境,是二进制字节码运行环境
好处:
一次编写,到处运行自动内存管理,垃圾回收功能数组下标越界检查使用虚方法调用的机制实现多态
比较:jvm jre jdk
基础类库:集合类,线程类,IO类等
编译工具:javac,javap
应用服务器工具:tomcat
2.学习路线
二.内存结构
1.程序计数器
1.1定义
1.1定义
1.2作用
java源代码经过编译后成为二进制字节码的jvm指令jvm指令经过解释器成为机器码机器码可以被cpu执行
而程序计数器就会记住下一条需要执行的的jvm指令的序号
特点:
线程私有的(每个线程都会有自己的程序计数器)不会存在内存溢出问题
2.虚拟机栈
2.1定义
栈:线程运行的内存空间
栈帧:每个方法运行时需要的内存(存放参数,局部变量,返回地址)
每个线程运行时所需的内存,成为虚拟机栈每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧,对应着当前正在执行的方法。
问题辨析:
垃圾回收是否涉及栈内存?
栈帧内存在每次方法执行完毕后都会弹出,垃圾回收是回收的堆内存。
栈内存分配越大越好吗?
不是,在物理内存一定的情况下,栈内存越大,可以运行的线程数量越少,从而会影响程序的运行速度。栈内存大小只决定连续调用方法的数量。例如:递归调用可能造成栈溢出问题。
方法内的局部变量是否线程安全?
如果方法内部的局部变量没有逃离方法的作用范围,他是线程安全的。如果局部变量引用了对象(传进来的参数),并逃离了(return出)方法的作用范围,则需要考虑线程安全。(因为无法考虑方法之外,变量是否被其他线程调用)static修饰的变量不是放在栈中的,是线程共享的,是线程不安全的。
2.2栈内存溢出(StackOverflowError)
造成原因:
栈帧过多导致栈内存溢出(错误的递归调用)栈帧过大导致栈内存溢出(一个方法中的局部变量过多,出现几率小)
2.3线程运行诊断
案例1:cpu占用过多
定位步骤:
用top命令定位哪个进程对cpu占用过高ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)。pid,tid,%cpu是选择查看的列。命令jstack 进程id。 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
执行top命令:
jstack 进程id命令
注:nid是pid的16进制转换
案例2:程序运行很长时间没有结果
定位同上,执行jstack 进程id命令。
查询结果是线程死锁。
3.本地方法栈
给本地方法运行提供内存空间。
本地方法:用c或c++写的系统本地的方法(native修饰的),例如:object类中的clone(),hashCode(),notify()等
4.堆
4.1定义
通过new关键字创建的对象都会使用堆内存。
特点:
是线程共享的,堆中的对象都需要考虑线程安全问题。有垃圾回收机制
4.2堆内存溢出(OutOfMemoryError)
代码演示:
package com.erp.payroll.test.VO;
import java.util.ArrayList;
import java.util.List;
public class JvmDemo {
public static void main(String[] args) {
int i = 0;
try {
List list = new ArrayList<>();
String s = "haha";
while (true) {
list.add(s);
s = s + s;
i++;
}
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println(i);
}
}
}
java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3332) at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448) at java.lang.StringBuilder.append(StringBuilder.java:136) at com.erp.payroll.test.VO.JvmDemo.main(JvmDemo.java:20) 26
堆空间内存也可以通过-Xmx 设置 ,例如:设置8m -Xmx8m。
4.3堆内存诊断
工具:
1.jps工具
查看当前系统中有哪些java进程
2.jmap工具(某一时刻)
查看堆内存占用情况
3.jconsole工具
图形界面的,多功能的检测工具,可以连续监测
演示:当执行1时候,堆中没有过多的内存被占用,但是在创建bytes后,就会增加10mb,在执行了2之后,会调用gc清理,堆内存占用又会减少。
package com.erp.payroll.test.VO;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class JvmDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("1=============");
TimeUnit.SECONDS.sleep(30);
byte[] bytes = new byte[1024 * 1024 * 10];//10mb
System.out.println("2=============");
TimeUnit.SECONDS.sleep(30);
bytes = null;
System.gc();
System.out.println("3=============");
TimeUnit.SECONDS.sleep(50);
}
}
执行命令
F:***erp-payroll>jps
得到
8844 JvmDemo
打印1=============之后
执行
F:***erp-payroll>jmap -heap 8844
堆内存使用:18187392
Heap Usage: PS Young Generation Eden Space: capacity = 58720256 (56.0MB) used = 18187392 (17.3448486328125MB) free = 40532864 (38.6551513671875MB) 30.972943987165177% used
打印2=============之后,执行
Heap Usage: PS Young Generation Eden Space: capacity = 58720256 (56.0MB) used = 28673168 (27.344863891601562MB) free = 30047088 (28.655136108398438MB) 48.83011409214565% used
打印3=============之后,执行
Heap Usage: PS Young Generation Eden Space: capacity = 58720256 (56.0MB) used = 2348848 (2.2400360107421875MB) free = 56371408 (53.75996398925781MB) 4.000064304896763% used
jconsole工具使用
启动main方法,执行命令
F:***erp-payroll>jconsole
弹出视图工具
动态查看堆内存使用情况
案例:
垃圾回收后,内存占用仍然很高(详情见视频)
通过jsp可以看出新生代堆内存清理后,老年代还在被占用(对象被引用,无法释放)
另一个视图工具
执行命令:(高版本jdk没有内置,需自主安装)
F:****erp-payroll>jvisualvm
5.方法区
5.1定义
方法区是所有线程共享的区域。存储了跟类的结构相关的信息,有成员变量,方法数据,成员方法,构造方法,运行时常量池等
方法区是在类启动时被创建。
5.2组成
不同版本的jdk内存结构有所不同
1.6中方法区占用了堆内存;字符串常量池放在了方法中的运行时常量池中
1.8之后方法区放在了本地内存中;字符串常量池放进了堆中。
5.3方法区内存溢出
1.8之后方法区放在本地内存中,所以内存溢出不太容易出现,可以把相关参数设置较小再进行测试。
元空间参数设置 -XX:MaxmetaspaceSize=8m 永生代参数设置 -XX:MaxPermSize=8m
并且两种版本号内存溢出报错也有所不同
元空间内存溢出:OutOfMemoryError:metaspace 永生代内存溢出:OutOfMemoryError:PermGen space
package com.erp.payroll.test.VO;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class JvmDemo extends ClassLoader {// 可以用来加载类的二进制字节码
public static void main(String[] args) throws InterruptedException {
int j = 0;
try {
JvmDemo test = new JvmDemo();
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);
}
}
}
5.4运行时常量池
演示查看字节码详情中的常量池:
代码:
package com.erp.payroll.test.VO;
//二进制字节码(类的基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world!");
}
}
操作指令:
1.先cd到文件的上一级目录
2.使用javac编译出字节码
***testVO>javac HelloWorld.java
3.使用javap查看字节码详情
****testVO>javap -v HelloWorld.class
二进制字节码详情:(类的基本信息,常量池,类方法定义,包含了虚拟机指令)
类的基本信息:
Last modified 2022-2-18; size 450 bytes MD5 checksum 284a6f82e2f72e0f5adafd276b197cef Compiled from "HelloWorld.java" public class com.erp.payroll.test.VO.HelloWorld minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER
运行时常量池:
Constant pool: #1 = Methodref #6.#15 // java/lang/Object."":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // hello world! #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // com/erp/payroll/test/VO/HelloWorld #6 = Class #22 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 HelloWorld.java #15 = NameAndType #7:#8 // " ":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 hello world! #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 com/erp/payroll/test/VO/HelloWorld #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V
类的方法定义:包含了虚拟机指令
{
public com.erp.payroll.test.VO.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 9: 0
public static void main(java.lang.String[]);
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 11: 0
line 12: 8
}
SourceFile: "HelloWorld.java"
虚拟机指令:
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
常量池作用:
例如行号3:对应的#3,表示进入运行时常量池中的#3进行翻译
运行时常量池中的#3对应的#18,继续翻译
#18对应 Utf8 hello world!
表示对应的是字符串形式的 hello world!
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量信息。
运行时常量池,常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
5.5StringTable面试题:
package com.erp.payroll.test.VO;
import java.sql.SQLOutput;
public class StringDemo {
public static void main(String[] args) {
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 x1 = new String("c") + new String("d");
String x2 = "cd";
x1.intern();
//问
System.out.println(x1 == x2);
//调换最后两行代码位置呢,如果是jdk1.6呢
String x3 = new String("c") + new String("d");
x3.intern();
String x4 = "cd";
System.out.println(x1 == x2);
}
}
常量池和串池的关系:
public class StringDemo2 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
查看二进制字节码:
Last modified 2022-2-18; size 334 bytes MD5 checksum 7b08065901c43270c4a91a4d85f35540 Compiled from "StringDemo2.java" public class com.erp.payroll.test.VO.StringDemo2 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#15 // java/lang/Object."":()V #2 = String #16 // a #3 = String #17 // b #4 = String #18 // ab #5 = Class #19 // com/erp/payroll/test/VO/StringDemo2 #6 = Class #20 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 StringDemo2.java #15 = NameAndType #7:#8 // " ":()V #16 = Utf8 a #17 = Utf8 b #18 = Utf8 ab #19 = Utf8 com/erp/payroll/test/VO/StringDemo2 #20 = Utf8 java/lang/Object { public com.erp.payroll.test.VO.StringDemo2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: return LineNumberTable: line 9: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=4, 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 9: return LineNumberTable: line 11: 0 line 12: 3 line 13: 6 line 14: 9 } SourceFile: "StringDemo2.java"
常量池中的信息都会被加载到运行时常量池中(这时a,b,ab都是常量池中的符号,还没变为java字符串对象)。
当执行到引用的代码上才会成为java字符串对象,
String s1 = "a";
0: ldc #2 // String a
#2 = String #16 // a #16 = Utf8 a
ldc #2会把a符号变成“a”字符串对象,在这个过程中会先准备串池StringTable[ ]的空间,并先以a为key查询串池中是否存在(不存在则放入串池)。 StringTable[ ]是hashTable结构
s4执行时做了什么:
String s4 = s1 + s2;
二进制字节码:
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBu
ilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBu
ilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
在这个过程中它做了这些事情:new StringBuilder( ).append("a").append("b").toString( )
但是查看toString()源码会发现,它是new了一个新的String(new String(“ab”))。
所以s4指向了一个堆内的地址。s3不等于s4。
s5:
String s5 = "a" + "b";
29: ldc #4 // String ab
31: astore 5
它是直接拿到了字符串ab。与String s3 = “ab”相同 (是在编译期确定的为ab的)
5.6StringTable特性常量池中的字符串仅是符号,第一次用到时才会变为对象利用串池的机制,来避免重复创建创建字符串对象字符串变量拼接的原理是StringBuilder(1.8之后),存在于堆中字符串常量拼接的原理是编译期优化,存在于串池中可以使用intern方法,主动将串池中还没有的字符串对象放入串池。
- 1.8 版本之后将字符串对象放到串池中。如果串池中有,则不会放入,如果没有,则会放入,并将串池中的对象返回 。 1.6版本将字符串对象放到串池中。如果串池中有,则不会放入,如果没有,则会把此对象复制一份放入(浅克隆地址不同),并将串池中的对象返回 。
案例1:
String s = new String("a") + new String("b");
动态的拼接是存在于堆中的,相当于new了一个“ab”
String s1 = s.intern();
此方法会将s(“ab”)放到串池中。如果串池中有“ab”,则不会放入,如果没有,则会放入(s的地址指向串池中“ab”),并将串池中的对象返回 。s1一定是串池中的对象。
结论:
System.out.println(s=="ab");//true
System.out.println(s1=="ab");//true
但是如果没有 String s1 = s.intern(); 则s==“ab”为false(ab是串池中的对象,s是堆中的对象)。
案例2:
String x = "ab";
String s = new String("a") + new String("b");
String s1 = s.intern();
System.out.println(s=="ab");
System.out.println(s1=="ab");
此代码与案例1代码不同之处在于在执行到x=“ab”时就已经将ab放进了串池中,所以在执行到s1时候将s(在堆中)放进串池,发现串池中已经存在(则没有放进串池,地址就没办法指向串池),则s还是堆中的ab。s1还是指向的串池中的ab。
回看面试题:
package com.erp.payroll.test.VO;
import java.sql.SQLOutput;
public class StringDemo {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";//常量拼接编译期就优化直接指向串池
String s4 = s1 + s2;//动态拼接相当于在堆中new了一个ab
String s5 = "ab";//常量池
String s6 = s4.intern();//如果串池中没有则将s4放入串池。有则不放入。s6一定是串池中对象
//问
System.out.println(s3 == s4);//false,s4在堆中
System.out.println(s3 == s5);//true,都在串池中
System.out.println(s3 == s6);//true,都在串池中
String x1 = new String("c") + new String("d");
String x2 = "cd";
x1.intern();
//问
System.out.println(x1 == x2);//false,x1在堆中,x2在串池中
//调换最后两行代码位置呢,如果是jdk1.6呢
String x3 = new String("e") + new String("f");
x3.intern();//1.8将x3放入串池,如果没有则放入(堆与串池同地址),有则不放(与串池对象不同)
//1.6将x3浅复制放进串池,x3与串池对象一定不同。
String x4 = "ef";
System.out.println(x3 == x4);//true
}
}
5.7StringTable位置
5.8StringTable垃圾回收 5.9StringTable性能调优



