jvm精学

1.什么是 JVM ?

定义:

Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查

2.常见的jvm

黑马程序员java大数据 黑马程序员jvm_java

2.1简单了解jvm

黑马程序员java大数据 黑马程序员jvm_开发语言_02

3.程序计数器

3.1.程序计数器的作用

什么是程序计数器,它是干什么用的?

java中程序计数器是用寄存器实现的,它的作用是寻找下一个要执行的程序。

当我们的java程序被编译成二进制字节码文件后,如下图:

黑马程序员java大数据 黑马程序员jvm_java_03

右面,是我们写的代码,左面是二进制字节码形式(.class)

它们将由我们的解释器来将他们转换为机械码,从而让机器运行。

细心的你会发现,每个二进制字节码的前面都有一个类似于索引的数字。他们的作用也跟索引差不多,为当前程序标一个序号,记上他们的地址。

即使有了地址,解释器也不知道他们的顺序是什么样的,他只负责运行。

于是,便有了程序计数器,程序计数器记下了字节码运行的顺序,每当一行字节码走完,他就会立即告诉解释器下一个该走哪里。

双双配合,最终实现全部代码。

这就是程序计数器的作用,不断为解释器寻找下一个要执行的程序。

3.2.程序计数器的特点

  • 线程私有
    为了保证每个线程独立,高效的工作,每个线程都会有一个自己的程序计数器,方便记录自己的程序执行到哪里。
  • 不会有内存溢出

4.虚拟机栈

每个线程运行时所需要的内存,称为虚拟机栈。在jvm里,方法的临时储存是在栈里完成的。

每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。 (正在运行的方法)

黑马程序员java大数据 黑马程序员jvm_黑马程序员java大数据_04

线程里的方法是逐次的进入对应的栈里的,最顶上的方法是当前执行的,被称为活动栈桢。方法全部执行完后,会反着退出栈。之后被清除。

4.1有关栈的问题

垃圾回收是否涉及栈内存?

  1. 不涉及,因为栈的运行结束后会按从顶至底的顺序移除栈对应的线程的方法,所以不需要垃圾回收机制来处理长久不用的垃圾。

栈内存分配越大越好吗?

不是的,我们的物理内存是有限的,如果栈内存分配过大,会导致我们能运行的线程数变少。

关于内存调整的方法如下:

黑马程序员java大数据 黑马程序员jvm_黑马程序员java大数据_05

方法内的局部变量是否线程安全?

如果线程的变量没被static修饰的话

int x=10;
for(int i=0;i<100;i++){
x++;
}

此时,即使有多个线程来运行这个代码,结果也是稳定的。

大概情况如图

黑马程序员java大数据 黑马程序员jvm_后端_06

但是,如果变量x加了static那么就有可能出现线程安全问题,此时就需要上锁来让变量私有化。

黑马程序员java大数据 黑马程序员jvm_黑马程序员java大数据_07

黑马程序员java大数据 黑马程序员jvm_开发语言_08

4.2.栈内存溢出

栈内存溢出一般指的就是栈桢的数量过多,超过了栈的大小。

而导致栈帧数量过多的一大原因就是,递归函数。

调节栈大小

-Xss256k

点击idea的右上角的主函数设置。

4.3线程运行诊断

案例一:cpu 占用过多

解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

  • top 命令,查看是哪个进程占用 CPU 过高
  • ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
  • jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

4.4.本地方法栈

一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法。

比如 Obeject的clone()方法等。。

5.堆

定义

通过new关键字创建的对象都会被放在堆内存

特点

  • 所有线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制

5.1.堆内存溢出问题(java.lang.OutofMemoryError :java heap space.)

//限制
 限制:-Xms  规定的空间大小

黑马程序员java大数据 黑马程序员jvm_后端_09

堆内存诊断工具(java自带)

jps:直接输入jps查看所有进程

jmap:jmap -heap 线程编号 查看对应线程堆的情况

jconsole

jvirsalvm:jvm的图形化显示(里面的heapjump(堆转储) 可以对当前情况进行快照)

6.方法区

方法区是一个概念,它包括常量池+ClassLoader+Class还有串常量(StringTable)。

在逻辑上,方法区算是堆内存的一部分使用的是堆的永久代的内存,但是在实际实现上,不一定用堆的内存。

而在1.8之后增加了元空间这种概念,将方法区的实现从堆内存改到了操作系统内存。

它的结构如下:

黑马程序员java大数据 黑马程序员jvm_后端_10

