java 虚拟机学到的知识点

一、java虚拟机可操作的数据类型分为原始类型和引用类型

------------>原始类型:操作的数叫做原始值。包含数值类型(整数类型,浮点数类型),boolean类型(映射为java虚拟机的int类型),returnaddress类型(指向的是操作码的指针)

------------>引用类型:操作的数叫做引用值。包含类类型,接口类型和数组类型

java虚拟机的数据区域:

pc寄存器(程序计数器):是线程私有的,根据线程的开始和结束而创建销毁。存放的是正在执行的字节码指令的地址,通过改变计数器里面的值类获得下一条需要执行的指令。如果是java方法放的就是正在执行的字节码 指令的地址,如果正在执行的是native方法(不是java语言的方法)存放的就是undefinded。不会有内存溢出

java虚拟机栈:是线程私有的,每个线程都有一个虚拟机栈,与线程同时建立,存放栈帧。栈帧是方法的调用则有栈帧入栈,方法的结束则有栈帧出栈。java虚拟机可以是物理上不连续的内存,但是逻辑上是联系的内存。java虚拟机内存可以是固定大小也可以是可扩展大小。如果栈帧申请的内存大于虚拟站的最大内存,则会报错StackOverflowError,如果虚拟机栈扩展的时候,在内存中申请不到够用的内存则或报错:outOfMemoryError

java 堆:是所有线程所共享的区域,是随着虚拟机的启动而创建,随着虚拟机的退出而销毁。且内存大小可以是固定的,也可以是可扩展的。此区域存在的目前就是存放对象的实例。也是垃圾回收期的主要区域。在物理上可以不是连续的内存空间,逻辑上连续就好了。细分的话有:新生代和老年代。没有内存可以分配无法扩展的时候回报错outOfMemoryError

方法区:是线程共享的,是堆的逻辑组成部分,是随着虚拟机的启动而创建,随着虚拟机的退出而销毁。用来存放被虚拟机加载的类的信息、常量、静态变量,运行时常量池。很多人愿意把方法区称为永久代。这块内存回收的目标主要是:针对常量池的回收和类的卸载。有机会抛出异常:·outOfMemoryError

运行时常量池:是class文件中每个类或者接口的常量池表的运行时表现形式,常量池存放编译期生成的各种字面量以及符号的引用,这部分内容在类加载后就会进入到运行时常量池。从编译期可知的数值字面量必须在运行期解析才能获得方法或者字段的引用。在加载类和接口到虚拟机后就会创建运行时常量池。运行时常量池相较于class文件的常量池具有动态性,运行期间可以将新的常量放到运行时常量池。因为运行时常量池在方法区,所以也有异常:·outOfMemoryError

栈帧:  随着方法的调用而创建随着方法的结束而销毁(无论方法是正常结束还是异常结束)。栈帧的存储空间由创建它的线程分配在java虚拟机栈上。每个栈帧都有自己的本地变量表,操作数栈和指向当前方法所属类的运行时常量池的引用

           局部变量表: 栈帧中的局部变量表的长度由编译期决定,类型为原始类型。局部变量表完成方法调用时的参数传递(第0个变量一定用来存储实例方法所在对象的引用(即java语言的this关键字)。类方法(static)不需要传递实例的引用)

          操作数栈:每个栈帧的内部都有一个称谓操作栈的后进先出的栈,栈帧中的操作数栈的最大深处由编译期决定。在栈帧刚刚创建的时候操作数栈是空的。java虚拟提供一些字节码指令从局部变量表或者对象实例的字段中复制常量或者变量值到操作数栈中。从操作数栈中取走数,做运算,将运算结果入栈。

           动态链接:每个栈帧内部都包含一个指向,当前方法所在类型的运行时常量池的引用,以便对当前的代码实现动态链接。

二、对象的创建

在堆中分配内存两种方式:指针碰撞,空闲列表

指正碰撞:如果内存空间是规整的,使用的在一边,没有使用的内在在领一边,只需要挪动指针,即可分配空间

空闲列表:内存空间不规整,一张列表记录可分配内存的空闲块,找到足够大小的空闲块分配对象需要内存。

内存空间是否规整由垃圾收集器是否有压缩整理的功能决定的。

两种分配内存的方式存在线程不安全的情况。本地线程分配缓冲解决这个问题(TLAB)。

每个线程提前划分内存大小,创建对象在该内存缓冲区域分配内存,线程安全隐患降低。当本地线程缓冲区扩容时还是有可能存在线程不安全。(同步锁解决这个问题)

