赞
踩
目录
2.2.3 程序计数器(Program Counter Register)
2.2.5 本地方法栈(Native Method Stacks)
3.2.3 永久代(Permanent Generationn)
在装有jdk电脑里面,在dos窗口里面的输入:java -version
HotSpot(TM):虚拟机将class文件加载到虚拟机中,需要把class文件编译成native code(本地代码),本地代码就是虚拟机能直接运行。热点探测,是指虚拟机加载class文件,会根据加载的文件做标记,在做标记过程中,当达到了一定的阈值,会触发一个即时编译机制,对频繁使用的class文件直接编译成本地代码缓存起来,而不再进行编译了。热点探测是在jdk1.3之后,但是真正加进去是在1.5了。
Server:是虚拟机的模式 ,有两种:client和server模式。client是为桌面级应用而提供的,使用的是一个代号为 C1 的轻量级编译器,server启动的虚拟机采用相对重量级,代号为 C2 的编译器. C2 比 C1 编译器编译的相对彻底,服务起来之后,性能更高
可以自己修改jvm模式,路径:JDK 安装目录/jre/lib/(x86、 i386、 amd32、 amd64) /jvm.cfg
-server 和-client 哪一个配置在上,执行引擎就是哪一个。 如果是 JDK1.5版本且是 64 位系统应用时, -client 无效
--64 位系统内容 -server KNOWN -client IGNORE
--32 位系统内容 -server KNOWN -client KNOWN
注意:在部分 JDK1.6 版本和后续的 JDK 版本(64 位系统)中, -client 参数已经不起作用了, Server 模式成为唯一
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、 Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
JVM8结构示意图:注意图中线程私有和线程共享
Java虚拟机在执行的时候会把管理的内存分配成不同的区域,这些区域被称为虚拟机内存,同时,对于虚拟机没有直接管理的物理内存,也有一定的利用,这些被利用却不在虚拟机内存数据区的内存,我们称它为本地内存,这两种内存有一定的区别:
JVM内存(运行时数据区域)中的JVM内存又分两种:数据区和运算区,如下图所示
程序计数器(Program Counter Register),也有称作为PC寄存器,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令
位于JVM内存运算区,程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它来实现跳转、循环、恢复线程等功能。在任何时刻,一个处理器内核只能运行一个线程,多线程是通过线程轮流切换,分配时间来完成的,这就需要有一个标志来记住每个线程执行到了哪里,这里便需要到了程序计数器。
程序计数器是线程私有的,每个线程都有自己的程序计数器。生命周期与线程一致,生命周期随着线程启动而产生,随线程结束而消亡
唯一 一个没有OOM的区域
PC(Program Counter) 寄存器是每一个线程私有的空间, java 虚拟机会为每一个 java线程创建 PC 寄存器。在任意时刻,一个 java 线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法, PC 寄存器就会指向当前正在被执行的指令。 如果当前方法是本地方法,那么 PC 寄存器的值就是 undefined
Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型
每当启动给一个线程时,Java虚拟机会为它分配一个Java栈。Java栈由许多栈帧组成,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法返回时,这个栈帧就从Java栈中弹出。Java栈存储线程中Java方法调用的状态–包括局部变量、参数、返回值以及运算的中间结果等。Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑,同时也便于Java虚拟机在只有很少通用寄存器的平台上实现。另外,基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。
Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么在使用递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个Java栈的模型
Java栈的模型介绍:
就是用来存储方法中的局部变量(方法参数
和局部变量
)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
我们都知道,类属性变量一共要经历两个阶段,分为准备阶段
和初始化阶段
,而局部变量是没有准备阶段,只有初始化阶段
,而且必须是显示
的。如果是非静态方法,则在index[0]位置上存储的是方法所属对象的实例引用
,随后存储的是参数
和局部变量
。字节码指令中的STORE指令就是将操作栈中计算完成的局部变量写回局部变量表的存储空间内
初始状态为空的桶式结构栈
,以字长为单位的数组。在方法执行过程中,会有各种指令往栈中写入和提取信息。JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈
。字节码指令集的定义都是基于栈类型
的在常量池中对当前方法的引用
,目的是支持方法调用过程的动态连接
方法执行时有两种退出情况:第一,正常退出,即正常执行到任何方法的返回字节码指令
,如 RETURN
、IRETURN
、ARETURN
等;第二,异常退出。无论何种退出情况,都将返回方法当前被调用的位置
。方法退出的过程相当于弹出当前栈帧,
退出可能有三种方式:返回值压入上层调用栈帧,异常信息抛给能够处理的栈帧和PC 计数器指向方法调用后的下一条指令
虚拟机栈特点:
虚拟机栈可能会抛出两种异常:
本地方法栈与虚拟机栈的作用是相似的,都会抛出OutOfMemoryError和StackOverFlowError,都是线程私有的,主要的区别在于:虚拟机栈执行的是java方法,本地方法栈执行的是native方法
本地方法栈用于本地方法(native method:非java语言代码)的调用,作为对 java 虚拟机的重要扩展, java 虚拟机允许 java 直接调用本地方法(通常使用 C 编写)
Java本地接口,也叫JNI(Java Native Interface),是为可移植性准备的。本地方法接口允许本地方法完成以下工作:
java堆是Java虚拟机运行时数据区共享数据区最大的区域,java 堆在虚拟机启动的时候建立,堆的生命周期与JVM进程一致,由所有线程共享,是由垃圾收集器管理的内存区域,主要存放对象实例,当然由于java虚拟机的发展,堆中也多了许多东西,现在主要有:
java堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx和-Xms设定),如果堆无法扩展或者无法分配内存时也会报OutOfMemeoryError
存储速度慢于栈内存,灵活性优于栈内存
一个可能的堆设计如下:
一个句柄池,一个对象池。一个对象的引用就是一个指向句柄池的本地指针。这种设计的好处有利于堆碎片的整理,当移动对象池中的对象时,句柄部分只需更改一下指针指向对象的新地址即可。缺点是每次访问对象的实例变量都要经过两次指针传递
方法区绝对是网上所有关于java内存结构文章争论的焦点,因为方法区的实现在java8做了一次大革新,现在我们来讨论一下:
常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:
类和接口的全限定名;
字段的名称和描述符;
方法和名称和描述符
方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后内容有少许不同,不过总的来说分为这两部分:
字面量和符合引用的理解:
在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制,并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中
JDK6方法区:
JDK1.8方法区:
Java8中的JVM元空间是不是方法区?
不是的
应该说:元空间是方法区的一种具体实现,可以把方法区理解为Java中定义的一个接口,把元空间/永久代看做这个接口的具体实现类,其中方法区是规范,元空间/永久代是Hotspot针对该规范进行的实现。
元空间这个东西,是在JDK8以后才存在的,JDK7及以前,只有永久代这个区域,元空间的存储位置是在计算机的内存当中,而永久代的存储位置是在JVM的堆(Heap)中
在JVM规范中,方法区被定义为一种逻辑区域,而方法区具体怎么实现是各JVM的实现细节,所以方法区的内容在堆里也好,不在堆里也好都是符合标准的。
jdk1.4中加入了NIO(New Input/Putput)类,引入了一种基于通道(channel)与缓冲区(buffer)的新IO方式,它可以使用native函数直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高IO性能,避免了在java堆和native堆来回复制数据
java 的 NIO 库允许 java 程序使用直接内存。直接内存是在 java 堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于 java 堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在 java 堆外,因此它的大小不会直接受限于 Xmx (虚拟机参数)指定的最大堆大小,但是系统内存是有限的, java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。直接内存位于本地内存,不属于JVM内存,不受GC管理,但是也会在物理内存耗尽的时候报OOM。
注意:direct buffer不受GC影响
,但是direct buffer归属的JAVA
对象是在堆上且能够被GC
回收的,一旦它被回收,JVM
将释放direct buffer的堆外空间
执行引擎是 java 虚拟机的最核心组件之一,它负责执行虚拟机的字节码,有即时编译和解释执行,通常采用解释执行方式。解释执行是指解释器通过每次解释并执行一小段代码来完成.class程序的所有操作。即时编译则是将.class文件翻译成机器码在执行(比如:经常多次访问的代码可以全部编译)
作为运行时实例的执行引擎就是一个线程。运行中Java程序的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,它要么在执行字节码,要么执行本地方法。
执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能紧凑,便于快速地在网络上传输(别忘了Java最初就是为网络设计的),同时也很容易适应通用寄存器较少的平台,并且有利于代码优化,由于Java栈和PC寄存器是线程私有的,线程之间无法互相干涉彼此的栈。每个线程拥有独立的JVM执行引擎实例。
JVM指令由单字节操作码和若干操作数组成。对于需要操作数的指令,通常是先把操作数压入操作数栈,即使是对局部变量赋值,也会先入栈再赋值。
如下代码说明
- public class HeapMemory {
- private Object obj1 = new Object();
-
- public static void main(String[] args) {
- Object obj2 = new Object();
- }
- }
上面的代码中,obj1 和obj2在内存中有什么区别?
根据上面可知,方法区存储每个类的结构,比如:运行时常量池、属性和方法数据,以及方法和构造函数等数据。所以我们这个obj1是存在方法区的,而new会创建一个对象实例,对象实例是存储在堆内的,于是就有了下面这幅图(方法区指向堆)
而obj2 是属于方法内的局部变量,存储在Java虚拟机栈内的栈帧中的局部变量表内,这就是经典的栈指向堆
这里我们再来思考一下,我们一个变量指向了堆,而堆内只是存储了一个实例对象,那么堆内的示例对象是如何知道自己属于哪个Class,也就是说这个实例是如何知道自己所对应的类元信息的呢?这就涉及到了一个Java对象在内存中是如何布局的。
创建好一个对象之后,当然需要去访问它,那么当我们需要访问一个对象的时候,是如何定位到对象的呢?目前最主流的访问对象方式有两种:句柄访问和直接指针访问。
句柄访问
使用句柄访问的话,Java虚拟机会在堆内划分出一块内存来存储句柄池,那么对象当中存储的就是句柄地址,然后句柄池中才会存储对象实例数据和对象类型数据地址
直接指针访问(Hot Spot虚拟机采用的方式) 直接指针访问的话对象中就会直接存储对象实例数据
句柄访问和直接指针访问对比
上面图形中我们很容易对比,就是如果使用句柄访问的时候,会多了一次指针定位,但是他也有一个好处就是,假如一个对象被移动(地址改变了),那么只需要改变句柄池的指向就可以了,不需要修改reference对象内的指向,而如果使用直接指针访问,就还需要到局部变量表内修改reference指向
随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么绝对了。
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
逃逸分析(Escape Analysis),是一种可能减少有效 Java 程序中同步负载和内存堆分配压力的跨全局函数数据流分析算法。通过逃逸分析, Java Hotspot 编译器能够分析出一个新的对象引用范围从而决定是否要将这个对象分配到堆上,逃逸分析的基本行为就是分析对象的动态作用域。
逃逸分析是一种数据分析算法,基于此算法可以有效减少 Java 对象在堆内存中的分配。Hotspot 虚拟机的编译器能够分析出一个新对象的引用范围,然后决定是否要将这个对象分配到堆上。例如:
方法逃逸:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如调用参数传递到其他方法中,这种称为方法逃逸。
线程逃逸:当一个对象可能被外部线程访问到,比如:赋值给其他线程中访问的实例变量,这种称为线程逃逸。
通过逃逸分析,编译器对代码的优化
如果能够证明一个对象不会逃逸到到方法外或线程外(其他线程方法或者线程无法通过任何方法访问该变量),或者逃逸程度比较低(只逃逸出方法而不逃逸出线程)则可以对这个对象采用不同程度的优化:
对象逃逸是指当我们在某个方法里创建了一个对象,这个对象除了被这个方法引用,还在方法体之外被其它的变量引用。这样的后果是当这个方法执行完毕后,GC无法回收这个对象,就被称为对象逃逸了。逃逸的对象的内存在堆中,未逃逸的对象内存分配在栈中。
- public stringBuffer append(String apple,String pear){
- StringBuffer buffer=new StringBuffer();
- buffer.append(apple);
- buffer.append(pear);
- return buffer;
- }
这种写法直接返回的是对象,用处就是被别的变量所引用,会造成对象逃逸,从而增加了GC的压力,引发STW(stop the world)现象,不推荐这样写。我们可以做修改如下代码:
- public string append(String apple,String pear){
- StringBuffer buffer=new StringBuffer();
- buffer.append(apple);
- buffer.append(pear);
- return buffer.toString();
- }
这种写法就避免了对象逃逸,从而减小了GC的压力。下面这种写法就是在方法内创建了一个对象,没有被外界方法所引用,称为未逃逸对象
整个堆的大小 = 年轻代大小 + 年老代大小,堆的大小不包含元空间大小,如果增大了年轻代,年老代相应就会减小,官方默认的配置为年老代大小/年轻代大小=2/1 左右
Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代(对 HotSpot 虚拟机而言),新版本中老年代不在堆范畴内,这就是JVM 的内存分代策略。
堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。
试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的 GC 效率。
有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中, 静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行 GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。
Java 虚拟机将堆内存划分为新生代、老年代和永久代,永久代是 HotSpot 虚拟机特有的概念(JDK1.8 之后为 metaspace 元空间替代永久代),它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且 HotSpot 也有取消永久代的趋势,在 JDK 1.7 中 HotSpot 已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
堆内存简图如下:
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70% ~ 95% 的空间,回收效率很高。
HotSpot 将新生代划分为三块,一块较大的 Eden(伊甸) 空间和两块较小的 Survivor(幸存者) 空间,默认比例为 8: 1: 1。划分的目的是因为 HotSpot 采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在 Eden 区分配(大对象除外,大对象直接进入老年代),当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
GC 开始时,对象只会存在于 Eden 区和 From Survivor 区, To Survivor 区是空的(作为保留区域)。 GC 进行时, Eden 区中所有存活的对象都会被复制到 To Survivor 区,而在 FromSurvivor 区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加 1, GC 分代年龄存储在对象的 header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到 To Survivor 区。
接着清空Eden 区和 From Survivor 区,新生代中存活的对象都在 To Survivor 区。接着, From Survivor区和 To Survivor 区会交换它们的角色,也就是新的 To Survivor 区就是上次 GC 清空的 FromSurvivor 区,新的 From Survivor 区就是上次 GC 的 To Survivor 区,总之,不管怎样都会保证To Survivor 区在一轮 GC 后是空的。 GC 时当 To Survivor 区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
Minor GC会一直重复这样的过程,直到“To”区被填满即Eden区存活的对象和from区存活的对象很多了,被复制到to区域时,to区域一下子接收装不下了,则“To”区被填满时,就不会再进行角色互换变成from了,而是“To”区被填满之后,会将所有对象移动到年老代中,则“To”区是空的了,即“To”区变成空有两种方式,一是对象从from移动到to区后,角色互换,为空的区域from变成to,to就变成空了的;二是Eden和from中未达到15岁的对象两者加起来太多,移动到to区填满了,则把填满了to区的对象移动到老年代,此时eden区和from区对象变少了,to区也没经过角色互换,变成空的
注意:假如说S0区或者S1区空间对象复制移动了之后还是放不下,那就说明这时候是真的满了,那就去老年区借点空间过来(这就是担保机制,老年代需要提供这种空间分配担保),假如说老年区空间也不够了,那就会触发Full GC,如果还是不够,那就会抛出OutOfMemeoyError异常了
在新生代中经历了多次(具体看虚拟机配置的阀值) GC 后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行 GC 的频率相对而言较低,而且回收的速度也比较慢。
默认情况下,年轻代与年老代的比例是1:2。这意味着年轻代占据整个堆内存的1/3,而年老代占据2/3。这个比例可以通过JVM参数-XX:NewRatio=<n>
进行调整。例如,设置-XX:NewRatio=2
将保持默认比例,而设置其他值会改变年轻代和年老代的相对大小
永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言, Java 虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。但是,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
在 Java8 中, 永久代已经被移除,被一个称为元数据区(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制
一个对象的人生轨迹图
从上面的介绍大家应该有一个大致的印象,一个对象会在Eden区,S0区,S1区,Old区不断流转(当然,一开始就会被回收的短命对象除外),我们可以得到下面的一个流程图:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。