6.1.内存溢出
  • 1.8以前会导致永久代内存溢出
  • 1.8以后会导致元空间内存溢出
6.2.常量池

常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,分为常量池运行时常量池

6.2.1.查看常量池的方法–通过反编译查看字节码文件
  • 获得对应类的.class文件
  • 在JDK对应的bin目录下运行cmd,也可以在IDEA控制台输入

    黑马程序员java大数据 黑马程序员jvm_字符串_11

  • 输入 javac 对应类的绝对路径(编译一次代码,可以用idea运行一次)
F:\JAVA\JDK8.0\bin>javac F:\Thread_study\src\com\nyima\JVM\day01\Main.javaCopy

输入完成后,对应的目录下就会出现类的.class文件

  • 在控制台输入 javap -v 类的绝对路径
javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy
  • 然后能在控制台看到反编译以后类的信息了
  • 类的基本信息
  • 常量池

黑马程序员java大数据 黑马程序员jvm_黑马程序员java大数据_12

  • 虚拟机中执行编译的方法(框内的是真正编译执行的内容,#号的内容需要在常量池中查找
6.2.2.运行时常量池和常量池区别
  • 常量池
  • 就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
  • 运行时常量池
  • 常量池是*.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
6.2.3.串池

常量池中的字符串仅是符号,只有在被用到时才会转化为对象储存到堆里或者串池里。

串池是什么?

黑马程序员java大数据 黑马程序员jvm_后端_10

黑马程序员java大数据 黑马程序员jvm_java_14

串池指的是串池(StringTable),如上图所示是方法区的一员,1.6之前串池在常量池里储存在逻辑上的堆内存的永久代里。因为永久代需要重GC清理,所以1.8之后对串池的位置进行了更改,使他物理意义上脱离了大多数方法区成员的位置,不在元空间(本地内存)里。

串池是用来保存String对象的,当常量池的字符被String对象引用时,若串池里无重复的对象,将该对象加入到串池。以后若再遇到相同String对象引用相同的字符串则直接使用串池里的对象。(拼接的时候不用,用的是堆内存)

实际代码如下图:

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a"; 
		String b = "b";
		String ab = "ab";
	}
}

常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串

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: returnCopy

当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)

当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中

当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中

最终StringTable [“a”, “b”, “ab”]

注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

串池的存在避免了字符串对象的重复创建。

字符串变量拼接和字符串常量拼接
  • 字符串变量拼接的原理是StringBuilder,拼接后的对象放在堆内存里。
  • 字符串常量拼接的原理是编译器优化,串池里如果有你拼接完的字符串则直接返回,没有则创建一个加入到串池。

使用拼接字符串变量对象创建字符串的过程

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a";
		String b = "b";
		String ab = "ab";
		//拼接字符串对象来创建新的字符串a
		String ab2 = a+b; 
	}
}Copy

反编译后的结果

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
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        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/Str
ing;
        27: astore        4
        29: returnCopy

通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()

最后通过toString方法的返回值是一个新的字符串.

String ab = "ab";
String ab2 = a+b;
//结果为false,因为ab是存在于串池之中,ab2是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
System.out.println(ab == ab2);//false

使用拼接字符串常量对象的方法创建字符串

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a";v
		String b = "b";
		String ab = "ab";
		String ab2 = a+b;
		//使用拼接字符串的方法创建字符串
		String ab3 = "a" + "b";
	}
}

反编译后的结果

Code:
      stack=2, locals=6, 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: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        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/Str
ing;
        27: astore        4
        //ab3初始化时直接从串池中获取字符串
        29: ldc           #4                  // String ab
        31: astore        5
        33: returnCopy
  • 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。
  • 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建
intern方法

1.8

调用字符串对象的intern()方法,会将该字符串对象尝试放入到串池中。

  • 如果串池中没有该字符串对象,则放入成功,返回引用的对象
  • 如果有该字符串对象,则放入失败,返回字符串里有的该对象

无论放入是否成功,都会返回串池中的字符串对象。

注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

1.6

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中,返回的是复制的对象
  • 如果有该字符串对象,则放入失败,返回串池原有的该字符串的对象

注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

测试

原来串池有该字符串

此时,无论1.6还是1.8 x都不等于s

黑马程序员java大数据 黑马程序员jvm_黑马程序员java大数据_15

原来串池里没有字符串

1.6 因为是复制一份新的堆的对象,所以和原来的对象s不同

黑马程序员java大数据 黑马程序员jvm_字符串_16

1.8.因为和原来的对象s相同所以 true

黑马程序员java大数据 黑马程序员jvm_java_17