JVM虚拟机的学习之路

  • Post author:
  • Post category:其他




JVM虚拟机

首先在了解JVM之前,先了解一下Java编辑器是如何运行的,就好比放现在举个例子:“Hello word!”是如何通过一系列编辑通过屏幕展现到你眼前的?

第一天学习Java之前,对于Java语言,老师总会提到一个Java的特性:“Java是跨平台语言”,然何出此言?

我们编写的”Hello word!“存储的文件名后缀叫”. java”文件,而电脑它是不认识这些字母的,他只认识“01010011”类似的二进制文件。所以电脑会通过一个叫做 Javac的工具,把这段代码编译成字节码文件”.class”后缀的文件,这个文件是c/c++编写的。但又是为什么要转化为”.class”文件,我们都知道c语言被称为面向过程的计算机编程语言,而作为底层的JVM只能识别这类语言(终于和我们的主题JVM 扯上关系了!),最后由JVM编译成电脑认识二进制的文件。

回答一下”java跨平台性”:这个跨平台性是由JVM来实现的。Java的JVM从软件层面屏蔽了底层硬件,指令层面的细节让他去兼容各种系统。

如图:

请添加图片描述



开始进入正式进入JVM



一、为啥要学习JVM,学了之后可以干啥

其实在我刚开始学习Java的时候,也会问老师这个问题。可能是急于想通过现在的学习做出一点有成果的东西,感觉很多帖子上好像随便一个人都可以设计一个小游戏,小型网页之类的,自己却copy都不知道如何copy。后来学习时间久了,才知道那是量变产生质变的积累,也就是所谓“操千曲而后晓音,观千剑而后识器!”

JAVA设计的高明之处在哪,为什么会成为很受欢迎的语言?

答:不需要内存管理!

众所周知,每次写C/C++最后还需要释放空间,而Java只求业务能够实现!

那学了可以干啥?

这里可以理解为御医,皇上养御医是防备不时之需,而不是皇上爱生病。内存也是一样,可能会内存溢出,内存泄漏等等,这个就需要程序员给它瞧病。



二,JVM的底层又是怎样实现的呢?




JVM 内存分配图

请添加图片描述

此图就是运行时候的数据区分配!


注意数据是线程共享的,而指令则是线程独立的。



三,Let we look 一下这些区域


方法区 :Method Area

其实刷题的时候经常会遇见一些问该数据是存放在哪个区域的?这里呢其实很简单区分!

方法区存储的是已被Java虚拟机加载的类信息,常量和静态变量,还有即时编译器编译后的代码等数据。

我们一般也称这个区为:非堆。而方法区存储不下的时候会抛出OutofMemory的报错。


Java堆 :Java Heap

Java堆是存储对象实例的,就是那些new出来的东西。

它是Java 虚拟机内存所管理的最大的一个区域(得力干将),在虚拟机启动的时候创建。

java堆也是垃圾回收管理的主要区域,因此被称为GC堆。

Java堆从内存回收角度上分为:新生代和老生代。

Java堆从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的的分配缓冲区。

划分只是为了方便回收内存,而存储的东西都是一样的。

根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。


程序计数器:Program Counter Register

程序计数器是一块比较小的内存空间,保存当前线程所正在执行的字节码指令的地址。

其实就相当于游戏存档功能。

Java虚拟机的多线程是通过线程之间不断切换分配处理器空间实现的。一个程序正在运行的时候,突然他妈给他来了个电话,这个时候为了高效率的使用处理器资源,会换另一个线程上线工作,而接完电话之后的那个线程重新上线的时候,会把之前做到一半的工作继续,而程序计数器就是存储线程接电话之前工作做到哪一部分了。


Java虚拟机栈 : Java Virtual Machine Stacks

就是对一个方法进行拆分细化!

Java虚拟机是线程私有的,它的生命周期和线程同步。

虚拟机栈描述的是Java方法执行的内存模型。创建一个方法是在虚拟机栈创建了一个栈帧,栈帧中就是这个方法所包含的什么局部变量,操作数栈,动态链接,出口等。


局部变量表

:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)


操作数栈

:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去

动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。