三、对象的内存定位(句柄访问,直接指针)

实例对象在堆中,对象类型数据位于方法区

句柄访问:在栈中放实例对象位于句柄池的地址,在内存堆空间中开辟空间放句柄池,句柄池中放到对象实例数据与对象类型数据的地址,优点是当对象被移动(垃圾回收整理),只需要要修改句柄池中的地址。缺点是访问速度慢,多了一次到句柄池的访问

直接指针访问:在栈中直接存储的是到实例对象的地址。需要考虑如何从实例对象得到对象类型数据信息,对象地址移动时需要修改栈中的对象指针地址。访问速度快

四、垃圾回收中如何判断对象已死

引用计数算法:对象添加引用计算器,当被引用就加一,引用失效时减一。存在的问题就是当两个对象相互引用时,但是在别的地方两个对象又没有意义,通过该方法两个对象都不死,浪费内存

可达性分析算法:GC Roots 节点到对象没有引用链,则对象已死

可作为GC Roots的对象:虚拟栈(栈帧本地变量表)中引用的对象,方法区类静态属性引用的对象,方法区中常量引用的对象,native方法引用的对象

引用分为强引用,软引用,弱引用,虚引用

方法区的内存回收:永久带主要回收两部分内容:废弃的常量和无用的类

五、垃圾回收算法

标记-清除:标记即是将不可达对象打上标记,一般是打上两次标记才会真的回收。标记-清除算法会产生很多内存碎片。而且效率不过。当分配大内存对象的时候,因为找不大大的内存碎片很有可能提前触发一次垃圾回收

复制算法:将内存平均分为两块,每次只使用其中的一块,每次垃圾回收只回收被使用的一块,将未被回收的内存复制放到未被使用的一块内存,然后一次清除被使用的那块内存。每次只使用一半的内存,内存利用率太低。

新生代采用此算法,按照8:1:1的比例分配三块eden,survivor1,survivor2。每次都浪费10%的空间。但是没有办法保证每次回收剩余内存都小于等于10%的内存空间,需要老年代做担保。

标记-整理算法:用于老年代,先标记整理然后将活的内存向一方移动,整理下

分代收集

六、hotspot算法实现:虚拟机中有办法知道哪些地方存放有对象的引用,不需要一个不漏的检查完所有执行完上下文和全局的引用位置。

在类加载完成的时候,Hotspot就把对象内什么偏移量上是什么类型的数据计算出来,编译过程中,也会在特定的位置记录下栈和寄存器哪些位置是引用

安全点:在这个点停顿下来进行垃圾回收。安全点的选定是:让程序长时间执行的特征。如:方法的调用,循环,异常跳转

线程抢式中断和主动式中断

抢断式中断:当到达安全点,将所有的线程挂起

主动式中断:设置一个标志,线程轮询这个标志,到达安全点则线程自己中断挂起

安全区域:在一段代码片段之中引用关系不发生改变,这段区域任何地方GC都是安全的。当线程本身就是阻塞状态不能响应JVM的中断请求。表明线程自己是安全区域,在线程要离开安全区域的时候去判断是否完成了Gc过程,如果完成了,则继续,如果没有完成则继续阻塞等待。

七、内存分配与回收策略

对象优先在Eden中分配内存,大对象之间分配在老年代,有参数表明多大的之间分配于老年代(比如数组)

长期存活的对象进入老年代:每次GC存活下来的内存年龄加一,默认15岁以后进入老年代。也有参数来设置这个年龄

还有一种算法:在Survivor空间中相同年龄所有对象大小总和大于Survivor空间一半,则所有大于或等于该年龄的对象直接进入老年代

空间担保:Minor GC新生代垃圾回收  Major GC 老年代垃圾回收  Ful  GC 整个堆空间包含老年代于新生代垃圾回收

当老年代最大连续空间大于新生代所有对象总和,则Minor GC安全。如果不成立则参数设置是否允许担保失败。如果允许则看老年代最大连续空间是否大于晋升到老年代空间平均值,如果满足该条件则Minor GC。

如果小于或者设置不允许担保风险则进行Full  GC

八、虚拟机的加载

加载、链接(验证、准备、解析)、初始化、使用、卸载

类加载的时机:明确了5中情况必须初始化,而加载在初始化之前

1、使用new关键字,读取或者设置一个类的静态字段,调用类的静态方法

2、进行反射调用

3、初始一个类发现父类还有没初始化,需要初始化父类

4、虚拟机启动时,用户需要指定一个主类,需要先初始化这个主类

