Jvm是一个可运行java代码的假想计算机,Java 源文件,通过编译器,能够生产相应的.Class 文件(字节码文件),而字节码文件通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。虽然对于每一种平台解释器有所不同,但实现的虚拟机是相同的。就这是java语言能够跨平台的原因。
Hotspot
java发展至今,先后出现过不少java虚拟机,发展初期,Sun使用的是一款叫做Classic的java虚拟机,后来,还曾短暂使用过Exact VM虚拟机,到现在,被大规模部署和应用的就是Hotspot虚拟机。JVM和Hotspot关系就可以理解为:JVM是一种规范,而Hotspot则是具体实现。
垃圾回收系统
垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆
是垃圾回收器的工作重点。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括java堆、方法区和直接内存中的全自动化管理。开发人员也可以通过System.gc()来通知圾回收器回收垃圾,但具体回收不回收还是由垃圾收集器的算法决定。
执行引擎
执行引擎是java虚拟机的最核心组件之一,它负责执行虚拟机的字节码,现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。
程序计数器(线程私有)
程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的指来选取下一条需要执行的字节码指令,分支判断、异常处理、跳转、循环、线程恢复等都需要依赖这个计数器完成。
这个内存区域是唯是虚拟机唯一一个不会出现OutOfMemoryError的区域。
为什么是线程私有的?
1.为了线程切换后能恢复到正确的执行位置 2.为了代码的执行不会混乱,各线程的计数器之间应该互不影响,独立存储。
虚拟机栈(线程私有)
java虚拟机栈是由一个个栈帧组成,java中每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。即方法执行完弹出栈帧,弹出方式有return、异常。
局部变量表主要存放了编译器可知的各种数据类型(byte、short、int、char、double、float、long、boolean)、对象引用。
虚拟机栈的生命周期和线程相同。在线程内部,每个方法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活动状态。
java虚拟机栈常出现的两个异常:
StackOverFlowError:java虚拟机的大小不允许动态扩展,线程请求栈的深度超过当前虚拟机栈的最大深度时会报StackOverFlowError异常。
OutOfMemoryError:java虚拟机的大小允许动态扩展,当前线程请求栈时内存用完了,无法再进行动态扩展了,会报OutOfMemoryError异常。
本地方法栈(线程私有)
和虚拟机栈作用相似,只不过虚拟机栈是执行java方法的,即java字节码,本地方法栈执行的是虚拟机使用到的Native方法。
Native执行时和虚拟机栈一样,创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,执行完后栈帧弹出。释放内存空间。
本地方法栈也会报StackOverFlowError和OutOfMemoryError异常。
堆(线程共享)
堆是JVM内存中最大的一块内存空间,几乎所有的对象和数组都被分配在堆内存中(也有在栈中分配的对象,这个涉及到java逃逸分析)。
堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 区和 Survivor 区,Survivor 又由 From Survivor 和 To Survivor 组成。
方法区(永久代)(线程共享)
指内存的永久保存区域,主要已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
它和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
常量被存储在运行时常量池(Runtime Constant Pool)中,是方法区的一部分。静态变量也属于方法区的一部分。
类信息(Class文件)中不但保存了类的版本、字段、方法、接口等描述信息,还保存了常量信息。在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在方法区的运行时常量池中。Java虚拟机对Class文件每一部分的格式都有明确的规定,只有符合JVM规范的Class文件才能通过虚拟机的检查,然后被装载、执行。
在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在方法区的运行时常量池中。Java虚拟机对Class文件每一部分的格式都有明确的规定,只有符合JVM规范的Class文件才能通过虚拟机的检查,然后被装载、执行。
元空间
在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 nativememory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。由于Metaspace的分配具有和Java Heap相同的地址空间,因此Metaspace和Java Heap可以无缝的管理。
补充
public class A{
public static void main(String[] args){
B b = new B();
}
}
public class B{
int i;
C c;
B(){
i = 1;
c = new C();
}
}
public class C{
}
方法体中的引用变量和基本类型的变量都在栈上,其他都在堆上。
所以B对象里面所有东西都在堆上,main方法中的b变量在栈上。
对于局部变量,如果是基本类型,会把值直接存储在栈;如果是引用类型,比如String s = new String(“william”);会把其对象存储在堆,而把这个对象的引用(指针)存储在栈。
成员变量作为对象的属性,当然是放在堆里了。对象在堆里,对象中的内容就是各种字段。
只有方法执行的时候所用到的各种指令参数才会入栈出栈。
类的成员变量在不同对象中各不相同,都有自己的存储空间(成员变量在堆中的对象中),基本类型和引用类型的成员变量都在这个对象的空间中,作为一个整体存储在堆。而类的方法却是该类的所有对象共享的,只有一套,对象使用方法的时候方法才被压入栈,方法不使用则不占用内存。(对象实质上就是各种成员变量,不包括方法)
--引自https://blog.csdn.net/q503385724/article/details/87910929
为什么要用元空间(Metaspace)替代方法区
String.intern()
是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
public static void main(String[] args) {
String str = "Hello";
System.out.println((str == ("Hel" + "lo")));
String loStr = "lo";
System.out.println((str == ("Hel" + loStr)));
System.out.println(str == ("Hel" + loStr).intern());
}
其运行结果为:
true
false
true
第一个为 true,是因为在编译成 class 文件时,能够识别为同一字符串的, JVM 会将其自动优化成字符串常量,引用自同一 String 对象。
第二个为 false,是因为在运行时创建的字符串具有独立的内存地址,所以不引用自同一 String 对象。
第二个为 false,是因为在运行时创建的字符串具有独立的内存地址,所以不引用自同一 String 对象。
最后一个为 true,是因为 String 的 intern() 方法会查找在常量池中是否存在一个相等(调用 equals() 方法结果相等)的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但这部分也会被频繁使用,因为java的NIO库允许java程序使用直接内存。在 JDK 1.4 中新加入的 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作。这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能
由于直接内存在java堆外,所以它的大小不会直接受限于Xmx核定的最大堆大小,但是系统的内存是有限的,java堆和直接内存的总和依然受限于操作系统的最大内存。
Minor Gc:即新生代垃圾回收,对象一般在新生代eden去分配,当eden去没有足够的空间的时候就会发起一次Minor Gc,特点是频繁,回收速度较快。
Full Gc:又叫Major Gc,老年代的垃圾回收,速度慢大概是新生代gc的10倍,一般Major Gc会伴随这至少一次的Minor Gc,但不是绝对。
垃圾回收前第一步就是判断哪些对象已经死亡。
引用计数法
在java中,对象和引用是关联的,如果要操作某个对象,就需要使用到这个对象的引用,所以,可通过引用计数来判断对象是否可回收。
引用计数法就是:给对象添加一个引用计数器,如果有一个对方引用计数器+1,引用时效,计数器-1,但计数器为0时,就认为对象不太可能会再被使用,就是可回收对象。
可达性分析法
为了解决引用计数器的循环引用问题,java使用了可达性分析得算法。
通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不等价于可回收对象, 不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
GC Roots中一般为全局性引用(常量或类静态属性)和执行上下文(栈帧中的本地变量表)中
标记-清除算法
最基础的垃圾回收算法,分为标记
,清除
两个阶段,标记阶段标记出所有需要胡思后的对象,清除阶段回收这些被标记的对象占用的内存空间。
缺点
1.效率问题
2.空间问题,标记清除后会产生大量不连续的碎片,即内存碎片化,后续可能会出现大对象找不到合适的内存空间的问题。
复制算法
为了解决上面问题,复制算法出现了。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存一次清掉。
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且随着存活对象的增多,赋值的效率会大大降低。
标记-整理算法
结合了以上两个算法,为了避免上述缺点而提出的垃圾回收方式。标记阶段和 标记-清除
算法相同,只不过标记后不是清理对象,而是将存活对象移向内存的一端。然后直接清除端边界外的对象。
分代收集算法
分代收集法是目前大多数jvm所采用的的方法,堆分为新生代和老年代,因此可以根据各自的特点选择合适的回收算法。
新生代每次垃圾回收时都有大量垃圾需要被回收,所以选择复制算法,只需要少量对象的复制成本就可以完成垃圾的回收。
一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
老年代中对象的存活率较高,每次回收只有少量的对象被回收,因此使用标记-清除和标记-整理。