出口:出口是什呢,出口正常的话就是return ,不正常的话就是抛出异常落

所以当我们使用递归的时候会创建很多栈帧么?

ofCourse!直到满足条件为止!

**

本地方法栈 :

Native Method Stack

本地方法栈就是栈,只是方法上带上了native关键字的栈字。

它是虚拟机栈为虚拟机执行Java方法的服务。

不多讲,其实我也不太明白,只是它是用C 和C++去实现的底层代码。


Java字节码执行引擎

就像是车子多线程的发动机,它是虚拟机的核心组件,负责运行虚拟机的字节码文件。




GC垃圾回收机制


垃圾回收机制回收的是哪种垃圾?

答:那些没有被引用指向的内存对象,对于一个程序而言,我们已经无法去调用他们了,所以还留着他们做什么。

在JVM垃圾回收机制中,为了确保程序运行的性能,因此JVM在程序运行的过程中是不断自动的进行垃圾回收。但回收垃圾是不定时的,因此它不是马上就会被回收的,作为程序员我们无法指定它去特定的时间去回收一些特定的垃圾。but我们可以建议:

System.gc(); // 手动回收垃圾

这也是我们唯一可以做的手动垃圾回收操作。但建议仅供参考,具体执行还是要看人家,我们只是谏言的大臣。


finalize方法

提到GC,就不得不讲finalize()方法。

finalize()方法实在每次执行GC操作之前会调用的,可以使用其去做必要清理的工作。

该方法是在Object类中定义的,所有类都继承于它,子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

在JVM回收机制中,经常会听到,新生代,老生代等,这其实是对垃圾的一个分类,垃圾分类。而分类无非是为了更好的管理这些垃圾。


新生代,老年代,以及永久代三者的含义和区别

其实在Java中,堆主要划分为新生代和老年代两个区域,永久代就是JVM的方法区,上面我们讲方法区存放的是静态变量,常量等,这些东西是不易被更改的,但作为数据避免不了被当作垃圾的命运,所以才有了永久代这个概念。

老年代是暮年。这天下主要是新生代施展本领的天地,因此对新生代又做了具体的如下区域划分:Eden,From Survivor,To Survivor三个区域,目的无非更好的管理。

新生代中,每次垃圾收集都发现大批对象死去,存活少量的对象,因此为提高效率,设计者通过复制这些少量的存活者来完成收集,这也叫复制算法。

老生代中对象存活率高,没有额外空间对其进行分配担保,因此会使用一种叫做“标记-清理”或者“标记-整理”的算法。这些算法下面会讲。

在JVM的工作机制中,每次只会使用Eden和其中一块Survivor区域来为对象服务,因此无论何时,总会闲着一块Survivor区域。



具体工作的步骤


先说一下三类GC机制

MinorGC:它是新生代GC,指的是发生在新生代的垃圾收集动作,因为新生代回收垃圾很频繁,因此它是最为典型的社畜,一般回收的速度也很快。

MajorGC是老年代GC,通常情况下Major GC和Minor GC会一块执行,但Major GC相比较来说会比较慢。

Full GC 是清理整个堆空间,包括老生代和新生代。

首先作为一个数据,首先会被分配到Eden区(但大型对象会直接放到老年代),直到Eden中无足够空间时才会触发jvm发起一次MinorGC,如果对象经过一次Minor-GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。

那如何判断该对象是否存活呢?

就像我们如何区别这东西属于垃圾–当它们的被使用率很低的时候!


首先是引用计数法

我们都知道回收机制回收的是没有被任何引用的对象,视之为垃圾,这个算法就是利用这个机制,当创建对象的时候,就给该对象绑定一个计数器,每当有一个引用指向该对象,计数器+1,而当这个被指向的引用被删除的时候,计数器-1,所以当计数器的值为0,就证明这个对象嘎了。

但注意这东西一般没人用,我们想想,若是对象间相互引用的话,这个方法就是个闭环状态,所以了解一下就好。


可达性分析

这个方法是目前来说的主流方法。

该方法是建立在存在一个GC Roots 上,那啥是GC Roots ?

