jvm的理解

Posted

tags:

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

1
JVM内存区域

我们在编写程序时,经常会遇到OOM(out of Memory)以及内存泄漏等问题。为了避免出现这些问题,我们首先必须对JVM的内存划分有个具体的认识。JVM将内存主要划分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。JVM运行时数据区如下:

1.1
程序计数器
程序计数器是线程私有的区域,很好理解嘛~,每个线程当然得有个计数器记录当前执行到那个指令。占用的内存空间小,可以把它看成是当前线程所执行的字节码的行号指示器。如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器的值为空(Undefined)。

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

1.2
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,其生命周期与线程相同。

如何理解虚拟机栈呢?

本质上来讲,就是个栈。里面存放的元素叫栈帧,栈帧好像很复杂的样子,其实它很简单!它里面存放的是一个函数的上下文,具体存放的是执行的函数的一些数据。执行的函数需要的数据无非就是局部变量表(保存函数内部的变量)、操作数栈(执行引擎计算时需要),方法出口等等。

执行引擎每调用一个函数时,就为这个函数创建一个栈帧,并加入虚拟机栈。换个角度理解,每个函数从调用到执行结束,其实是对应一个栈帧的入栈和出栈。

注意这个区域可能出现的两种异常:

一种是StackOverflowError,当前线程请求的栈深度大于虚拟机所允许的深度时,会抛出这个异常。制造这种异常很简单:将一个函数反复递归自己,最终会出现栈溢出错误(StackOverflowError)。

另一种异常是OutOfMemoryError异常,当虚拟机栈可以动态扩展时(当前大部分虚拟机都可以),如果无法申请足够多的内存就会抛出OutOfMemoryError,如何制作虚拟机栈OOM呢,参考一下代码:

这段代码有风险,可能会导致操作系统假死,请谨慎使用~~~

1.3
本地方法栈
本地方法栈与虚拟机所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

1.4
Java堆
Java堆可以说是虚拟机中最大一块内存了。它是所有线程所共享的内存区域,几乎所有的实例对象都是在这块区域中存放。当然,随着JIT编译器的发展,所有对象在堆上分配渐渐变得不那么“绝对”了。

Java堆是垃圾收集器管理的主要区域。由于现在的收集器基本上采用的都是分代收集算法,所有Java堆可以细分为:新生代和老年代。在细致分就是把新生代分为:Eden空间、From Survivor空间、To Survivor空间。当堆无法再扩展时,会抛出OutOfMemoryError异常。

1.5
方法区
方法区存放的是类信息、常量、静态变量等。方法区是各个线程共享区域,很容易理解,我们在写Java代码时,每个线程度可以访问同一个类的静态变量对象。由于使用反射机制的原因,虚拟机很难推测那个类信息不再使用,因此这块区域的回收很难。另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了。同样,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。

制造方法区内存溢出,注意,必须在JDK1.6及之前版本才会导致方法区溢出,原因后面解释,执行之前,可以把虚拟机的参数-XXpermSize和-XX:MaxPermSize限制方法区大小。

运行后会抛出java.lang.OutOfMemoryError:PermGen space异常。

解释一下,String的intern()函数作用是如果当前的字符串在常量池中不存在,则放入到常量池中。上面的代码不断将字符串添加到常量池,最终肯定会导致内存不足,抛出方法区的OOM。

下面解释一下,为什么必须将上面的代码在JDK1.6之前运行。我们前面提到,JDK1.7后,把常量池放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码:

这段代码在JDK1.6和JDK1.7运行的结果不同。

JDK1.6结果是:false,false ,JDK1.7结果是true, false。

原因是:JDK1.6中,intern()方法会吧首次遇到的字符串实例复制到常量池中,返回的也是常量池中的字符串的引用,而StringBuilder创建的字符串实例是在堆上面,所以必然不是同一个引用,返回false。