九、加载的各个过程

加载:通过通过类的全限定名获取此类的二进制字节流,将字节流的二进制存储结构转化为方法区运行时数据结构,在内存中生成一个代表j这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证:class文件字节流是否符合当前虚拟机的要求

准备:正式为类的变量分配内存,并赋予初始值。仅是类变量。被final修饰的类变量直接被赋值为代码中的常量

解析:字段,方法,接口,类,父类,等是否符合规范

初始化:初始化阶段开始真的执行类方法。执行类构造器<clinit>()方法

<clinit>()方法:是编译器收集类中所有类变量的赋值语句和静态代码块中的语句合并产生的,收集顺序是语句在源文件出现的顺序,先执行父类中的<clinit>()方法

接口中不需要先执行父类中的<clinit>()方法

十、类加载器

同一个源文件被不同的类加载器加载也不一定相同

双亲委派模型:中存在两种加载器:一种是启动加载器,另一种所有其他的类加载器

启动类加载器

扩展类加载器

应用程式类加载器

自定义类加载器     自定义类加载器

除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类。如果一个类加载器收到一个类加载请求的时候,把这个请求交给父类去完成,每一层的类加载器都如此,最后由启动类加载器来加载,只有父类反馈无法加载的时候才有子类加载

相同的class文件,保证了在任何虚拟机环境下都能得到相同的类,保证了Java程序的稳定性。

十二 内存模型

java内存模型规定了,所有的变量都存储在主内存中(主要对应java堆中对象实例部分),每条线程都有自己的工作内存(虚拟栈中的部分区域),线程的工作内存保存了被该线程使用到的主内存变量副本拷贝,线程对变量的所有操作都必须在工作内存中完成,线程中变量值的传递需要通过内存来完成

内核线程实现:由操作系统内核支持的线程,线程由内核来完成切换,内核通过调度器对线程调度,并负责将线程和任务映射到各个处理器上。程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程。轻量级进程与内核线程之间是1:1的关系。

用户线程实现:用户线程的建立,同步,销毁和调度完全在用户态完成

用户线程加轻量级进程混合实现:轻量级进程在用户线程和内核线程之间作为桥梁

 CMS收集器:以获取最短回收停顿时间为目标的垃圾的收集器。

采用标记——清除算法,整个过程有四个步骤:

初始标记:仅仅是标记一下GC Roots能关联到的对象

并发标记:GC Roots 追踪

重新标记:修正在并发标记期间因为用户程序继续运行初始标记发生变化的部分,时间停顿比初始标记时间长,但是比并发标记块很多

并发清除:与线程一起运行,并发清除

使用时间最长的是并发标记与并发清除

CMS收集器对CPU资源比较敏感,虽然不会使用户线程停顿但是会占用cpu资源,使得系统吞吐量下降,而CMS收集器默认启动(CPU数+3)/4个线程

CMS收集器不能处理浮动垃圾,在并发清理阶段,用户线程是在运行的,期间产生的垃圾没有办法在本次处理。也因为此需要在并发清理阶段还需要预留出内存为在运行的线程使用,不能像其他处理器一样等到老年代几乎没有内存以后再运行垃圾回收。

标记清除的算法会产生很多的垃圾碎片,出现明明有很多空间但是没有足够的大空间分配时触发FULL GC,设置参数在要FULLGC的时候进行整理,整理的过程需要停顿。也可以设计多次不压缩的FULLGC后跟着一次压缩整理

G1回回收器:

 优点:并行并发,分代收集,空间整合(采用复制算法,没有空间碎片),可预测停顿

G1将整个堆空间划分为大小相等的区域块,虽然保留新生代老年代的概念,但是不再物理隔离,在每个区域内开辟一块空间,当对引用对象进行写操作的时候,就检测是不是在不同区域块内对象的引用,如果是就将地址内容放在开辟出来的空间没,当进行GC Roots检索的时候,只是检测开辟出来的空间。

G1运作的动作包含几个动作:

初始化标记:标记GC Roots能关联到的对象,所有线程停顿

并发标记:GC Roots可达性分析,找出存活对象,可与别的线程,并发执行

最终标记:标记在并发标记期间,因为用户线程,而产生变动的可达分析对象,这个期间所有的用户线程停顿,但是可以多个线程并发进行最终分析

筛选回收:对各个分区的回收价值成本排序,根据设置的停顿时间,选择区域进行回收

发布了15 篇原创文章 · 获赞 0 · 访问量 1068

猜你喜欢

转载自blog.csdn.net/ma316110/article/details/82690703