这是一个象征性的东西,例如:1,虚拟机栈中引用的对象。2,方法区中类静态属于引用的对象。3,方法区中常量引用的对象。4,本地方法栈中的JNI引用的对象等…

该方法是从一个GC Roots开始向下搜索,搜索所走过的路径为引用链,当一个对象没有任何引用链,则证明该对象是废物,可以回收。




垃圾回收机制策略


复制算法

这是针对新生代的

主要就是去复制那些存活的对象,该算法将内存分为两部分,每次将存货的对象放到另一部分内存啊,然后将之前的内存清空,只使用这一部分,依次循环。

当然该算法的局限是当存活的对象少的时候效率高。


标记-清除算法

该算法是针对老年代的

首先为每个对象存储一个标记位,记录对象状态

阶段一是标记,为每个对象更新标记位,检查对象是否存活。阶段二是清除,是针对死亡对象直接清除。

该算法一定程度上解决了成环问题,但回收的时候需要挂起,标记和清除效率不是很高,因为需要扫描所有对象,且直接删除会造成内存碎片化。


标记-整理算法

该算法是对标记清除算法的一个优化,解决的是碎片化问题。

它的解决方法是对不在被引用的对象进行整理,然后去复制那些存活的对象放到另一个空间,把剩余的所有对象那个空间删除。有点复制算法那味,但是这个算法是在统一区域进行复制的。


分代算法

该算法是建立在上述的几个算法之上的,原理就是根据存货周期的不同将内存划分为老年代和新生代,然后根据不同代之间的特点采用做合适的收集算法,去解决问题。




垃圾收集器

垃圾收集器便是这些算法的具体实现:

当然不同的JDK版本所提供的JVM垃圾回收器是有一定区别的。

请添加图片描述

注意连线的收集器是可以搭配使用。

下面及具体介绍一下:


1、Serial

Serial收集器属于新生代,是最为古老的收集器,也是一个单线程收集器,因此在运行过程中,只能暂停其他线程,直到它收集结束为止。


2、ParNew

ParNew收集器也是新生代,他可以称之为Serial的多线程版本,即同时启动后多个线程进行垃圾回收。

其特点是只有它可以和CMS收集器配合使用。


3、Parallel Scavenge

Parallel Scavenge 收集器属于新生代。和ParNew不同的是,它主打大的吞吐量,以达到更快完成工作的目的。

其特点和ParNew基本相同,不同点就是以高吞吐量为目标。


4、Serial Old

Serial Old 收集器是老年代,也是Serial的老年代版本,其功能和Serial相同,不过采用的是标记-整理s算法,也是单线程收集。




5、Parallnel old

Parallnel old 收集器采用多线程,是Parallel的老年版本,功能基本相同。

**6、CMS收集器:**老年代,是一种以获取最短回收停顿时间为目标的收集器,适用于互联网站或者其他B/S系统服务器。

CMS的三大特点,1 对CPU资源特命敏感。2无法处理浮动文件。3产生大量内存空间碎片


7、G1

分代收集器,也是当今收集器技术发展最前沿的成果之一,是一款面向服务端应用的垃圾收集器。G1亦可以说是CMS的的改良版,他很好的解决了CMS的内存碎片,和更多的内存空间登陆问题。虽然流程与CMS比较相似,但底层的原理已是完全不同。其特点是:

能充分利用CPU,多核环境下的硬件优势。

可以并行缩短停顿时间。

也可以并发让垃圾收集与用户程序同时进行。

分代收集,手机范围广。

独立管理整个GC堆,而不需要与其他收集器搭配。

能够采取不同的方式处理不同时期的对象。

应用场景可以面向服务端应用,针对具有大内存,多处理器的机器。

采用标记-整理 + 复制算法回收垃圾。



JVM参数配置

#常用的设置
-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。 

-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。 

-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。 

-XX:NewSize=n 设置年轻代初始化大小大小 

-XX:MaxNewSize=n 设置年轻代最大值

-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 13,年轻代占整个年轻代+年老代和的 1/4 

-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8

-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。

-XX:ThreadStackSize=n 线程堆栈大小

-XX:PermSize=n 设置持久代初始值	

-XX:MaxPermSize=n 设置持久代大小
 
-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。

