深入理解JVM知识总结

一、 虚拟机
1.1 JVM内存结构模型
主要划分为五个部分:类加载器->运行时数据区->执行引擎->本地库接口->本地方法库

1.1.1 运行时数据区划分
虚拟机栈、本地方法栈、程序计数器、方法区、堆
其中方法区和堆是线程共享的;虚拟机栈、本地方法栈、程序计数器是线程独占的,每个线程一份

1.1.2 各个区域的作用
虚拟机栈
虚拟机栈是线程私有的,生命周期和线程一样同生共死,每个java方法在执行的时候都会创建一个栈帧,这个栈帧储存着局部变量表、操作数栈、动态链接、方法出口等。每一个方法在调用和结束的过程就对应了一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧的大小可用参数-Xss配置;栈的大小可以固定也可以动态扩展,当栈的调用深度大于jvm所允许的范围的时候,会抛出StackOverFlowError的错误。
局部变量表:是一组变量值的存储空间,它用于存储方法执行过程中的所有变量,包括方法中声明的局部变量和形参,对象的引用;
操作数栈:方法中的计算过程都是借助于操作数栈来完成的,将参与计算的数据压入操作数栈;
动态链接
方法返回地址:一个方法调用结束之后要返回到调用它的地方,所以栈帧中要保持能够返回到方法调用处的地址。

本地方法栈
本地方法栈的作用和虚拟机栈差不多,它两最大的区别是虚拟机栈服务于java方法,而本地方法栈服务于native方法(本地方法可以看作c++对Java提供的接口)
本地方法详解:什么是 Java 的本地方法?Java 是基于应用层的高级编程语言,无法访问操作系统底层信息,如底层硬件设备等,这个时候就需要使用其他语言来完成功能了,比如 C 语言,本地方法的使用原理如下所示:
1、在 Java 程序中声明 native 修饰的方法,只有方法定义,没有方法实现,将该 Java 文件编译成字节码文件
2、用 javah 编译字节码文件,生成一个 .h 文件。
3、写一个 .cpp 文件实现 .h 文件中的方法。
4、将 .cpp 文件编译成动态链接库文件 .dll .
5、使用 System.loadLibrary() 加载动态连接库文件。
6、这样就可以实现本地方法的调用,用 Java 调用非 Java 编写的接口,基本原理是利用反射机制,在运行的时候找到 .dll 文件并且解析,根据动态链接7、库中的文件名称创建出对象和方法,然后我们就可以利用对象调用方法。.
常见的本地方法有:public final native Class<?> getClass()、public native int hashCode()、protected native Object clone()。

程序计数器:
程序计数器占用的内存空间较小,是当前线程所执行的字节码行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令。多个线程之间的程序计数器相互独立,互不影响,为了保证每个线程都恢复后都可以找到具体的执行位置。

方法区:
也是线程共享的内存区域,方法区是一种规范,储虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码等数据:方法区的实现方式在jdk1.7及以前是永久代,在jdk1.8及以后是元空间
方法区的内存分配可通过一下的参数进行调整:
jdk1.7及以前:-XX:PermSize; -XX:MaxPermSize
Jdk1.8及以后:-XX:MetaSpaceSize; -XX:MaxMetaSpacecSize, jdk1.8以后它的大小只受本机总内存大小的限制

堆:
定义:堆内存是jvm所有线程所共享的部分,在虚拟机启动的时候已经创建了;堆是对象的主要储存位置,是虚拟机进行垃圾回收的主要位置,当申请内存不足的时候会抛出OutOfMemoryError。
堆细分又可以分为新生代和老年代:
新生代:新生代又可分为 Eden,from Survivor,to Survivor。
Eden区:对象刚被创建的时候,存放在 Eden 区,如果 Eden 区放不下,则放在 Survivor 区,甚至老年代中。
Survivor 区: Survivor 又可分为 Survivor From 和 Survivor To,GC 回收时使用,将 Eden 中存活的对象存入 Survior From 中,下一次回收时,将 Survior From 中的对象存入 Survior To 中,清除 Survior From ,下一次回收时重复次步骤,Survior From 变成 Survior To,Survivor To 变成 Survivor From,依次循环,同时每次回收,对象的年龄都 +1,年龄增加到一定程度的对象,移动到老年代中
老年代:存放生命周期较长的对象。
堆的内存分配大小可通过一下参数进行调整:
-Xms:堆的最小值
-Xmx:堆的最大值
-Xmn:新生代大小
-XX:NewSize:新生代的最小值
-XX:MaxNewSize:新生代的最大值

