关于java虚拟机

Posted frienda1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于java虚拟机相关的知识,希望对你有一定的参考价值。

 

JVM

堆和栈

简单意义上,Java把内存划分为两种:一种是栈内存,另一种是堆内存

  • 栈式存储:

    • 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。

    • 优点:存取速度比堆要快,仅次于寄存器,栈数据可以共享。

    • 缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄。

  • 堆式存储:

    • 堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。

    • 优点:堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。

    • 缺点是:由于要在运行时动态分配内存,存取速度较慢。

       

JVM内存模型

细致划分后,JVM内存模型可以如下分为两部分

线程共享:堆和方法区

线程私有:虚拟机栈,本地方法栈,程序计数器

技术图片

堆内存

堆区是理解Java GC机制最重要的区域。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区用来存储对象实例及数组值,可以认为java中所有通过new创建的对象都在此分配。根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可

对于堆区大小,可以通过参数-Xms-Xmx来控制,-Xms为JVM启动时申请的最新heap内存,默认为物理内存的1/64但小于1GB;-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4但小于1GB,默认当剩余堆空间小于40%时,JVM会增大Heap到-Xmx大小,可通过-XX:MinHeapFreeRadio参数来控制这个比例

当空余堆内存大于70%时,JVM会减小Heap大小到-Xms指定大小,可以通过-XX:MaxHeapFreeRatio来指定这个比例。对于系统而言,为了避免在运行期间频繁的调整Heap大小,我们通常将-Xms-Xmx设置成一样。为了让内存回收更加高效,从Sun JDK 1.2开始对堆采用了分代管理方式,如下图所示:

技术图片

堆内存是线程共享的,可以分为三个部分:新生代,老年代和永久代(Java8之前)。

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

新生代

新生代主要是用于存放新生的对象,一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收

新生代又可以划分为 Eden区、ServivorFrom、ServivorTo三个区域

Eden区和Servior区的内存比为8:1,可通过-Xmn参数来调整新生代大小,也可通过-XX:SurvivorRadio来调整Eden Space和Survivor Space大小。

Eden区

Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。Eden区内存是连续的,所以其分配会非常快;同样Eden区的回收也非常快当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java Heap Space异常

ServivorTo:

保留了一次MinorGC过程中的幸存者。

ServivorFrom:

上一次GC的幸存者,作为这一次GC的被扫描者。

MinorGC:

如果Eden空间占满了, 会触发 minor GC。MinorGC采用复制算法,首先把Eden和ServivorFrom区域中存活的对象复制到ServivorTo区域,同时会有一个计数器把对象年龄+1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。,最后将ServivorFrom和Eden区域清空,同时将ServivorFrom和ServivorTo区域互换。依次循环此过程。

从以上过程可知,JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

技术图片

老年代

老年代主要用于存放在年轻代中经多次垃圾回收仍然存活的对象,可以理解为比较老一点的对象,例如缓存对象;

新建的对象也有可能直接在老年代上直接分配内存,主要有两种情况。一种为大对象,可以通过启动参数设置-XX:PretenureSizeThreshold=1024,表示超过多大时就不在年轻代分配,而是直接在老年代分配。此参数在年轻代采用Parallel Scavenge GC时无效,因为其会根据运行情况自己决定什么对象直接在老年代上分配内存;另一种为大的数组对象,且数组对象中无引用外部对象。

当老年代满了的时候就需要对老年代进行垃圾回收,老年代的垃圾回收称作Full GC。老年代所占用的内存大小为-Xmx对应的值减去-Xmn对应的值。

 

 

方法区

方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。

方法区主要可以分为两个板块

Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是 String.intern() 方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

注意我们这里所说的运行时的常量池仅仅是指Class文件中的常量池,因为JVM可能会进行即时编译进行优化,在运行时将部分常量载入到常量池中。

class文件信息与运行时常量池 如图所示

技术图片

技术图片

Class文件由无符号数构成。

无符号数:以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值、按照UTF-8编码构成的字符串值。

:由多个无符号数或其他表作为数据项构成的复杂数据类型,所有表都习惯性地以“_info”结尾。

技术图片

?
class文件信息
?

技术图片

 

?
运行时常量池
#### Java中的常量池
?

Java中的常量池,实际上分为两种形态:静态常量池运行时常量池

所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名

  • 字段名称和描述符

  • 方法名称和描述符

    运行时常量池,则是JVM虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

常量池的好处

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。 例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。 (1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。 (2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

关于class文件各个属性的具体学习可以参考

关于Java中的常量池应用可以参考

关于Java的class文件结构,可以参考

 

方法区和永久代

方法区是一种规范,永久代是Hotspot针对这一规范的一种实现。

对于Java8, Hotspot取消了永久代(Perm区),取代永久代的是元空间(metaspace)

总结:

(1)方法区是规范层面的东西,规定了这一个区域要存放哪些东西

(2)永久带或者是metaspace是对方法区的不同实现,是实现层面的东西。

永久代和元空间的区别:

存储位置不同,永久代是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;

存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。

 

程序计数器

程序计数器:也叫PC寄存器,JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是 JVM 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native方法,则PC寄存器中为空。(PS:线程执行过程中并不都是一口气执行完,有可能在一个CPU时钟周期内没有执行完,由于时间片用完了,所以不得不暂停执行,当下一次获得CPU资源时,通过程序计数器就知道该从什么地方开始执行)

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

Java虚拟机栈即常说的”栈内存“

虚拟机栈:线程私有,随线程创建而创建。栈里面是一个一个“栈帧”,每个栈帧对应一次方法调用。栈帧中存放了局部变量表(基本数据类型变量和对象引用)、操作数栈、方法出口等信息。

当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误。如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

技术图片

 

局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

操作数栈

和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。

虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。

 

本地方法栈

本地方法栈(NativeMethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

 

关于JVM,可以参考以下内容

 

以上是关于关于java虚拟机的主要内容,如果未能解决你的问题,请参考以下文章

实战Java虚拟机之中的一个“堆溢出处理”

Java虚拟机系列(25篇文章)一起啃

关于JAVA虚拟机内存

节:虚拟机与Java虚拟机介绍

JAVA 虚拟机深入研究——Java内存区域

节:虚拟机与Java虚拟机介绍