JVM就是java虚拟机,它是一个虚构出来的计算机,可在实际的计算机上模拟各种计算机的功能。JVM有自己完善的硬件结构,例如处理器、堆栈和寄存器等,还具有相应的指令系统。
Java字节码是执行在JRE((Java Runtime Environment Java运行时环境)上的。JRE中最重要的部分是Java虚拟机(JVM),JVM负责分析和执行Java字节码。
JVM是java字节码执行的引擎,还能优化java字节码,使之转化成效率更高的机器指令。
JVM中类的装载是由类加载器和它的子类来实现的,类加载是java运行时一个重要的系统组件,负责在运行时查找和装入类文件的类。
不同的平台对应着不同的JVM,在执行字节码(class文件)时,JVM负责将每一条要执行的字节码送给解释器,解释器再将其翻译成特定平台换将的机器指令并执行,这样就实现了跨平台运行。
(1)jvm是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的。
(2)jvm包含一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域。(3)JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。
在2006年的JavaOne大会上,Sun公司宣布最终会把Java开源,并在随后的一年,陆续将JDK的各个部分(其中当然也包括了HotSpot VM)在GPL协议下公开了源码, 并在此基础上建立了OpenJDK。这样,HotSpot VM便成为了Sun JDK和OpenJDK两个实现极度接近的JDK项目的共同虚拟机。
VM在整个JDK中处于最底层,负责与操作系统的交互。操作系统装入jvm是通过JDK中的java.exe来实现的,具体步骤如下:
a、创建JVM装载环境和配置;
b、装载jvm.dll;
c、初始化jvm.dll;
d、调用JNIEnv实例装载并处理class类;
e、运行java程序
深入理解JVM—JVM内存模型
参考URL: https://www.cnblogs.com/dingyingsi/p/3760447.html
【推荐】 JVM运行时数据区域划分
参考URL: https://blog.csdn.net/bruce128/article/details/79357870?utm_source=blogxgwz9
在hotsnop vm中,方法区和永久代是一个东西,java规范中是分开的。在jdk 1.7之前 常量池是放在方法区中。
元空间的本质和永久d代类似,都是对JVM规范中方法区的实现。不过元空间与永久带之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间大小仅受本地内存限制。
堆
堆(heap)是存储java实例或者对象的地方,是GC的主要区域,同样是线程共享的内存区域。
堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。
老年代 : 三分之二的堆空间
年轻代 : 三分之一的堆空间
eden区: 8/10 的年轻代空间
survivor0 : 1/10 的年轻代空间
survivor1 : 1/10 的年轻代空间
命令行上执行如下命令,查看所有默认的jvm参数
java -XX:+PrintFlagsFinal -version
参数 作用
-XX:InitialSurvivorRatio 新生代Eden/Survivor空间的初始比例
-XX:Newratio Old区 和 Yong区 的内存比例
JVM 年轻代到年老代的晋升过程的判断条件是什么呢?
年轻代:所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。
年老代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代:用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。 jdk1.7以后该参数已废除。永久代被元数据区替代。
注意:这里引用类型,比如User对象,我们指的这个对象User的引用存储在栈中,对象本身是存储在堆中。
在JDK8之前的HotSpot虚拟机中,类的这些“永久的”数据存放在一个叫做永久代的区域。永久代一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小,32位机器默认的永久代的大小为64M,64位的机器则为85M。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。但是有一个明显的问题,由于我们可以通过‑XX:MaxPermSize 设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误(OOM)。
在JDK7之前的HotSpot虚拟机中,纳入字符串常量池的字符串被存储在永久代中,因此导致了一系列的性能问题和内存溢出错误。
随着Java8的到来,我们再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域就是我们要提到的元空间。
这项改动是很有必要的,因为对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。并且为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值,如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量。
JDK1.7 就开始“去永久代”的工作了。 1.7把字符串常量池从永久代中剥离出来,存放在堆空间中。
Java进阶——Java中的字符串常量池
参考URL: https://blog.csdn.net/qq_30379689/article/details/80518283
JVM为了减少字符串对象的重复创建,其内部维护了一个特殊的内存,这段内存被成为字符串常量池(方法区中)。
运行时常量池在JDK1.6及之前版本的JVM中是方法区的一部分,而在HotSpot虚拟机中方法区放在了”永久代(Permanent Generation)”。所以运行时常量池也是在永久代的。
但是JDK1.7及之后版本的JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。
字符串对象创建
1、String str = new String(“abc”) 创建多少个对象?2个
在常量池中查找是否有”abc”对象,有则返回对应的引用实例,没有则创建对应的实例对象(1个)
在堆中 new 一个 String(“abc”) 对象(1个)
将对象地址赋值给str,创建一个引用
2、String str = new String(“A”+”B”)创建多少个对象?4个
在常量池中查找,字符串”A”,”B”,”AB”(3个)
在堆中 new 一个 String(“AB”) 对象(1个)
将对象地址赋值给str,创建一个引用
java虚拟机对内存的分配策略。
优先分配到eden。
指定堆内存大小参数。指定Eden大小,进行测试。
如下
制定堆总大小20M,10M是新生代,那么老年代也就是10。survivorRatio=8指定了Eden所占比例。
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
/**
* Eden区测试
*/
public class EdenTest {
public static void main(String[] args){
byte[] b1 = new byte[2 * 1024 * 1024];
byte[] b2 = new byte[2 * 1024 * 1024];
byte[] b3 = new byte[2 * 1024 * 1024];
byte[] b4 = new byte[4 * 1024 * 1024];
System.gc();
}
}
上图中第一个标红就是MinorGC,第二个就是Full GC。是我们代码中System.gc();触发的。
第一个GC触发,是因为我们前面3个2M的后,再加4M大于伊甸园的8M,所以触发了GC。它把前面那3个2M的对象放到了老年代区域,所以途中你看到大概6M,另外的新的4M,他又放到了Eden区。
如上图,
新的对象会先生成在Young area,也就是PSYoungGen中
在几次GC以后,如过没有收集到,就会逐渐升级到PSOldGen 及Tenured area(也就是PSPermGen永久代)中。
Minor GC、Major GC和Full GC之间的区别
参考URL: http://www.importnew.com/15820.html
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
Full GC 是清理整个堆空间—包括年轻代和老年代。
JVM对大对象的判定不同,我们可以通过制定参数。
–XX:PretenureSizeThreshold
如下,我指定6M为大对象,我new 6M的字节数组对象,发现它放到了老年代。
-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=6M
可以指定参数:
-XX:MaxTenuringThreshold
空间分配担保就是新生代内存不够了,想老年代借用。
java虚拟机默认启用了空间分配担保。通过如下参数可以禁用。
-XX:+HandlePromotionFailure
使用逃逸分析,把没有逃逸的对象在栈上分配。
其实就是对象的作用域。
如果这个对象只在内部使用没有外部引用,那么这个对象就会在栈上分配。否则这个对象发生逃逸。
只有在方法体内有效的对象,就是这个对象没有逃逸,否则就是对象发生了逃逸。
对象的访问方式有如下两种方式。
hotspot采用的是 直接指针的方式。
垃圾回收算法其实就是处理如下问题,重点前两个:
在对象中添加一个计数器,当有地方引用这个对象的时候,引用计数器+1,当用引用失效时,计数器就-1
目前的java虚拟机基本没有用这种实现。
因为引用计数,没法回收堆中相互引用(循环引用),栈中没有指向堆的情况,此时计数器不为0,所以无法回收。
jdk8 默认采用的 是Parallel 并行收集器。
效率、功能比较强大。
思路:从GCRoot对象向下一级一级查找,看是否引用,如果,找不到则判定为垃圾对象。
作为GCRoots的对象:
主流的JVM都是使用可达性分析算法。
应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。
上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。
先进行标记,标记完再整理,整理完再清除。
被标记的对象向内存的一端移动,整理到一块,然后一起删除。
其实就是根据内存不同区域,比如新生代使用复制算法,老年代使用标记整理算法。
吞吐量 = (执行用户代码时间) / (执行用户代码时间 + 垃圾回收所占用的时间)
JVM(HotSpot) 7种垃圾收集器的特点及使用场景
参考URL: https://www.cnblogs.com/chengxuyuanzhilu/p/7088316.html
hotspot的JVM中的垃圾回收
参考URL: https://www.cnblogs.com/jinshiyill/p/5290675.html
HotSpot VM垃圾收集器
参考URL: https://www.aliyun.com/jiaocheng/793006.html
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。**虚拟机具体如何进行内存回收动作,是由虚拟机所采用的GC收集器所决定的,而通常虚拟机中往往不止有一种GC收集器,**像目前(JDK7时代)的HotSpot里面就包含有Serial、SerialOld、ParNew、ParallelScavenge、ParallelOld、ConcurrentMarkSweep和GarbageFirst七种收集器。
那么如何判断当前使用了哪个收集器呢?
加打印 -verbose:gc -XX:+PrintGCDetails,打印输出如下
PSYoungGen 就是Parallel收集器,说明这里新生代使用的是Parallel收集器。我们根据这里的名字就可以判断使用那个收集器。
使用什么由JDK所处的环境决定,一般JDK服务端(java -version查看)默认就是使用Parallel收集器。
你可以手动加参数让使用Serial收集器:
-XX:+UseSerialGc
深入理解JVM内幕
参考URL: https://blog.csdn.net/zhoudaxia/article/details/26454421?utm_source=blogxgwz1
Java提供了动态的装载特性;它会在运行时的第一次引用到一个class的时候对它进行装载和链接,而不是在编译期进行。JVM的类装载器负责动态装载。
每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name)进行搜索来检测这个类是否已经被加载了。如果两个类的全局限定名是一样的,但是如果命名空间不一样的话,那么它们还是不同的类。
JVM系列三:JVM参数设置、分析
参考URL: https://blog.csdn.net/see__you__again/article/details/51998038
JVM参数配置总结
参考URL: https://blog.csdn.net/u014351782/article/details/53098227?utm_source=blogxgwz0