1.1.3 版本变动(以最常用的HotSpot虚拟机为准)
运行时常量池的变迁
Jdk1.7以前常量池在方法区中永久代中
Jdk1.7及以后常量池变迁到堆中
永久代的变迁
方法区是虚拟机的一种规范,永久代和元空间是他的实现方式
Jdk1.8及以后元空间取代了永久代
永久代和元空间的区别
内存上的区别:
永久代和堆一样使用的是连续的虚拟机内存空间,是由虚拟机同一分配管理的,它的大小可以通过 -XX:PermSize 和-XX:MaxPermSize参数进行调配;
而元空间使用的是本机的内存,内存大小受本机的内存大小限制可通过参数
-XX:MetaspaceSize,设置初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 
 
垃圾收集的区别:
永久代的垃圾收集和老年代是捆绑在一起的,因此无论谁堆满了,都会触发永久代和老年代的 垃圾收集,当JVM加载类信息的容量超过了参数 -XX:MaxPermSize设定的值的时候,应用会报OOM错误;上边说 过了元空间的内存大小可以通过参数:-XX:MetaSpaceSize 和参数-XX:MaxMetaSpaceSize调整,但是如果不设置 MaxMetaSpaceSize的话,元空间的大小仅受限与native memory(本机内存大小的影响).
元空间取代永久代的原因:
1:永久代的大小是在启动的时候就配好的,但是虚拟机是不知道要将要加载的类的元数据信息,方法的大小以及常量池的大小是多少,会占用多大的,这都是不可估计的,所以一不小心初始分配内存不够就遭遇OOM错误。
2:元空间简化了Full GC,每一个回收期都有专门的元数据迭代器
3:元空间可以在GC不进行暂停的其工况下并发的释放类数据
4:HotSpot很有可能和JRokit 合二为一,改进原来永久代的一些缺陷

1.2 深入辨析虚拟机栈和堆
1.2.1功能
虚拟机栈以栈帧的方式储存方法的条用过程,并储存方法调用的过程中基本数据类型以及对象的引用,指向的对象实例储存在堆上,变量出了作用域
堆内存是用来存储java中的对象,无论是成员变量,局部变量还是类变量,他们指向的实例对象都是存储在堆中

1.2.2线程独占还是共享
栈内存归属于单个线程,每个线程都会由一个栈内存,其储存的变量只能是在所属的线程中可见
堆内存中的对象堆所有的线程都可见,堆内存中的对象可以被所有的线程访问

1.2.4栈的具体运算方式
栈的具体运算方式是这样的,编译器是通过两个栈来实现的,一个是保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,直接压入操作数栈。当遇到运算符,先与运算符栈的栈顶元素进行比较,如果高于当前栈顶元素的优先级,直接压入,否则取出当前栈顶的运算符,同时取出操作数栈的前两个数据进行运算,并将结果压入操作数栈。再次重复上述步骤,直到当前的运算符被压入栈中,当没有新的运算符需要入栈的时候,取出当前的栈顶元素以及操作数栈的两个运算,进行运算,将结果压入操作数栈,如果方法定义时需要返回值,直接将操作数栈栈顶元素返回即可

1.3 对象分配
1.3.1对象布局
对象头:存放对象自身运行时数据(比如哈希值、对象偏向锁等);类型指针(通过改指针可以找到该对象所对应的类型)
实例数据:程序代码中所定义的各种类型的字段内容
对齐填充:

1.3.2对象内存分配规则
指针碰撞: 如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,
那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。
空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,
虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。
栈上分配:把对象打散分配在栈上(比如把对象的属性会转化为局部变量分配在方法中)
1、开启-Server JVm运行模式,开启server模式才能进行逃逸分析
2、-XX:+DoEscapeAnalysis: 启用逃逸分析(默认是开启的)
3、-Xx:+EliminateAllocation : 标量替换(默认打开)
对栈上分配发生影响的参数就是三个:-server 、-XX:+DoEscapeAnalysis 、 -XX:EliminateAllocation,这三个参数任何一个发生变化都不会发生栈上分配;
对象内存分配也存在线程安全性问题,解决方法是:
1、使用过CAS配上失败重试,保证操作的原子性
2、本地线程分配缓存

1.3.3对象访问方式
句柄:如果使用句柄访问方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,
而句柄中包含了对象实例数据和类型数据各自的具体地址信息。使用句柄方式最大的好处就是reference中存储的是稳定的句柄地址
在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
直接指针:如果使用该方式,Java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,
reference中直接存储的就是对象地址。使用直接指针方式最大的好处就是速度更快,他节省了一次指针定位的时间开销。

1.4 Class类文件结构
1、 class文件是一组以8位字节为基础单位的二进制流
2、类似于结构体的伪结 构类存储数据
3、只有两种数据类型:无符号数和表,无符号数属于基本的数据类型:以u1 u2 u4 u8分别代表一个字节、二个字节、四个字节、八个字节的无符号数,无符号数可以用来描述数字、索隐引用、数量或者按照UTF-8编码构成的字符串。
4、 表是有多个无符号数或者其他表作为数据构成的复合数据类型,整个class文件本质上就是一张表

1.5 Class文件格式详解
1.1:魔数与class文件的版本:每个class文件的头四个字节称为魔数,唯一作用是确定这个文件是否为一个被虚拟机接收的class文件
1.2:常量池:主要存放字面量和符号引用;字面量比较接近java语言层面的常量概念,如文本字符串、声明为final的常量值等;
符号引用则属于编译原理方面的概念,包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
1.3:访问标志:用于识别一些类或者接层次的访问信息,包括这个class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话是佛丠声明为final等
1.4:类索引、父类索引与接口索引结合:这三项数据来确定这个类的继承关系
1.5:字段表集合:描述接口或者类声明的变量;字段包括类级变量以及实例变量
1.6:方法表集合:描述了方法的定义
1.7:属性表集合

1.6 字节码指令
java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字以及跟随其后的零个或者多个代表此操作所需参数而构成。
由于限定了虚拟机操作码的的长度为一个字节(即0-255),这以为值指令集的操作码数不超过256个
大多数指令都包含了操作的所对应的数据类型信息,例如:iload指令是指将int的数据从局部变量表加载到操作栈中;
大部分的指令都没有支持整形类型byte、char、short、boolean,而实际上是以int类型作为操作的数据类型

1.6.1加载和储存的指令
用于:将数据从栈帧中的局部变量表加载到操作栈中,例如有:
iload:将int类型的数据类型从局部变量表加载到操作栈;fload:将float类型的数据从局部变量表加载到操作栈
istore:将一个数值从操作数栈储存到局部变量表;fstore:将一个float类型的数据从操作数栈储存到局部变量表

1.6.2运算或算是指令
用于:将两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶,例如有
加法指令:iadd、fadd 减法指令:isub、fsub 乘法指令:imul、fmul

1.6.3创建实例的指令
用于:创建实例,例如有:new

1.6.4访问字段的指令
用于:访问字段 ,例如有:
getField、putField、getStatic、putStatic

1.6.5方法调用指令
用于:虚拟机调用方法,例如有:
invokeVirtual:该指令用于调用对象实例方法,根据对象的实际类型进行分派(虚方法分派),java语言中最常见的分派方式
invokeInterface:改指令用于调用接口方法,他会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用。
invokeSpecial:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法、和父类方法
invokeStatic:指令用于调用类方法(也就是静态方法)
invokedNamic:指令用于在运行时动态解析出调用点限定符所引起的方法,并执行该方法;
前边四条方法调用指令的分派逻辑都固话在java虚拟机内部,而invokeNamic指令的分派逻辑是有用户所设定的引导方法决定的。
方法的调用执行与数据类型无关

