【JVM】一文看懂JVM的内存结构

JVM简介

什么是JVM

定义:java virtual meachine -java运行时环境(java二进制字节码的运行环境)。
好处:

  1. 一次编写到处运行
  2. 自动内存管理,垃圾回收
  3. 数组下标越界检查
  4. 多态

JVM的位置

【JVM】一文看懂JVM的内存结构_第1张图片

一些JVM

  • SUN Classic
  • Exact VM
  • HotSpot VM :HotSpot指热点代码探测技术
  • BEA JRockit:(BEA 已被Oracle收购) 专注于服务端应用,世界最快的jvm之一
  • IBM J9
  • Taobao JVM: 目前已经在淘宝、天猫上线,替换了Oracle官方JVM;
  • Graal VM: Oracle 2018年4月公开,口号 Run Programs Faster Anywhere.最可能替代HotSpot的产品

java代码执行流程

java程序 -->(编译)-->  字节码文件 -->  解释器  -->  机器码  -->   操作系统(Win,Linux,Mac JVM)

栈的指令集架构和寄存器的指令集架构

【JVM】一文看懂JVM的内存结构_第2张图片

由于跨平台的设计,java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的。

jvm生命周期

1.启动

通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的.

2.执行

  • 一个运行中的java虚拟机有着一个清晰的任务:执行Java程序;
  • 程序开始执行的时候他才运行,程序结束时他就停止;
  • 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。

3.退出

  • 程序正常执行结束
  • 程序异常或错误而异常终止
  • 操作系统错误导致终止
  • 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且java安全管理器也允许这次exit或halt操作
  • 除此之外,JNI规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况

 JVM内存结构

【JVM】一文看懂JVM的内存结构_第3张图片

运行时数据区域

程序计数器

作用

是当前线程锁执行的字节码的行号指示器,字节码解释器通过改变计数器的值来选取下一条需要执行的字节码指令。

 特点

  • 线程私有。Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间实现的,任何一个时刻只执行一个特定的指令,因此为了不同线程切换的时候知道之前自己运行到哪里了,每条线程需要拥有自己的程序计数器。
  • 不会内存泄漏。唯一一个不会出现内存溢出问题的区域。
  • 如果线程执行的是一个Java方法,那么程序计数器记录的是正在执行的字节码指令的地址,如果执行的是本地方法(Native),那么计数器中为空。

Java虚拟机栈

作用

是线程运行时需要的内存空间。

特点

  • 是线程私有的,生命周期与其所在线程相同。
  • 每个方法被执行的时候都要创建一个栈帧(一个栈帧对应一次方法的调用)用于存储信息。每个方法的实行过程都对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
  • 每个线程只能有一个活动栈帧,也就是正在执行的那个方法。

注意

  • 垃圾回收不涉及栈内存
  •  方法内的局部变量,如果多个线程调用,不会线程不安全,各个线程拥有自己的这个局部变量,互不干扰
  • 虚拟机栈中的局部变量表所需要的内存空间在编译时候分配完成,在方法运行期间不会改变大小

栈内存溢出的原因

  • 栈帧过多导致,StackOverflowError异常 
  • 栈帧过大导致,OutOfMemoryError异常

线程诊断

  • cpu占用过多

    • 定位进程,top找出占用cpu过多的进程PID
    • grep PID 找出该进程下占用过大的线程PID
    • jstack pid 查看进程中各线程的详细信息,可以看出问题所在。注意:这里PID为十六进制,PID转换为16进制。

本地方法栈

作用

与虚拟机栈类似,只不过本地方法栈是为虚拟机使用到的Native方法服务。

是虚拟机中所管理的内存中最大的一块,通过new关键字创建的对象,都存放在堆内存中,被所有的线程所共享,在虚拟机启动时被创建,具有垃圾回收机制。

堆内存溢出

一个简单的例子 java.lang.OutOfMemeryError:Java Heap space

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List 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);
        }
    }
}

程序执行过程中,list对象始终被关联,无法被回收,死循环不断将list规模变大,最终大于堆内存大小,内存溢出。

堆内存诊断

  • jps
    • 查看系统有哪些进程。
  • jmap
    • 查看堆内存使用情况 jmap -heap PID
  • jconsole
    • 图形界面,多功能检测工具,连续监测。

方法区

和堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息(方法、构造器)、常量、静态变量等,在虚拟机启动时被创建。  

垃圾回收在方法区较少出现,主要针对常量池的回收和对类型的卸载。