在JDK1.7中,intern不再复制实例,常量池中只保存首次出现的实例的引用,因此intern()返回的引用和由StringBuilder创建的字符串实例是同一个。为什么对str2比较返回的是false呢?这是因为,JVM中内部在加载类的时候,就已经有"java"这个字符串,不符合“首次出现”的原则,因此返回false。
参考技术A JVM主要就是为java程序提供一个运行环境,包括类的加载,内存的分配,垃圾的回收,JVM将内存划分为堆,虚拟机栈,线程计数器,本地方法栈,方法区五个内存区域。

为了满足java程序运行时的垃圾回收,jvm提供了一些垃圾回收器用于堆内存的回收,常用的垃圾收集器包括ParNew新生代垃圾收集器,cms老年代垃圾收集器,G1垃圾收集器,这些垃圾收集器根据年龄代对象的特点使用不同的垃圾回收算法,为了解决垃圾收集时GC停顿对于Java程序的影响,使用一些参数的配置尽量减少垃圾回收时的停顿。

比如ParNew新生代垃圾收集器采用复制收集算法,使用多线程收集,提高垃圾回收的效率,CMS采用分段收集,对于比较耗时的阶段允许用户线程并行,但随之而来的也会导致一些缺陷,比如浮动垃圾,cpu资源紧张,内存碎片的问题,对于这些问题,可以通过JVM调优去尽量避免,比如浮动垃圾则可以减小CMS垃圾回收的老年代内存阈值。

G1垃圾收集器则采用可控的GC停顿时间来进行垃圾回收,将内存划分为一个个小的region,逻辑上划分出年轻代和老年代,所以G1垃圾收集器的调优主要就是对于GC停顿时间的调优,太大可能会导致每次GC停顿时间太长,太小可能导致GC发生的太频繁。

对于JVM调优这个话题,我们主要要保证减少YGC的次数,和尽量避免Full GC,因为对老年代的回收由于存活的对象比较多,回收是比较耗时的,那么对于这目标的实现,我们主要围绕一个思想来做,就是尽量保证每次回收后存活的对象可以存放在s区,这些都需要对程序有一个预测和平时的JVM观测
参考技术B JVM的理解可能就是一个非常好玩的一个游戏小孩玩的 参考技术C 是一套平台,里面有很多规范,按照规范形成的文件都可以在上面执行 参考技术D 这为俺们的理解,这应该是应该是一个。认为这个还非常棒啊,非常的。

Jvm(32),理解升级----(挺不错的)图解深入理解JVM之JVM内存区域与内存分配

解释了java中对象的在内存中的模型,学习了对象的内存模型后,对理解多态、参数传递等的理解都有帮助。

  前言:这是一篇关于JVM内存区域的文章,由网上一些有关这方面的文章和《深入理解 Java虚拟机》整理而来,所以会有些类同的地方,也不能保证我自己写的比其他网上的和书本上的要好,也不可能会这样。写博客的目的是为了个人对这方面自己理解的分享与个人的积累,所以有写错的地方多多指教。

  看到深入两字,相信很多的JAVA初学者都会直接忽略这样的文章,其实关于JVM内存区域的知识对于初学者来说其实是很重要的,了解Java内存分配的原理,这对于以后JAVA 的学习会有更深刻的理解,这是我个人的看法。

  先来看看JVM运行时候的内存区域

技术分享图片

  大多数 JVM 将内存区域划分为 Method Area(Non-Heap)(方法区),Heap(堆),Program Counter Register(程序计数器),VM Stack(虚拟机栈,也有翻译成JAVA 方法栈的),Native Method Stack (本地方法栈),其中Method

Area和Heap是线程共享的,VMStack,Native Method Stack 和Program Counter

Register是非线程共享的。为什么分为线程共享和非线程共享的呢?请继续往下看。

  首先我们熟悉一下一个一般性的 Java 程序的工作过程。一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),每个java程序都需要运行在自己的JVM上,然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。那么程序开始运行后,都是如何涉及到各内存区域的呢?

  概括地说来,JVM初始运行的时候都会分配好Method Area(方法区)和

