什么是JVM?
JVM简图
内存结构
1. 程序计数器
2. 虚拟机栈
3. 本地方法栈
4. 堆
5. 方法区
①程序计数器
Program Counter Register 程序计数器(寄存器)
- 作用,是记住下一条jvm指令的执行地址;当多个线程执行代码时,需要获取cpu的执行时间片才可以执行,程序计数器作用就是记住下一次从哪里开始执行的位置。
- 特点
- 是线程私有的
- 唯一一个不会存在内存溢出
②虚拟机栈
Java Virtual Machine Stacks (Java 虚拟机栈)
虚拟机栈类似于子弹夹,一个压子弹的过程,先压的后出来,先进后出;一个子弹就类似于一个栈帧,一个栈帧就对应一个方法的调用,每个方法在执行的时候都会创建一个栈帧,栈帧包含存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型(局部变量表编译器完成,运行期间不会变化)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
虚拟机栈的演示:
栈帧的出栈时会返回到调用该栈帧的返回地址,活动栈帧就是正在执行的栈帧,也就是正在执行的某一个方法;
问题辨析
-
垃圾回收是否涉及栈内存?
不会,栈内存就是一次次方法调用,产生是栈帧内存,当该方法执行后对应的栈帧也就被弹出栈,自动的就被回收了,不用垃圾回收 -
栈内存分配越大越好吗?
栈内存在jvm启动时可以设置;栈内存设置越大,线程数就会越少,一般栈默认是1M大小; -
方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全问题
栈内存溢出
- 栈帧过多导致栈内存溢出(递归调用)
- 栈帧过大导致栈内存溢出
/**
* 演示栈内存溢出 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();
}
}
线程运行诊断
案例1: cpu 占用过多定位
-
1.用top定位哪个进程对cpu的占用过高
-
2.ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高) H输出进程内容 -eo输出感兴趣的内容
-
3.jstack 进程id; 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
jstack 32655 ,会将java进程ID是32655 内部所有的线程都会打印出来,32665线程Id需要转为16进制7f99,因为jstack 命令输出的线程id是16进制表示的;
案例2:程序运行很长时间没有结果
演示死锁案例:
/**
* 演示线程死锁
*/
class A{};
class B{};
public class Demo1_3 {
static A a = new A();
static B b = new B();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (a) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
synchronized (b) {
synchronized (a) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
}
}
执行jstack pid
③本地方法栈
用C,C++语言编写的本地方法(native)与操作系统底层api打交道;给本地方法的运行提供内存空间。
④堆
Heap 堆
- 通过 new 关键字,创建对象都会使用堆内存
- 特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆内存溢出
/**
* 演示堆内存溢出 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 工具: 查看堆内存占用情况 jmap -heap 进程id (只能查询某一个时刻堆内存使用情况)
/**
* 演示堆内存
*/
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);
}
}
------>>>>>执行Jps命令
D:\other\jvm\>jps
11648 Jps
5600 Demo1_4
8400
10052 Launcher
12196 RemoteMavenServer
------>>>>>第一次执行jmap -heap 5600 命令
Heap Usage:
PS Young Generation
Eden Space: >>>>>新创建的对象都先会分配到Eden 区
capacity = 34078720 (32.5MB)
used = 4783456 (4.561859130859375MB) #使用了4M
free = 29295264 (27.938140869140625MB)
14.036489633413462% used
------>>>>>第二次执行jmap -heap 5600 命令
Heap Usage:
PS Young Generation
Eden Space:
capacity = 34078720 (32.5MB)
used = 15269232 (14.561874389648438MB) #使用了14M
free = 18809488 (17.938125610351562MB)
44.80576735276442% used
------>>>>>第三次执行jmap -heap 5600 命令 # 调用System.gc();
Heap Usage:
PS Young Generation
Eden Space:
capacity = 34078720 (32.5MB)
used = 681592 (0.6500167846679688MB) #gc后明显减少了
free = 33397128 (31.84998321533203MB)
2.0000516451322117% used
- jconsole 工具: 图形界面的,多功能的监测工具,可以连续监测
案例:垃圾回收后,内存占用仍然很高;说明GC后并没有回收垃圾,还存在引用。
/**
* 演示查看对象个数 堆转储 dump
*/
public class Demo1_13 {
public static void main(String[] args) throws InterruptedException {
//students 一直有强引用,不会被垃圾回收
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
// Student student = new Student();
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024*1024];
}
内存诊断_jvisualvm
⑤方法区
最权威的定义:
在jdk1.6版本中,方法区是概念上的划分,用永久代作为了方法区的实现,存储了类元信息,类加载器,运行时常量池(StringTable),占用jvm堆内存;
到jdk1.8版本中,方法区还是概念上的东西,用元空间作为了方法区的实现,也是存储了类元信息,类加载器,常量池;但是他不在占有堆内存了,不在是jvm来管理它的结构了,移到操作系统的本地内存了,但运行时常量池(StringTable)仍然分配在堆中。运行时常量池下面会重点分析。
方法区内存溢出
/**
* 演示元空间内存溢出 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);
}
}
}
//在jdk1.8异常 java.lang.OutOfMemoryError: Metaspace 对用jvm参数是:-XX:MaxMetaspaceSize=8m
//jdk1.6异常 java.lang.OutOfMemoryError: PermGen space 对用jvm参数是:-XX:MaxPermSize=8m
- 1.8 以前会导致永久代内存溢出
演示永久代内存溢出 java.lang.OutOfMemoryError:PermGenspace
-XX:MaxPermSize=8m - 1.8 之后会导致元空间内存溢出
演示元空间内存溢出java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspaceSize=8m
场景
动态生成类是很正常的,在spring,mybatis中都有使用,cglib中asm包就是动态生成class字节码的技术封装。
运行时常量池
字节码的基本组成分类
1.类基本信息,2.常量池,3.类方法定义,包含了虚拟机指令
// 二进制字节码(1.类基本信息,2.常量池,3.类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
使用 javap -v HelloWorld.class 命令对class文件进行反编译, -v 显示详细信息
Last modified 2020-3-13; size 567 bytes
MD5 checksum 8efebdac91aa496515fa1c161184e354
Compiled from "HelloWorld.java"
public class cn.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
---------------------以上是 1.类基本信息-------------------
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/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/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/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
---------------------以上是 2.常量池-------------------
{
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/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
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 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
---------------------以上是 3.类方法定义,包含了虚拟机指令-------------------
SourceFile: "HelloWorld.java"
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池(加载到堆中被使用),并把里面的符号地址变为真实地址
案例:
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
对应的字节码:
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 //4个局部变量表,其中包含this的传参
0: ldc #2 加载常量池2号的常量 // String a
2: astore_1 储存到下面 LocalVariableTable Slot为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 19: 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;
}
当该类被加载,它的常量池信息就会放入运行时常量池,但是注意这时 a b ab 都是常量池中的符号,
还没有变为 java 字符串对象,只有第一次使用了才会在运行时常量池真正的生成java的对象。
比如执行到 String s1 = "a" 这行代码时,执行 ldc #2 会把 a 符号变为 "a" 字符串对象,
在运行时常量池里面有一个 StringTable 对象(数据结构是hash表)找有没有"a" 字符串对象,
刚开始里面是空的,没有对象。他就会在StringTable里面找有没有字符串为"a"的值,第一次是没有的,
它就会把生成的 "a" 放入到StringTable,并返回StringTable里面的 "a" 值了;
同样,执行到String s2 = "b"这行代码时,在StringTable 数据里面又多了"b"这个对象,
执行到String s3 = "ab" 这行代码时,在StringTable 数据里面又多了"ab"这个对象,
这样StringTable 就从开始的没有对象就有了这3个对象 "a" "b" "ab"了。
注意,并不是每个字符串对象事先就放到StringTable里面,而是执行的用到它的这行代码
才开始创建这个字符串对象并放到StringTable,以后就可以复用这个字符串对象了,这个行为是一个懒惰的行为。
当执行 String s4 = s1 + s2; 这行代码时
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
//分析得到就是 new String("ab") new了一个对象
String s4 = s1 + s2;// new StringBuilder().append("a").append("b").toString() new String("ab")
System.out.println(s3 == s4); //false
当执行 String s5 = “a” + “b”; 这行代码时
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
System.out.println(s3 == s4); //false
System.out.println(s3 == s5); //true
测试字符串加载时延时的特性案例:
/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println(); // 字符串个数 2275
System.out.print("1");// 字符串个数 2276
System.out.print("2");// 字符串个数 2277
System.out.print("3");// 字符串个数 2278
System.out.print("4");// 字符串个数 2279
System.out.print("5");// 字符串个数 2280
System.out.print("6");// 字符串个数 2281
System.out.print("7");// 字符串个数 2282
System.out.print("8");// 字符串个数 2283
System.out.print("9");// 字符串个数 2284
System.out.print("0");// 字符串个数 2285
System.out.print("1");// 字符串个数 2285
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");// 字符串个数 2285
System.out.print(x); // 字符串个数
}
}
StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
测试:
public class Demo1_23 { // jdk 1.8 测试
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
//下面new String("ab")得到s,但是s不会放入到常量池中,可以通过intern方法将其放入到常量池中
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
}
}
字符串相关面试题
/**
* 演示字符串相关面试题
*/
public class Demo1_21 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab") 堆中的对象
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd") 堆中的对象
x2.intern();//如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
String x1 = "cd";
// 问,如果调换了【最后两行代码】的位置呢
System.out.println(x1 == x2);//true
}
}
StringTable 位置
StringTable之前在永久代,而永久代在FullGc的时候才会发生垃圾回收,而字符串使用很频繁,1.8移动到堆中,只需要minorgc就会发生垃圾回收,垃圾回收效率得到提高
/**
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit java.lang.OutOfMemoryError: Java heap space
* 在jdk6下设置 -XX:MaxPermSize=10m java.lang.OutOfMemoryError: PermGen space
*/
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());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
StringTable 垃圾回收
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100; j++) { // j=0, j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
StringTable内部是hashtable,数组 + 链表实现
j=0时:
j=100时:没有发生垃圾回收
j=10000时:发生了垃圾回收,10M内存存不下10000个字符串,发生了gc
StringTable 性能调优
/**
* 演示串池大小对性能的影响
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
*/
public class Demo1_24 {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
line.intern();//入池
}
System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
}
}
}
默认桶的个数是60013个,耗时331ms
改变桶的大小 -XX:StringTableSize=1009,这样hash碰撞概率就会增加,耗时8224ms
当字符串比较多并且很多重复的字符串的的时候,可以适当的增加StringTableSize,减少hash碰撞的几率;同时需要考虑将字符串对象是否入池。
/**
* 演示 intern 减少内存占用
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/
public class Demo1_25 {
public static void main(String[] args) throws IOException {
//防止垃圾回收,测试方便
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
//linux.words 文件有480万个字符串,但是只有48万个相同的,其余都是重复的
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if(line == null) {
break;
}
address.add(line);//test1
address.add(line.intern());//test2
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
address.add(line) //test1
address.add(line.intern()) //test2
很明显在很多字符串重复的情况下,加到StringTable是可以提高效率的,当然,经常被使用的字符串是最好的了。
直接内存
定义 Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
演示直接内存溢出
直接内存分配的底层原理
/**
* 直接内存分配的底层原理:Unsafe
*/
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存 base是分配的直接内存地址
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存 直接内存分配方法
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
分配和回收原理
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存