Java虚拟机JVM详解

JVM内存模型

Java虚拟机(Java Virtual Machine=JVM)的内存空间分为五个部分,分别是:

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • 方法区(1.8中由元空间替代)

程序计数器

程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示
器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。
程序计数器有两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:
    顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回
    来的时候能够知道该线程上次运行到哪儿了。

程序计数器的特点

  • 是一块较小的存储空间
  • 线程私有。每条线程都有一个程序计数器。
  • 是唯一一个不会出现OutOfMemoryError的内存区域。
  • 生命周期随着线程的创建而创建,随着线程的结束而死亡。

Java虚拟机栈(JVM Stack)

什么是Java虚拟机栈?
Java虚拟机栈是描述Java方法运行过程的内存模型。
Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于
存储该方法在运行过程中所需要的一些信息,这些信息包括:

  1. 局部变量表
    局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,他不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
  2. 操作数栈 方法操作的数
    当一个方法即将被运行时,Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块“栈
    帧”,栈帧中包含局部变量表、操作数栈、动态链接、方法出口信息等。当方法在运行过程
    中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中。当这个方法执行完毕
    后,这个方法所对应的栈帧将会出栈,并释放内存空间。
    虚拟机栈的栈元素是栈帧,当有一个方法被调用时,代表这个方法的栈帧入栈;当这个方法返回时,其栈帧出栈。后进先出最后调用的方法先结束。
    真正的Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数
    栈、动态链接、方法出口信息。
    Java虚拟机栈的特点
  • 局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。而且,局部变量
    表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。此
    外,在方法运行的过程中局部变量表的大小是不会发生改变的。
  • Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程
    的创建而创建,随着线程的死亡而死亡。

本地方法栈

本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法区是本地方法运行的内存模
型。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量
表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间。
也会抛出StackOverFlowError和OutOfMemoryError异常。

堆是用来存放对象的内存空间。
几乎所有的对象都存储在堆中。
堆的特点

  1. 线程共享
    整个Java虚拟机只有一个堆,所有的线程都访问同一个堆。
  2. 在虚拟机启动时创建
  3. 垃圾回收的主要场所。
  4. 可以进一步细分为:新生代、老年代。
    新生代又可被分为:Eden、From Survior、To Survior。
    不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算
    法,从而更具有针对性,从而更高效。
  • 堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的,因此当线程
    请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError。

堆配置

  • -Xms :设置Java堆栈的初始化大小
  • -Xmx :设置最大的java堆大小
  • -Xmn :设置Young区大小
  • -Xss :设置java线程堆栈大小
  • -XX:PermSize and MaxPermSize :设置持久带的大小
  • -XX:NewRatio :设置年轻代和老年代的比值(Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置)
  • -XX:NewSize :设置年轻代的大小
  • -XX:SurvivorRation=n :设置年轻代中E去与俩个S去的比值

由于对年轻代的复制收集,依然必须停止所有应用程序线程,原理如此,只能靠多CPU,多收集线程并发来提高收集速度,但除非你的 Server独占整台服务器,否则如果服务器上本身还有很多其他线程时,切换起来速度就… 所以,搞到最后,暂停时间的瓶颈就落在了年轻代的复制算法上。
因此Young的大小设置挺重要的,大点就不用频繁GC,而且增大GC的间隔后,可以让多点对象自己死掉而不用复制了。但Young增大时,GC 造成的停顿时间攀升得非常恐怖,据某人的测试结果显示:默认8M的Young,只需要几毫秒的时间,64M就升到90毫秒,而升到256M时,就要到 300毫秒了,峰值还会攀到恐怖的800ms。谁叫复制算法,要等Young满了才开始收集,开始收集就要停止所有线程呢。

-Xmn和-Xmx之比大概是1:9(通常年轻代占堆的1/3,具体情况具体分析年轻代够用的情况下越小越好),假设把新生代内存设置得太大会导致young gc时间较长
一般Xms 和Xmx配置成一样以避免每次gc后JVM又一次分配内存。

一个好的Web系统应该是每次http请求申请内存都能在young gc回收掉,full gc永不发生。当然这是最理想的情况

xmn的值应该是保证够用(够http并发请求之用)的前提下设置得尽量小
虚拟机参数配置

Java 8以后移除了方法区,取而代之的是本地元空间Metaspace,大小由-
XX:MetaspaceSize和-XX:MaxMetaspaceSize调节。

方法区
Java虚拟机规范中定义方法区是堆的一个逻辑部分。
方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
方法区的特点

  • 线程共享
    方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法
    区。
  • 永久代
    一般说堆的持久代就是说方法区,因为一旦JVM把方法区(类信息,常量池,静态字段,
    方法)加载进内存以后,这些内存一般是不会被回收的了。方法区物理上存在于堆里,而且
    是在堆的持久代里面;但在逻辑上,方法区和堆是独立的。

1.8已经没有永久代了 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不
过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因
此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大
小。另外,在jdk1.8中将字符串常量池和静态变量放到 Java 堆里。运行时常量池还在元空间内存储

  • 内存回收效率低
    方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。
    对方法区的内存回收的主要目标是:对常量池的回收 和 对类型的卸载。
  • Java虚拟机规范对方法区的要求比较宽松。
    和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。

Java中的常量池分为三种类型:

类文件中常量池(The Constant Pool)
运行时常量池(The Run-Time Constant Pool)
字符串常量池
类文件中常量池 ---- 存在于Class文件中
所处区域:堆

诞生时间:编译时

内容概要:符号引用和字面量

class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。

  • 这里的字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;声明为final的常量值指的是成员变量,不包含本地变量,本地变量是属于方法的。这些都在常量池的 UTF-8 表中(逻辑上的划分);
  • 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用;

运行时常量池 ---- 存在于内存的元空间中
诞生时间:JVM运行时

内容概要:class文件元信息描述,编译后的代码数据,引用类型数据,类文件常量池。

所谓的运行时常量池其实就是将编译后的类信息放入运行时的一个区域中,用来动态获取类信息。

运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

在运行期间,可以向常量池中添加新的常量。如:String类的intern()方法就能在运行期间
向常量池中添加字符串常量。

当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收
集器回收。
字符串常量池 ---- 存在于堆中

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

String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2);

结果是 true;

采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在”abc”这个对象,如果不存在,则在字符串常量池中创建”abc”这个对象,然后将池中”abc”这个对象的引用地址返回给”abc”对象的引用s1,这样s1会指向字符串常量池中”abc”这个字符串对象;如果存在,则不创建任何对象,直接将池中”abc”这个对象的地址返回,赋给引用s2。因为s1、s2都是指向同一个字符串池中的”abc”对象,所以结果为true。

String s3 = new String("xyz");
String s4 = new String("xyz");
System.out.println(s3==s4);

结果是 false

采用new关键字新建一个字符串对象时,JVM首先在字符串池中查找有没有”xyz”这个字符串对象,如果有,则不在池中再去创建”xyz”这个对象了,直接在堆中创建一个”xyz”字符串对象,然后将堆中的这个”xyz”对象的地址返回赋给引用s3,这样,s3就指向了堆中创建的这个”xyz”字符串对象;如果没有,则首先在字符串池中创建一个”xyz”字符串对象,然后再在堆中创建一个”xyz”字符串对象,然后将堆中这个”xyz”字符串对象的地址返回赋给s3引用,这样,s3指向了堆中创建的这个”xyz”字符串对象。s4则指向了堆中创建的另一个”xyz”字符串对象。s3 、s4是两个指向不同对象的引用,结果当然是false。
String.intern()方法: 官方解释Returns a canonical representation for the string object.(返回字符串对象的规范化表示形式。)简单点说就是返回String池的字符串。

  • 对于直接做+运算的两个字符串(字面量)常量,并不会放入字符串常量池中,而是直接把运算后的结果放入字符串常量池中
    (String s = “abc”+ “def”, 会直接生成“abcdef"字符串常量 而不把 “abc” "def"放进常量池)

  • 对于先声明的字符串字面量常量,会放入字符串常量池,但是若使用字面量的引用进行运算就不会把运算后的结果放入字符串常量池中了
    (String s = new String(“abc”) + new String(“def”),在构造过程中不会生成“abcdef"字符串常量)
    总结一下就是JVM会对字符串常量的运算进行优化,未声明的,只放结果;已经声明的,只放声明

  • 常量池中同时存在字符串常量和字符串引用。直接赋值和用字符串调用String构造函数都可能导致常量池中生成字符串常量;而intern()方法会尝试将堆中对象的引用放入常量池

  • String str1 = “a”;
    String str2 = “b”;
    String str4 = str1 + str2; //该语句只在堆中生成一个对象(str4) 一共3个对象
    在1.8中这句被Java编译器做了优化, 实际上使用StringBuilder实现的(不在堆里生成str1和str2对象)

  • String str5 = new String(“ab”);只在堆中生成了一个对象str5, 不在字符换常量池中创建字面量"ab"
    当通过new一个String对象的方式创建时,无论字符串常量池中存在与否,都会在堆内存中创建对象,此刻如果字符串常量池已经存在,则只在堆内存中创建一个内存,如果字符串常量池中没有该字符串对象,则分别在常量池和堆中创建。(也就是说:常量池中有,则创建一个对象,没有,则创建两个对象)
    综上所述

  1. Java虚拟机的内存模型中一共有两个“栈”,分别是:Java虚拟机栈和本地方法
    栈。
    两个“栈”的功能类似,都是方法运行过程的内存模型。并且两个“栈”内部构造相同,都
    是线程私有。
    只不过Java虚拟机栈描述的是Java方法运行过程的内存模型,而本地方法栈是描述Java本
    地方法运行过程的内存模型。
  2. Java虚拟机的内存模型中一共有两个“堆”,一个是原本的堆,一个是方法区。方
    法区本质上是属于堆的一个逻辑部分。堆中存放对象,方法区中存放类信息、常量、静
    态变量、即时编译器编译的代码。
  3. 堆是Java虚拟机中最大的一块内存区域,也是垃圾收集器主要的工作区域。
  4. 程序计数器、Java虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的
    程序计数器、Java虚拟机栈、本地方法区。并且他们的生命周期和所属的线程一样。
    而堆、方法区是线程共享的,在Java虚拟机中只有一个堆、一个方法栈。并在JVM启动的时
    候就创建,JVM停止才销毁。

元空间fullgc案例 https://blog.csdn.net/qiang_zi_/article/details/100700784

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值