Heap(堆),而JVM 每遇到一个线程,就为其分配一个Program Counter

Register(程序计数器),VM Stack(虚拟机栈)和Native Method Stack (本地方法

栈),当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。这也是为什么我把内存区域分为线程共享和非线程共享的原因,非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)的原因。

1.  程序计数器

  程序计数器是一块较小的内存区域,作用可以看做是当前线程执行的字节码的位置指示器。分支、循环、跳转、异常处理和线程恢复等基础功能都需要依赖这个计算器来完成,不多说。

2.VM Strack

  先来了解下JAVA指令的构成:

  JAVA指令由操作码 (方法本身)和操作数(方法内部变量) 组成。

  1. 方法本身是指令的操作码部分,保存在Stack中;
  2. 方法内部变量(局部变量)作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型(int,byte,short 等)保存在Stack中,对象类型在

Stack中保存地址,在Heap 中保存值);

  虚拟机栈也叫栈内存,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就 Over,所以不存在垃圾回收。也有一些资料翻译成JAVA方法栈,大概是因为它所描述的是java方法执行的内存模型,每个方法执行的同时创建帧栈(Strack Frame)用于存储局部变量表

(包含了对应的方法参数和局部变量),操作栈(Operand Stack,记录出栈、入栈的操作),动态链接、方法出口等信息,每个方法被调用直到执行完毕的过程,对应这帧栈在虚拟机栈的入栈和出栈的过程。

  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、 int、float、long、double)、对象的引用(reference类型,不等同于对象本身,根据不同的虚拟机实现,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或者其他与对象相关的位置)和 returnAdress类型(指向下一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,在方法在运行之前,该局部变量表所需要的内存空间是固定的,运行期间也不会改变。

  栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,执行完毕后,先弹出 F2栈帧,再弹出 F1 栈帧,遵循"先进后出"原则。光说比较枯燥,我们看一个图来理解一下 Java栈,如下图所示:

技术分享图片

3.Heap

  Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,是被所有线程共享的内存区域,在JVM启动时候创建,专门用来保存对象的实例。在Heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(以帧栈的形式保存在Stack中),在Heap 中分配一定的内存保存对象实

例。而对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例,是垃圾回收的主要场所。

java堆处于物理不连续的内存空间中,只要逻辑上连续即可。

4.Method Area

  Object Class Data(加载类的类定义数据) 是存储在方法区的。除此之外,常量、静态变量、JIT(即时编译器)编译后的代码也都在方法区。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap。方法区也可以是内存不连续的区域组成的,并且可设置为固定大小,也可以设置为可扩展的,这点与堆一样。

  垃圾回收在这个区域会比较少出现,这个区域内存回收的目的主要针对常量池的回收和类的卸载。

5.运行时常量池(Runtime Constant Pool)

  方法区内部有一个非常重要的区域,叫做运行时常量池(Runtime Constant Pool,简称 RCP)。在字节码文件(Class文件)中,除了有类的版本、字段、方法、接口等先关信息描述外,还有常量池(Constant Pool Table)信息,用于存储编译器产生的字面量和符号引用。这部分内容在类被加载后,都会存储到方法区中的RCP。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如 String 类中的 intern() 方法产生的常量。

  常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用.例如:

◆类和接口的全限定名;

◆字段的名称和描述符;

◆方法和名称和描述符。

  池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用.

   

6.Native Method Stack

与VM Strack相似,VM Strack为JVM提供执行JAVA方法的服务,Native Method Stack 则为JVM提供使用native 方法的服务。

7.直接内存区

  直接内存区并不是 JVM 管理的内存区域的一部分,而是其之外的。该区域也会在 Java 开发中使用到,并且存在导致内存溢出的隐患。如果你对 NIO 有所了解,可能会知道 NIO 是可以使用 Native Methods 来使用直接内存区的。

小结:

技术分享图片  在此,你对JVM的内存区域有了一定的理解,JVM内存区域可以分为线程共享和非线程共享两部分,线程共享的有堆和方法区,非线程共享的有虚拟机栈,本地方法栈和程序计数器。

8.JVM运行原理 例子

以上都是纯理论,我们举个例子来说明 JVM 的运行原理,我们来写一个简单的类,代码如下:

public class JVMShowcase {

//静态类常量,

public final static String ClASS_CONST = "I‘m a Const"?

//私有实例变量

private int instanceVar=15?

public static void main(String[] args) {

//调用静态方法

runStaticMethod()?

//调用非静态方法

JVMShowcase showcase=new JVMShowcase()? showcase.runNonStaticMethod(100)?

}

//常规静态方法

public static String runStaticMethod(){ return ClASS_CONST?

}

//非静态方法

public int runNonStaticMethod(int parameter){ int methodVar=this.instanceVar * parameter? return methodVar?

}

个类没有任何意义,不用猜测这个类是做什么用,只是写一个比较典型的类,然后我们来看

看 JVM 是如何运行的,也就是输入 java JVMShow 后,我们来看 JVM 是如何处理的:

第 1 步 、向操作系统申请空闲内存。JVM 对操作系统说"给我 64M(随便模拟数据,并不是真实数据) 空闲内存",于是,JVM 向操作系统申请空闲内存作系统就查找自己的内存分配表,找了段 64M 的内存写上"Java 占用"标签,然后把内存段的起始地址和终止地址给 JVM,JVM 准备加载类文件。

第 2 步,分配内存内存。JVM 分配内存。JVM 获得到 64M 内存,就开始得瑟了,首先给 heap 分个内存,然后给栈内存也分配好。

第 3 步,文件检查和分析class 文件。若发现有错误即返回错误。

第 4 步,加载类。加载类。由于没有指定加载器,JVM 默认使用 bootstrap 加载器,就把 rt.jar 下的所有类都加载到了堆类存的Method Area,JVMShow 也被加载到内存中。我

们来看看Method Area区域,如下图:(这时候包含了 main 方法和 runStaticMethod 方法的符号引用,因为它们都是静态方法,在类加载的时候就会加载

技术分享图片

Heap 是空,Stack 是空,因为还没有对象的新建和线程被执行。

第 5 步、执行方法。执行 main 方法。执行启动一个线程,开始执行 main 方法,在 main 执行完毕前,方法区如下图所示:

(public final static String ClASS_CONST = "I‘m a Const"; )

技术分享图片

在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被访问时产生的(runStaticMethod方法内部)。

堆内存中有两个对象 object 和 showcase 对象,如下图所示:(执行了

JVMShowcase showcase=new JVMShowcase(); )

技术分享图片

为什么会有 Object 对象呢?是因为它是 JVMShowcase 的父类,JVM 是先初始化父类,然后再初始化子类,甭管有多少个父类都初始化。

在栈内存中有三个栈帧,如下图所示:

技术分享图片

于此同时,还创建了一个程序计数器指向下一条要执行的语句。

第 6 步,释放内存。释放内存。运行结束,JVM 向操作系统发送消息,说"内存用完了,我还给你",运行结束。

--------------------------------------------------------------------------------------------

预备知识:

1.一个Java文件,只要有main入口方法,我们就认为这是一个Java程序,可以单独编译运行。

2.无论是普通类型的变量还是引用类型的变量(俗称实例),都可以作为局部变量,他们都可以出现在栈中。只不过普通类型的变量在栈中直接保存它所对应的值,而引用类型的变量保存的是一个指向堆区的指针,通过这个指针,就可以找到这个实例在堆区对应的对象。因此,普通类型变量只在栈区占用一块内存,而引用类型变量要在栈区和堆区各占一块内存。

示例:(以下所有实例中,是根据需要对于栈内存中的帧栈简化成了只有局部变量表,实际上由上面对帧栈的介绍知道不仅仅只有这些信息,同理堆内存也一样)

技术分享图片

1.JVM自动寻找main方法,执行第一句代码,创建一个Test类的实例,在栈中分配一块内存,存放一个指向堆区对象的指针110925。

2.创建一个int型的变量date,由于是基本类型,直接在栈中存放date对应的值9。

3.创建两个BirthDate类的实例d1、d2,在栈中分别存放了对应的指针指向各自的对象。他们在实例化时调用了有参数的构造方法,因此对象中有自定义初始值。

技术分享图片

调用test对象的change1方法,并且以date为参数。JVM读到这段代码时,检测到i是局部变量,因此会把i放在栈中,并且把date的值赋给i。

技术分享图片

把1234赋给i。很简单的一步。

技术分享图片

change1方法执行完毕,立即释放局部变量i所占用的栈空间。

技术分享图片

调用test对象的change2方法,以实例d1为参数。JVM检测到change2方法中的b参数为局部变量,立即加入到栈中,由于是引用类型的变量,所以b中保存的是d1中的指针,此时b 和d1指向同一个堆中的对象。在b和d1之间传递是指针。

技术分享图片

change2方法中又实例化了一个BirthDate对象,并且赋给b。在内部执行过程是:在堆区 new了一个对象,并且把该对象的指针保存在栈中的b对应空间,此时实例b不再指向实例 d1所指向的对象,但是实例d1所指向的对象并无变化,这样无法对d1造成任何影响。

技术分享图片

change2方法执行完毕,立即释放局部引用变量b所占的栈空间,注意只是释放了栈空间,堆空间要等待自动回收。

技术分享图片

调用test实例的change3方法,以实例d2为参数。同理,JVM会在栈中为局部引用变量b分

配空间,并且把d2中的指针存放在b中,此时d2和b指向同一个对象。再调用实例b的 setDay方法,其实就是调用d2指向的对象的setDay方法。

技术分享图片

调用实例b的setDay方法会影响d2,因为二者指向的是同一个对象。

技术分享图片

change3方法执行完毕,立即释放局部引用变量b。

以上就是Java程序运行时内存分配的大致情况。其实也没什么,掌握了思想就很简单了。无非就是两种类型的变量:基本类型和引用类型。二者作为局部变量,都放在栈中,基本类型直接在栈中保存值,引用类型只保存一个指向堆区的指针,真正的对象在堆里。作为参数时基本类型就直接传值,引用类型传指针。

小结:

1.分清什么是实例什么是对象。Class a= new Class();此时a叫实例,而不能说a是对象。实例在栈中,对象在堆中,操作实例实际上是通过实例的指针间接操作对象。多个实例可以指向同一个对象。

2.栈中的数据和堆中的数据销毁并不是同步的。方法一旦结束,栈中的局部变量立即销毁,但是堆中对象不一定销毁。因为可能有其他变量也指向了这个对象,直到栈中没有变量指向堆中的对象时,它才销毁,而且还不是马上销毁,要等垃圾回收扫描时才可以被销毁。

3.以上的栈、堆、代码段、数据段等等都是相对于应用程序而言的。每一个应用程序都对应唯一的一个JVM实例,每一个JVM实例都有自己的内存区域,互不影响。并且这些内存区域是所有线程共享的。这里提到的栈和堆都是整体上的概念,这些堆栈还可以细分。

4.类的成员变量在不同对象中各不相同,都有自己的存储空间(成员变量在堆中的对象中)。而类的方法却是该类的所有对象共享的,只有一套,对象使用方法的时候方法才被压入栈,方法不使用则不占用内存。

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

jvm的理解

JVM简单理解,全局观理解

JVM简单理解,全局观理解

JVM简单理解,全局观理解

JVM简单理解,全局观理解

深入理解JVM虚拟机6:深入理解JVM类加载机制