组成与实现: jdk6(永久代实现)和jdk8(元空间实现)中方法区的区别,其中最主要的区别是jdk8中将方法区转移到本地内存中,且常量池分为运行时常量池和字符串常量池;且字符串常量池被留在内存中的堆中。

方法区的结构:

【JVM】一文看懂JVM的内存结构_第4张图片

常量池

常量池,运行时常量池和字符串池 

全局字符串池(string pool也有叫做string literal pool):

  • 全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。
  • 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享

class文件常量池(class constant pool):class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译 器生成的各种字面量(Literal)和符号引用(Symbolic References)。

  • 字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。
  • 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
    • 类和接口的全限定名
    • 字段名称和描述符
    • 方法名称和描述符

运行时常量池:

  • jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。
  • 当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。
  • class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

举例说明

public class HelloWorld {
    public static void main(String []args) {
		String str1 = "abc"; 
		String str2 = new String("def"); 
		String str3 = "abc"; 
		String str4 = str2.intern(); 
		String str5 = "def"; 
		System.out.println(str1 == str3);//true 
		System.out.println(str2 == str4);//false 
		System.out.println(str4 == str5);//true
    }
}
  • String str1 = "abc";
    • 中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值
  • String str2 = new String("def"); 会生成两个实例。
    • 一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值
    • 还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例
  • String str3 = "abc"; 解析str3时,在StringTable中寻找“abc”,发现有全局字符引用,所以str3的引用地址和str1相同
  • String str4 = str2.intern();
    • intern()函数返回StringTable中”def”的引用值。因为StringTable中已经有“def”引用值。
    • 返回str2中new出来的“def”在StringTable中的引用值。
    • str5亦是如此。

1. 编译期优化

        String s1 = "a";
        “a”的时候把对象放入串池

        String s2 = "b";
        String s3 = "ab";
        /*
            new StringBuilder().append("a").append("b").toString();
             s1 s2 为变量,其值没有确定,s4为新new出的对象,存放在堆中
         */
        String s4 = s1+s2;
        System.out.println(s4==s3);//false

         /*
            javac编译期优化,“a”,“b”已经确定,所以在编译期s5就已经确定为“ab”
            在串池中寻找到“ab”,返回“ab”引用值
         */
        String s5 = "a"+"b";
        System.out.println(s3==s5);//true

2. 字符串延迟实例化

	String s1 = "a";
	//java.lang.string.count = 2361
        String s2 = "b";
	//java.lang.string.count = 2362
        String s3 = "ab";
	//java.lang.string.count = 2363	

3. 常量池和串池的关系

  • 常量池中的都会被加载到运行时常量池中,此时字符串都是常量池中的符号,尚且未转化为对象
  • 运行时,将String字面量符号“a”转化为字符串对象,并在StringTable中寻找“a”,如多没有则添加“a”字符串对象。
  • StringTable 结构为HashTable,且不能扩容。

4. intern方法的运用jdk1.8

public class internDemo {
    public static void main(String[] args) {

        String s = new String("a") + new String("b");
        //"a"把对象放入串池,不是new String("a")放入,new放入的是堆
        //new String("a") new String("b") new String("ab")
        //StringTable["a","b"] "ab"属于动态拼接,不会加入StringTable,s在堆中

        //使用intern方法 在StringTable中放入此字符串对象,如果有则返回给字符串对象,没有在放入
        String s2 = s.intern();  //s放入串池

        //s2指向的是串池中字符串对象 s也被放入串池中
        System.out.println(s==s2);//true
    }
}



public class internDemo {
    public static void main(String[] args) {
        String s3 = "ab";

        //new String("a") new String("b") new String("ab")
        //StringTable["a","b","ab"]
        String s = new String("a") + new String("b");

        //使用intern方法 在StringTable中放入此字符串对象,如果有则返回给字符串对象,没有再放入,此时串池中实际上已经有了“ab”,因此返回了串池中的“ab”
        String s2 = s.intern();

        //s2指向的是串池中字符串对象 s未被放入串池,还在堆中
        System.out.println(s3==s2);//true
        System.out.println(s3==s);//false

    }
}

5. intern方法的运用jdk1.6 