#下面是一些不常用的

-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小

-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能

-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用	

-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动

-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用	

-Xnoclassgc 是否禁用垃圾回收

-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用	



JVM的GC收集器设置

-XX:+UseSerialGC:设置串行收集器,年轻带收集器 

 -XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。

-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量

-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。 

-XX:+UseConcMarkSweepGC:设置年老代并发收集器

-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器



类加载器



类加载器实现过程

程序在主动使用某个类的时候,若该类未被加载到内存中,则会通过加载,链接,初始化3个步骤来对该类进行初始化。若无意外,JVM将会连续完成3个步骤,所以有时候也把这3个步骤统称为类加载或者类初始化。




1、加载:

加载是指将类的class文件读入到内存,并将这些静态数据砖混啊为方法区的运行时数据结构,并在堆中生成一个代表这个类的Java.lang.Class对象,作为方法区类数据的的访问 入口,这个过程需要类加载器的参与。

Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

类加载器可以从不同来源加载类的二进制数据,如:本地Class文件,Jar包Class文件,网络Class文件。

类加载的最终产物就是位于队中的Class对象,该对象封装了类在方法区的数据结构,并向用户提供了访问方法区数据类型的接口,即Java反射的接口。




2、连接过程

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入链接阶段,链接阶段负责把类的二进制数据合并到JRE中(就是将java类的二进制代码合并到JVM的运行和状态中)

连接状态又分为如下三个阶段

验证:确保加载的类的信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。

准备:正式为类变量分配内存并设置类变量的初始值的阶段,这些内存都将子啊方法区中进行分配

解析:虚拟机常量池的符号引用替换为字节引用过程


3、初始化

初始化阶段是执行类构造器

<clinit>

() 方法的过程。类构造器

<clinit>

() 方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句中的语句合并产生,代码从上往下执行。

当初始化一个类的时候,若发现其父类还没有进行过初始化,则需要先出发父类的初始化,虚拟机会保证一个类的

<clinit>

() 方法在多线程环境被正确加锁和同步。

初始化就是为类的静态变量赋予正确的初始值。

类加载器的介绍篇

1,启动类加载器(Bootstrap)

2,扩展类加载器(Extension)

3,系统类加载器

4,自定义类加载器


跟类加载器(bootstrap class loader)

它是用来加载java的核心类,使用原生代码来实现的,并不继承自Java.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的类,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。


扩展类加载器(extensions class loader)

扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由java语言实现,是Launcher的静态内部类,她负责加载<JAVA_HOME>/lib/ext目录下或者由系统环境变量DJava.ext.dir指定为路径中的类库,开发者可以直接使用标准扩展类加载器。


系统类加载器(system class loader)

它负责在JVM启动时架子啊来自java命令的-classpath现象,Java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和路径,程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父类架子啊其,由Java语言实现,父类加载器为ExtClassLoader。(java虚拟机采用的是双亲委派模式请求交由父类处理)




类加载器加载Class大致的八个步骤

1,检测此Class是否载入过,即在缓冲区中是否有此Class,若有直接进入第八步。

2,若没有父类加载器,则要么Parent是跟类加载器,要么本身就是,则跳到第四步,若有父类加载器,则顺序往下。

3,请求使用父类加载器去载入目标类,若载入成功则跳至第八步,否则第五步。

4,请求使用跟类加载器去载入目标类,成功则至第八步,否则第七步。

5,当前类加载器尝试寻找Class文件,若找到则执行第6步,若找不到这执行第七步。

6,从文件中载入Class,成功后跳至第八步。

7,抛出ClassNotFountException异常。

8,返回对应的java.lang.Class对象。




双亲委派模式

简而言之就是一个类加载器收到类加载器的请求,自己不会先去加载,而是把这个请求委托交由父类,父类若存在父类,则逐级网上递交直到顶层。

优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。


类加载器间的关系

启动类加载器,由c++实现,没有父类

扩展类加载器,由java语言实现,父类加载器为null

系统类加载器,由Java语言实现,父类加载器为ExtClassLoader

自定义类加载器,父类加载器肯定为AppClassLoader!



版权声明:本文为pluto_ian原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。