JVM


课程:JVM-黑马(B站资源)

JVM内存结构;

1. JVM的执行过程

??Java 源代码经过jvm编译成class二进制字节码文件后(会生成对应的jvm指令),经过解释器,把jvm指令翻译成机器码,最后交给CPU执行。

该过程涉及程序计数器,

程序计数器的作用:

??记住下一条jvm指令的执行地址,即当解释器把第一条指令翻译成机器码之后,会到计数器中找下一条要被翻译的指令。程序计数器在物理上是由寄存器实现的

程序计数器的特点:

??1. 线程私有的 : 当多线程时,线程1抢占cpu时间片,代码执行到地址为10的时候,此时线程2抢占了CPU时间片,线程1进入等待状态, 当线程1再次抢占到CPU时间片之后,会找到程序计数器,看下一条要执行的指令是哪一个

??2. 不会存在内存溢出

2. 虚拟机栈

  • 虚拟机栈定义:每个线程运行时需要的内存空间。
  • 每个栈由多个栈帧组成,对应着每次方法调用时所需要的的内存。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

栈帧:每个方法运行时需要的内存。(该内存用于:参数、局部变量等), 多个栈针的情况:方法1调用了方法2,方法2调用了方法3

虚拟机栈:与程序计数器、本地方法栈都是属于线程私有的JVM内存区域。虚拟机栈的生命周期是和线程相同的,是在JVM运行时创建的,在线程中,方法在执行的过程创建一个栈帧。主要用于存放局部变量表、操作栈、动态链接、方法出口等信息

3.问题

1. 垃圾回收是否涉及栈内存? -- 不涉及

??因为栈内存是一次次的方法调用产生的栈帧内存,而栈帧内存在每一次方法调用结束后都会自动弹出栈,自动回收掉。所以不需要垃圾回收参与,垃圾回收回收的是堆内存中的无用对象。

2. 栈内存分配越大越好吗? 

??栈内存可以在JVM运行时通过调整参数来指定大小。示例:- Xss Size   : - Xss 1024k 即指定的内存大小为1M。 WIndows系统的JVM内存大小是继续物理内存

不是将栈内存分配的越大越好,由于物理内存是一定的,假设为物理内存为500M, 如果一个指定线程的运行时需要的栈内存为1M,一共可以运行500个线程, 如果线程运行需要的栈内存调整未2M,则最多可以运

行250个线程

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

??这个要看这个变量是对每个线程是共享的还是私有的,线程私有的不会有线程安全问题。 如果把变量改成static,两个线程都会读取,此时不安全

public class Test7 {
	//static int x = 0;
	static void get() {
		int x = 0;
		for (int i = 0;i<= 100;i++) {
			x++;
		}
	}
}

4.栈内存溢出  java.lang.StackOverflowError

情况:

  • 栈帧过多导致栈内存溢出;

??    场景:1. 方法的递归调用,没有结束条件 ; Java对象转Json时,两个类之间的循环引用问题导致Json解析时会出现栈内存溢出,解决方案:在Json转换时打破这种循环引用,使用一个@JsonIgnore注解

  • 栈帧过大导致栈内存溢出

          场景:这种情况很少出现

5. 线程运行诊断

案例1:CPU占用过多

定位

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id
  • 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

案例2:程序运行很长时间没有结果

??线程死锁

6. 本地方法栈

本地方法:Native Method : 不是有Java编写的方法,是由C或C++编写的, 本地方法运行时所需要的的内存空间叫做本地方法栈

常见的本地方法:Object类中的 hashcode()、clone()、wait()、notify()等方法都是Native修饰的方法

public final native void notify();

7、Heap 堆

通过 new 关键字,创建对象都会使用堆内存

特点

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

8、堆内存溢出 OutOfMemoryError

堆内存中的垃圾回收机制是在对象不再被使用的情况下才会进行回收。

调整堆内存参数:-Xmx1024m

堆内存诊断:

1. jps 工具

??查看当前系统中有哪些 java 进程

2. jmap 工具

??查看堆内存占用情况 jmap - heap 进程id

3. jconsole 工具(jdk目录下bin目录下 jconsole.exe)

??图形界面的,多功能的监测工具,可以连续监测

9、方法区组成

jdk1.6 JVM方法区的实现使用的是永久代PermGen,占用堆内存空间。永久代理存储的是 class类信息、ClassLoader类加载器、运行时常量池,jdk1.6 运行时常量池包含StringTable

jkd1.8 JMV方法区的实现使用的是元空间MetaSpace,占用机器的物理内存。元空间也存储的是 class类信息、ClassLoader类加载器、运行时常量池. jdk1.8中StringTable位于堆中,而不是常量池中