public class internDemo {
    public static void main(String[] args) {

        //new String("a") new String("b") new String("ab")
        //StringTable["a","b"]
        String s = new String("a") + new String("b");
        //使用intern方法 在StringTable中放入此字符串对象,如果有则返回给字符串对象,没有则复制一份后放入,因此串池中的“ab”和s是不同的对象
        String s2 = s.intern();
        //s2指向的是串池中字符串对象(s复制后的对象) ,s未被放入串池,还在堆中
	    //StringTable["a","b","ab"]
 	    String s3 = "ab";
        System.out.println(s3==s2);//true
        System.out.println(s3==s);//flase,s还在堆中,s3在常量池中

    }
}
  • 总结
  1. 全局字符串池每个虚拟机只有一个,存储字符串常量的引用值;
  2. class常量池是java程序编译之后才有的,每个类都有,存放字面值和符号引用常量;
  3. 运行时常量池是在类加载完之后,常量池内容存储在运行时常量池中,每个类都有一个,且常量池中符号引用转换为直接引用,与全局字符串池中保持一致;
  4. 常量池中的字符串仅仅是符号,在试一次使用的时候才会变为对象
  5. 利用串池的机制,避免重复创建字符串对象
  6. 字符串变量的拼接原理是StringBuilder(1.8)
  7. 字符串常量拼接的原理是编译期进行优化
  8. 使用intern方法,主动将串池中海没有的字符串放入到串池当中。在JDK1.8中,将这个字符串对象尝试放入串池,如果串池中已经存在则不会放入,如果没有则会放入,会把串池中的对象放回。在JDK1.6中,将这个字符串对象尝试放入串池,如果串池中已经存在则不会放入,如果没有则会把次对象复制一份,放入串池,会把串池中的对象放回。

6. StringTable位置

  • jdk1.6及之前,StringTable与方法区一同在永久代中。
  • jdk1.8之后,方法区转移到本地内存中,但是将StringTable转移到堆内存中。
  • 原因
    • StringTable中存在大量的字符串对象,运行时间增长永久代内存占用过多,且永久代只有在触发FULL GC时才进行垃圾回收,回收频率过慢。
    • 转移到堆中可以利用虚拟机在堆内存中频繁的垃圾回收,处理StringTable中对象过多情况。

7. StringTable调优

  • 调整hash表中桶子个数,-XX:StringTableSize=桶个数
  • 考虑字符串是否入池

直接内存

不是虚拟机运行时内存的一部分,而是系统内存。

  • 常见于NIO操作中,用于数据缓冲
  • 分配回收成本高,但读写能力强
  • 不受JVM内存回收管理

直接内存使用前和使用后

不使用直接内存:

【JVM】一文看懂JVM的内存结构_第5张图片

  • 因为java无法操作本地文件,在java堆内存中划出java缓冲区;
  • 从用户态转移到内核态,本地方法在系统内存中划出一段系统缓冲区,将磁盘文件部分缓冲到系统缓冲区中,间接的将系统缓冲区中数据传输到java缓冲区中
  • 内核态转到用户态,调用输出流写入操作,将文件copy到另一个位置,循环copy,直到全部复制完成。
  • 效率低

使用直接内存:

【JVM】一文看懂JVM的内存结构_第6张图片

  • ByteBuffer.allocateDirect(_size),在系统内存中分配直接内存;
  • 系统方法和java方法都可以访问直接内存;
  • 与不使用直接内存相比,减少了一次从系统缓存区向java缓冲区复制的操作,复制效率成倍上升。

 直接内存不受JVM内存回收管理,那么如何进行垃圾回收?看一下内存溢出的情况:

package com.example.jvm;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class Demo3 {

    public static void main(String[] args) {
        List list = new ArrayList<>();
        int i = 0;
        try {
            while(true){
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100*1024*1024);
                list.add(byteBuffer);
                i++;
            }
        }finally {
            System.out.println(i);
        }
    }
}

36  3.6G
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.example.jvm.Demo3.main(Demo3.java:14)

直接内存的分配和回收原理

  • 内部使用一个叫Unsafe的对象实现直接内存的分配和回收,回收主要使用的是其freeMemory方法,不是垃圾回收
  • ByteBuffer类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦对象被回收,就会由ReferenceHandler线程通过Cleaner的clean对象调用freeMenory来释放直接内存。
  • 在调优时候使用 -XX:+DisableExplicitGC 使显式的(通过System.gc()显式的垃圾回收)垃圾回收FULL GC,被禁用。
  • 因为考虑到系统性能,FULL GC时间够长,会严重影响性能。所以涉及到直接内存的使用,释放内存使用Unsafe.freeMemory,不建议使用System.gc()。

你可能感兴趣的:(JVM)