JVM(四)(虚拟机)的组成简介

JVM(四)(虚拟机)的组成简介

 

 

 

 1、简介

JVM 由  类加载器、运行时数据区 执行引擎、 本地接口组成。本文重点介绍运行时数据区

运行时数据区(JVM内存):

  • 堆:存放对象实例
  • 栈:存放局部变量表,方法出口
  • 方法区:类信息,常量,静态变量,编译后的代码
  • 程序计数器:行号指示器。循环,跳转等基础指令
  • 本地方法栈:VM调用native方法

执行引擎:

  • gc
  • 解释器

JDK1.8以前:

JDK1.8

1、Class Loader 类加载器
  类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java 程序,然后通过javac 编译成class 文件,那怎么才能加载到内存中被执行呢?Class Loader 承担的就是这个责任,那不可能随便建立一个.class 文件就能被加载的,Class Loader 加载的class 文件是有格式要求(后面会讲到)。
Class Loader 只管加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由Execution Engine 负责的。

2、 Runtime data area 运行数据区
  运行数据区是整个JVM 的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java 生态系统如此的繁荣,得益于该区域的优良自治。

3、Execution Engine 执行引擎
  执行引擎也叫做解释器(Interpreter) ,负责解释命令,提交操作系统执行。

4、Native Interface 本地接口
  本地接口的作用是融合不同的编程语言为Java 所用,它的初衷是融合C/C++ 程序,Java 诞生的时候是C/C++ 横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++ 程序,于是就在内存中专门开辟了一块区域处理标记为native 的代码,它的具体做法是Native Method Stack 中登记native 方法,在Execution Engine 执行时加载native libraies 。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java 程序驱动打印机,或者Java 系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket 通信,也可以使用Web Service 等等,不多做介绍。

 

2、JVM的内存管理

所有的数据和程序都是在运行数据区存放,它包括以下几部分:

2.1、  Stack 栈

栈也叫栈内存,是Java程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生命期线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就Over。问题出来了:栈中存的是那些数据呢?又什么是格式呢?

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循栈“先进后出”原则。8种基本类型的变量+对象的引用变量+实例方法都是在栈里面分配内存。

栈帧中主要保存3类数据:

本地变量(Local Variables),包括输入参数和输出参数以及方法内的变量;栈操作(Operand Stack),记录出栈、入栈的操作;栈帧数据(Frame Data),包括类文件、方法等等。

栈异常:如果线程请求的栈的深度大于虚拟机栈的最大深度,就会报 StackOverflowError (这种错误经常出现在递归中)。Java虚拟机也可以动态扩展,但随着扩展会不断地申请内存,当无法申请足够内存时就会报错 OutOfMemoryError

 

2.2、Heap 堆内存

       jvm中线程共享的区域又分为方法区又进一步分为新生代和老年代,方法区为永久代。

堆中区分的新生代和老年代是为了垃圾回收,新生代中的对象存活期一般不长,而老年代中的对象存活期较长,所以当垃圾回收器回收内存时,新生代中垃圾回收效果较好,会回收大量的内存而老年代中回收效果较差,内存回收不会太多。

基于以上特性,新生代中一般采用复制算法,因为存活下来的对象是少数,所需要复制的对象少,而老年代对象存活多,不适合采用复制算法,一般是标记整理和标记清除算法

因为复制算法需要留出一块单独的内存空间来以备垃圾回收时复制对象使用,所以将新生代分为eden区两个survivor区,每次使用eden一个survivor区,另一个survivor作为备用的对象复制内存区。

一个JVM实例只存在一个堆类存,堆内存的大小是可以调节的。Eden,survivor0 和 survivor1 的默认占比为 8:1:1。当然这个东西其实也可以通过一个 -XX:+UsePSAdaptiveSurvivorSizePolicy 参数来根据生成对象的速率动态调整。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。

堆内存分为三部分:

  • Permanent Space 永久存储区

     永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

堆内存中存放的是对象,垃圾收集就是收集这些对象然后交给GC算法进行回收。非堆内存其实我们已经说过了,就是方法区。在1.8中已经移除永久代,替代品是一个元空间(MetaSpace),最大区别是metaSpace是不存在于JVM中的,它使用的是本地内存。并有两个参数:

MetaspaceSize:初始化元空间大小,控制发生GC。
MaxMetaspaceSize:限制元空间大小上限,防止占用过多物理内存。
  • Young Generation Space 新生区

     新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。当Eden空间满了之后,会触发一个叫做Minor GC(就是一个发生在年轻代的GC)的操作,存活下来的对象移动到Survivor0区。Survivor0区满后又会触发 Minor GC,就会将存活对象移动到Survivor1区,此时还会把from和to两个指针交换,这样保证了一段时间内总有一个survivor区为空且to所指向的survivor区为空。经过多次的 Minor GC后仍然存活的对象(这里的存活判断是15次,对应到虚拟机参数为 -XX:MaxTenuringThreshold 。为什么是15,因为HotSpot会在对象头中的标记字段里记录年龄,分配到的空间仅有4位,所以最多只能记录到15)会移动到老年代。老年代是存储长期存活的对象的,占满时就会触发我们最常听说的 Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。

而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xmx来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。

补充说明:关于-XX:TargetSurvivorRatio参数的问题。其实也不一定是要满足-XX:MaxTenuringThreshold才移动到老年代。可以举个例子:如对象年龄5的占30%,年龄6的占36%,年龄7的占34%,加入某个年龄段(如例子中的年龄6)后,总占用超过Survivor空间*TargetSurvivorRatio的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即例子中的年龄6对象,就是年龄6和年龄7晋升到老年代),这时候无需等到MaxTenuringThreshold中要求的15。

  • Tenure generation space养老区

 养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。   三个区的示意图如下:

2.3、 Method Area 方法区

方法区是被所有线程共享,该区域保存所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。

2.4、PC Register 程序计数器

每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令。

 2.5、本地方法栈

本地方法栈(Native Method Stacks)作用和虚拟机栈类似,虚拟机栈执行的是Java方法,本地方法栈执行的是 Native 方法,本地方法栈也会抛出抛出 StackOverflowError 和 OutOfMemoryError 异常。

 

 

3、JVM垃圾回收(GC)

  GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

  • (1)对新生代的对象的收集称为minor GC
  • (2)对旧生代的对象的收集称为Full GC
  • (3)程序中主动调用System.gc()强制执行的GC为Full GC。
(1) MinorGC的过程:MinorGC采用复制算法,首先,把Eden区域和使用的幸存者区域(SurvivorFrom)中存活的对象复制到另一块空闲的幸存者区(SurvivorTo)中,同时,把这些对象的年龄+1,如果有对象的年龄已经达到了老年代的标准,则赋值到老年代区,如果空闲的幸存者区(SurvivorTo)不够存放Eden和使用的幸存者区(SurvivorFrom)移动过来的数据,则直接放到老年代,然后,清空Eden和使用的幸存者区中(SurvivorFrom)的对象,然后,幸存者区互换,SurvivorTo中的数据换到SurvivorFrom中,SurvivorFrom继续等待下一次GC,Survivor区每熬过一次MinorGC,就将对象的年龄+1,当对象的年龄到达某个值时(默认时15,可用通过参数-XX:MaxTenuringThreshold 来设定),这些对象就会成为老年代。
 