1.6.6方法返回指令
用于:控制方法返回,例如有
ireturn(返回值类型是byte、char、short、boolean、int类型)
freturn(返回值类型为float型)、return(返回类型为void)

1.6.7异常处理指令
用于:处理异常,athrow

1.6.8同步指令
由monitorenter和monitorexit 两条指令来支持synchronized关键字的语义

1.7 方法调用详解
1.7.1解析
虚拟机在程序编辑的时候就知道调用的方法是哪个了,类的静态方法、构造方法、私有方法都是属于方法调用的解析形式
1.7.2静态分派
和解析的实现是一样的效果,就是在编译器虚拟机就知道调用的方法了,方法的重载是静态分派的一种经典的体现,即方法的调用是根据变量的静态类型来决定的:
例如HuMan h = new Man();HuMan是变量h的静态类型,而Man是变量h的实际类型,所以如果在发生重载,则调用的方法由变量的静态类型决定的
1.7.3动态分派
动态分派的经典体现是方法的重写,即在编译期虚拟机并不知道调用的方法是哪个,是在运行的时候由变量的实际类型决定的
实现本质:在运行期,虚拟机会维护一张虚方法表,每个类一个虚方法表,虚方法表记录的是每个方法的实际入口;

1.8 类加载机制
1.8.1类加载原理
虚拟机将字节码文件通过类的加载器加载进虚拟机内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的class对象,作为这个方法区类数据的访问入口
1.8.2加载流程
(类的生命周期):加载->验证->准备->解析->初始化->使用->卸载
a:加载:通过类加载器完成,主要完成三件事

            1:通过类的全限定名来获取定义此类的二进制字节流(这一步实际上是在虚拟机外部类加载器)
            2:将这个类字节流代表的静态储存结构转化为方法区的运行时数结构
            3:在堆中生成一个代表此类的java.lang.class 对象,作为访问方法区这些数据结构的入口

b:连接:这一步可以细分为:
1:验证:此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。
2:准备:进行内存的初始分配,给变量设置默认值;比如给int型的变量设置值为0
3:解析:将符号引用解析为直接引用

c:初始化:初始化阶段是执行类构造器方法的过程。< clinit >方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。
虚拟机会保证方法执行之前,父类的< clinit >方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
发生初始化的情况有且只有五种:
1、使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
2、初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
3、反射调用
4、虚拟机启动时,用户会先初始化要执行的主类(含有main)
5、jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。

1.8.3类加载器
把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。
这样的好处在于,我们可以自定义加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。(自定义类加载器的应用场景:类加载器的加密和解密、热加载、OSGI等)
1、系统自带的类加载器分三种:
a:启动类加载器:加载存放在<JAVA_HOME>\bin目录下的类,是虚拟机识别的类库加载到虚拟机内存中
b:扩展类加载器:加载存放在<JAVA_HOME\lib\ext目录下的所有类库,开发者可以直接使用
c:应用程序类加载器:加载用户类路径上指定的类库,开发者可以直接使用,一般情况下这个就是程序中默认的类加载器

1.8.4双亲委派机制
双亲委派机制的工作过程:
某个特定的类加载器在接收到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以加载则成功返回;
只有父类加载器无法完成加载(它的搜索范围中没有找到所需的类)时.子加载器才会尝试自己去加载。
双亲委派模型的优点
java类随着他的加载器一起具备了带有优先级层级关系,保证java程序稳定运行(在虚拟机中,类的唯一性根据类本身和他的类加载器 一块决定的)
例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.
因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.
如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类.java类型体系中最基础的行为也就无法保证.应用程序也将会一片混乱.

1.9 垃圾收集GC
1.9.1、判断对象存活的方式
引用计数法:通过判断对象的引用数量来判断对象是否存活,每一个对象都有一个计数器,被应用则加1,完成引用则减1
完成引用:当该对象的引用超过了生命周期,或者引用指向了其他对象,在某方法中定义一个对象的引用变量,方法结束之后变量被虚拟机栈自动释放,则该对象的引用也就结束了,任何一个引用技术为0的对象都被认为是不存货的,将被当成垃圾处理