1.永久代(在JVM中):JDK8版本之前的实现,永久代,顾名思义是永久存在,不进行垃圾回收,数据一直在里面,但是永久代存在一个问题,也就是说永久代的内存大小是给定的,也就是MaxPermSize,你不知道应

该设置为多大,如果使用默认值(非常大)的话,可能会发生OOM(OutOfMemory)内存溢出的问题,于是JDK8之后就设计出了元空间来解决此问题,把永久代从堆空间中移除

2.元空间(在本地内存):元空间是位于本地内存的一块区域,也就是说你能存放多少类的数据,不取决于MaxPermSize,而是由系统内存大小来决定,只要内存足够大,就不会发生OOM问题

10、方法区的内存溢出

1.8 以前会导致永久代内存溢出

1.8 之后会导致元空间内存溢出

场景

mybatis

* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space

* -XX:MaxPermSize=8m

* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace

* -XX:MaxMetaspaceSize=8m 

11、方法区的组成部分:常量池

二进制字节码文件主要组成: 类基本信息,常量池Constant pool(里面都是一些地址),类方法定义,包含了虚拟机指令

示例:

package cn.itcast.jvm.t5;

// 二进制字节码大致由3部分组成:(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
//在idea中使用Terminal进行查询,先javac HelloWorld.java得到HelloWorld.class ,在 javap -v HelloWorld.class
里面包含:
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            // cn/itcast/jvm/t5/HelloWorld
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               
   #8 = Utf8               ()V
方法:
1. 构造方法
public cn.itcast.jvm.t5.HelloWorld();

2. main方法, 方法中包含
public static void main(java.lang.String[]);
        //虚拟机指令:
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;   getstatic:获取一个静态变量
         3: ldc           #3                  // String hello world                                 ldc:加载一个参数
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V      invok :执行一次虚方法调用
         8: return
       // 这里面的#2 #3 等对应Constant pool常量池中的#2 #3

12、运行时常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址 

13、运行时常量池的组成部分StringTable

package cn.itcast.jvm.t1.stringtable;
// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    public static void main(String[] args) {
        // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 信息都是运行时常量池中的符号,还没有变为 java 字符串对象
        // 等到程序执行到具体引导到这个代码时,才会变为a的字符串对象,同时,准备好一块空间StringTable,查看table中是否有a对象
        // 如果没有,则将a放入table串池中,如果有,不添加 。 所以可以理解为懒加载 。 串池是一个hashtable结构,不能扩容
        
        // ldc #2 会把 a 符号变为 "a" 字符串对象
        // ldc #3 会把 b 符号变为 "b" 字符串对象
        // ldc #4 会把 ab 符号变为 "ab" 字符串对象
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        // new StringBuilder().append("a").append("b").toString()  new String("ab")
        //初始化一个StringBuilder,调用appened方法,最后转成toString, 再new String()对象将参数放进去
        //所以s4是位于堆内存中的
        String s4 = s1 + s2;
        String s5 = "a" + "b";  //ab  执行到这里的时候,s3的ab已经在串池中了 。 这是javac在编译期间的优化
        System.out.println(s3 == s4); //false
        System.out.println(s3 == s5); //true  

    }
}

14、StringTable 特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池

1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回  

//该规则是针对jdk1.7(包含)之后才适用
package cn.itcast.jvm.t1.stringtable;

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {

        //情况1:先于aa.intern() 执行String bb = "ab";
        //String bb = "ab";
        String aa = new String("a")+new String("b");
        String x = aa.intern();
        String bb = "ab";
        //情况2:后于aa.intern() 执行String bb = "ab";

        //情况1时:在执行aa.intern()时,此时常量池中已经有[ab],所以不再往常量池中放入,但此时的引用x指向的是常量池中的bb,所以bb = x 返回true
//        System.out.println(aa == x); //flase  aa是对内存中的对象
//        System.out.println(bb == x); //true

        //情况2时:在执行aa.intern()时,此时常量池还没有[ab],所以要往常量池中放入,所以bb = x 返回true
        System.out.println(aa == x); //true  当jdk为1.7之后时,为true; 但jdk1.6的时候,是把对堆中的ab进行复制了一份,放入了常量池,所以与bb定义的ab不是同一个,返回false
        System.out.println(bb == x); //true
    }
}

测试:

15、StringTable 的位置

jdk1.6 :StringTable是位于永久代的常量池中,即1.6串池(StringTable)占用的是永久代内存, 永久代内存溢出:PermGen space

jkd1.7开始,StringTable 转移到了堆中。即1.8串池(StringTable)占用的是堆空间内存 , 对内存溢出:head space

16.StringTable的垃圾回收

JVM