(2) FullGC的过程:老年代的回收,不会那么频繁,老年代使用的回收算法是标记-清除或标记-整理算法,标记-清除算法会产生内存碎片,即不连续的空间,如果此时,有大的对象进来,内存中没有足够的连续空间时,会提前触发FullGC(这是一个优化点),一次FullGC的时间要比一次MinorGC的时间长,当年老代也装不下,就会抛出OOM(Out Of Memory)异常  (3) Full GC的触发:老年代满了而无法容纳更多的对象,会触发Full GC,Full GC 清理整个内存堆,包括年轻代和老年代

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

  • (1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
  • (2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
  • (3)弱引用:在GC时一定会被GC回收
  • (4)虚引用:由于虚引用只是用来得知对象是否被GC

     程序员可以调用 System.gc()方法,手动回收,但是调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
  其次虚拟机会自行根据当前内存大小,判断何时进行垃圾回收,比如前面所说的,新生代满了,新产生的对象无法分配内存时,便会触发垃圾回收机制。
Java技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前被自动调用的。
垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize()的方法,它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作(如关闭流等操作)。但JVM(Java虚拟机)不保证此方法总被调用。Finalize 可以采取任何操作,包括在垃圾回收过程中清理了对象后使对象复活(即,使对象再次可访问)。但是,对象只能复活一次;在垃圾回收过程中,不能对复活对象调用 Finalize。JDK9中已被废弃。

finalize() 与 finally{...} 的区别: 

  • finalize() :  不能保证一定执行、何时执行。
  • finally{...} :  用于释放资源,最典型的就是java.io.InputStream 类的 close() 方法。无论怎样都会执行(宕机除外)。

对象的可触及性:

 

 举例说明:

public static void method () {
  Integer a1 = new Integer(10); // 1
  Integer a2 = new Integer(20); // 2
  a1 = a2;   // 3
}

public static void main(String[] args){
 method();
 System.out.printIn("End);
}

当 程序执行完 第 3 步时, 取值为 10 的 Integer 对象不再被任何变量所引用, 因此转到可复活状态, 取值为 20 的Integer 对象处于可触及状态,它被变量 a1 和 a2 所引用。当程序退出 method() 方法并返回到 main() 方法时, 在method() 中定义的局部变量 a1 和 a2 都将结束生命周期。

 

 

4、JVM相关问题

问:堆和栈有什么区别

答:堆是存放对象的,但是对象内的临时变量是存在栈内存中,栈是跟随线程的,有线程就有栈,堆是跟随JVM的,有JVM就有堆内存。

 

问:堆内存中到底存在着什么东西?

答:对象,包括对象变量以及对象方法。

 

问:类变量和实例变量有什么区别?

答:静态变量是类变量,非静态变量是实例变量,直白的说,有static修饰的变量是静态变量,没有static修饰的变量是实例变量。静态变量存在方法区中,实例变量存在堆内存中。

 

问:Java的方法(函数)到底是传值还是传址?

答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的地址。对于原始数据类型,JVM的处理方法是从Method Area或Heap中拷贝到Stack,然后运行frame中的方法,运行完毕后再把变量指拷贝回去。

 

问:为什么会产生OutOfMemory产生?

答:一句话:Heap内存中没有足够的可用内存了。这句话要好好理解,不是说Heap没有内存了,是说新申请内存的对象大于Heap空闲内存,比如现在Heap还空闲1M,但是新申请的内存需要1.1M,于是就会报OutOfMemory了,可能以后的对象申请的内存都只要0.9M,于是就只出现一次OutOfMemory,GC也正常了,看起来像偶发事件,就是这么回事。       但如果此时GC没有回收就会产生挂起情况,系统不响应了。

 

问:我产生的对象不多呀,为什么还会产生OutOfMemory

答:你继承层次忒多了,Heap中 产生的对象是先产生 父类,然后才产生子类,明白不?

 

问:OutOfMemory错误分几种?

答:分两种,分别是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen space”,两种都是内存溢出,heap size是说申请不到新的内存了,这个很常见,检查应用或调整堆内存大小。

“PermGen space”是因为永久存储区满了,这个也很常见,一般在热发布的环境中出现,是因为每次发布应用系统都不重启,久而久之永久存储区中的死对象太多导致新对象无法申请内存,一般重新启动一下即可。

 

问:为什么会产生StackOverflowError

答:因为一个线程把Stack内存全部耗尽了,一般是递归函数造成的。

 

问:一个机器上可以看多个JVM吗?JVM之间可以互访吗?

答:可以多个JVM,只要机器承受得了。JVM之间是不可以互访,你不能在A-JVM中访问B-JVM的Heap内存,这是不可能的。在以前老版本的JVM中,会出现A-JVM Crack后影响到B-JVM,现在版本非常少见。

 

问:为什么Java要采用垃圾回收机制,而不采用C/C++的显式内存管理?

答:为了简单,内存管理不是每个程序员都能折腾好的。

 

问:为什么你没有详细介绍垃圾回收机制?

答:垃圾回收机制每个JVM都不同,JVM Specification只是定义了要自动释放内存,也就是说它只定义了垃圾回收的抽象方法,具体怎么实现各个厂商都不同,算法各异,这东西实在没必要深入。

 

问:JVM中到底哪些区域是共享的?哪些是私有的?

答:Heap和Method Area是共享的,其他都是私有的,

 

问:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?

程序计数器为什么是私有的?
   程序计数器主要有下面两个作用:

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

 

posted @ 2021-02-02 18:07  邓维-java  阅读(325)  评论(0)    收藏  举报