可达性分析算法:通过判断对象的引用链是否可达来决定对象是否要被回收,这个算法的基本思想就是通过一系列的称为 GC Root 的对象作为起点
从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Root 没有任何引用链相连的话,则证明此对象是不可达的,即认为它不可用

1.9.2哪些对象可以作为GCRoot
1、虚拟机栈中的引用对象
2、本地方法栈中的引用对象
3、活跃线程中的引用对象
4、方法区中的常量引用变量
5、方法区中的静态属性变量

1.9.3垃圾回收算法:
1、标记-清楚算法:
标记:即从跟集合进行扫描,堆存活的对象做标记
清楚:对堆进行遍历,回收不可达的对象
缺点:清楚后回残生大量的不连续的内存碎片,可能导致后续在储存较大的对象的时候无法找到足够大的连续内存空间而触发在一次的垃圾收集

2、复制算法
将可用的内存分为对象面和空闲面,在对象面上创建对象,当对象面没有空间的时候,将还存活的对象复制到空闲面,然后将对象面的所有对象清除
好处:解决了内存碎片问题,顺序分配内存,简单高效,适用于对象存活率比较低的场景(比如年轻代),因为复制的内容少,所以效率高

3、标记-整理算法:
标记:即从跟集合进行扫描,堆存活的对象进行标记
整理清除:移动所有存活的对象,按内存地址一次排序,然后将末端地址以后的内存全部回收。
优点:在标记-清除的基础上完成了移动,解决了内存碎片的问题
缺点:成本更高,适用于对象存活率较高的场景

4、分代收集算法:
是一种组合的回收机制,也是 GC 的主流回收算法,将不同生命周期的对象分配到堆中的不同区域,采用不同的垃圾回收算法,提高 JVM 垃圾回收效率。

1.9.4不同内存区域的回收方式、
1、年轻代:采用Minor GC回收,采用赋值算法,年轻代又分为Eden和from survivor to survivor
Eden区:对象刚被创建的时候存放在Eden区,如果Eden区存放在就放在Survivor区,甚至放到老年代中。
Survivor:Minor回收时,将Eden区存放的对象存放到From Survivor区,在一次回收的时候,又将From Survivor区的对象存放到To Survivor区然后清除 From Survivor,到达To Survivor 的对象年龄加1,下一次Minor时循环这个操作(Eden->From Survivor->to Survivor 清除From Survivor,到 To Survivor 还存活的对象年龄加1),当年龄增加到15岁的时候,对象会进入到老年代

2、老年代:存放生命周期较长的对象,使用标记-清除算法或者标记-整理算法

1.9.4垃圾收集器分类:
1、年轻代常见的垃圾收集器
Serial收集器:采用复制算法;单线程收集;进行垃圾收集事必须暂停所有的工作线程;
ParNew收集器:采用复制算法;多线程收集;垃圾收集和工作线程可以同时执行
Parallel scavenge收集器:采用复制算法;多线程收集;更关注系统的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间),相当于:CPU 运行用户代码时间与 CPU 总消耗时间的比值。)

2、老年代常见的垃圾收集器
Serial old收集器:采用标记-整理算法;单线程收集;进行垃圾收集时,必须暂停所有的工作线程
ParNew old收集器:采用标记-整理算法:多线程收集;垃圾收集和工作线程可以同时执行,吞吐量优先
CMS收集器:采用标记-清除算法;垃圾收集线程和工作线程几乎可以同时工作
G1收集器:采用复制算法+标记-整理算法;支持并发和并行,使用多个CPU来缩短stop the world 的停顿时间,与用户线程并发执行,并且可以采用不同的线程去处理新产生的对象;基于标记-整理算法,有利于空间整合处理内存碎片问题
Stop the world:虚拟机要执行GC而停止应用程序的执行;
发生任何一种GC都会发生stop the world,当发生stop the world时除了GC线程不暂停之外,所有的线程都都会处于等待状态,知道GC任务完成;
多数GC优化就是通过减少stop the world的时间和次数来提高程序的性能

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值