JVM知识梳理

一.源文件加载为字节码文件

当源文件加载为字节码文件时原文件中有几个类就会产生几个.class文件 内部类(外部类$内部类.class)和匿名内部类(外部类$1因为内部类没有名字所以用1,2代替)也会产生相应的字节码文件

二、类加载器(classloader)

当前ClassLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器也会先在自己的缓存中查找,一直到bootstrap。当所有的父类加载器都没有加载的时候在由当前的类加载器加载放到自己的缓存中

 Bootstrap加载核心类库

 ExtClassLoader加载除了基本api之外拓展

 AppClassLoader 加载应用程序和程序员自定义的类

 自定义类加载器 所有用户自定义的加载器应该继承java.lang.ClassLoader

类加载过程

加载

通过一个类的全限定名来获取定义此类的二进制字节流

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

在内存中生成一个代表这个类的java.lang.Class独享,作为方法区这个类的各种数据的访问入口

验证(确保Class文件的字节流中包涵的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全)

文件格式验证

元数据验证(是否有父类,是否继承了不允许被继承的类等)

字节码验证(确定程序语义是合法的,符合逻辑的)

符号引用验证(通过字符串描述的权限定名是否能找到对应的类)

准备(为类变量分配内存并设置类变量初始值)

解析(虚拟机将常量池内的符号引用替换为直接引用的过程)

a.类和接口解析

b.字段解析

c.类方法解析

d.接口方法解析

初始化(根据程序去初始化类变量和其他资源)

三.运行时数据区(方法区,堆,栈,本地方法栈,程序计数器)

1.程序计数器

用于保存正在执行线程的内存地址。Java支持多线程,多个线程交叉执行,当某一个线程被中断时就要记录下他当前执行到哪个内存地址,以方便下次此线程执行时可以准确找到次线程。每一个线程都有一个独立的程序计数器,各个程序计数器独立互不影响。

如果线程正在执行的是一个java方法这个计数器记录的就是正在执行的虚拟机字节指令的地址;如果正在执行的是Native方法,这个计数器值则为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

2.虚拟机栈

和程序计数器一样也是线程私有的。创建一个线程是就会为此线程创建对应的java栈。当每一个方法执行时又会创建对应的栈帧。栈帧中会保存局部变量、操作栈、和方法返回值等信息。每一个方法被调用直至执行完成的过程就对应入栈出栈的过程。

 当在A栈帧中调用另一个方法B时。与此方法对应的相应的B栈帧会被创建,并且会被放到栈顶成为活动栈。当B方法执行完成后会执行出栈操作。A成为活动栈。B栈帧的返回值变为A栈帧的操作栈的一个操作数。Java栈数据不是线程共有的,不会存在同步锁的问题。

 栈帧:存储局部变量表(方法中的局部变量),操作数栈,动态链接,方法出口

1.局部变量表:用来存储方法参数和方法内部定义的局部变量。(包括8种基本数据类型、对象引用和returnAddress())long和double会占用两个局部变量空间,其余只占一个空间,所需内存空间在编译时期分配。

2.操作数栈:操作数栈的每一个元素可以是任意Java数据类型。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值

3.动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所有属性方法的引用

4.方法返回地址:方法正常返回,调用者的程序计数器的值作为返回地址,栈帧可能保存这个值。如果异常返回,由异常处理器来确定

异常:如果线程请求的栈深度大于虚拟机栈所允许的深度时会发生StackOverflowError异常。如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

3. 方法区Method Area

是各个线程共享的内存区域,存储类的结构信息(修饰符,名称)、类中的静态变量、类中定义为final的类型的常量、类中的field信息、类中的方法信息。当使用Class对应调用相应方法时这些数据都来自方法区

4. 本地方法栈Native Method Stack

与栈类型,本地方法栈为Nativ方法服务。在虚拟机规范中对本地方法使用的语言,使用方式与数据结构没有强制规定,因此具体的虚拟机可以自由实现它。

5.堆

所有的对象实例以及数组都要在堆上分配。被java线程共享所以线程不安全。在虚拟机启动时创建

根据java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

新生代:老年代(1:2)新生代中(Eden:from:to=8:1:1)

常量池:常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。

字符串常量池:是常量池中的一部分,存储编译期类中产生的字符串类型数据。

运行时常量池:虚拟机加载Class后把常量池中的数据放入到运行时常量池。

如果在堆中没有内存完成实例分配,并且堆也无法在扩展是,将会抛出OutOfMemoryError

Jdk1.8之前分为新生代,老年代,永久代

Jdk1.8之后分为新生代,老年代,元空间

jdk1,6常量池放在方法区,jdk1.7常量池放在堆内存,jdk1.8放在元空间里面,和堆相独立。

年轻代:新对象和没达到一定年龄的对象年轻代

老年代:对象被长时间使用的对象。老年代的内存空间应该比年轻代的内存空间更大

元空间:像一些方法中的操作的临时变量,对象等最初的永久代是需要在JVM堆内存里面划分,元空间是物理内存使用

四、对象访问模式

1.通过句柄访问

 Java堆中会分配一块内存空间作为句柄池,reference中存放对象的句柄地址,句柄地址中包含在堆中分配的对象实例数据的地址、这个对象类型数据地址; 

2.使用直接指针访问

reference中存储就是在堆中分配的对象类型数据的地址;

五.Gc

 1.去除永久代原因

 是为了将HotSpot与JRockit两个虚拟机标准联合为一个

元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

新new的对象放在Eden区当Eden区中没有足够空间进行分配时,虚拟机将发生一次Minor GC(复制算法)。老年代使用标记整理算法

当Eden满时不被引用的被清除,仍有引用的会被挪到survivor s0

2.判断对象是否存活

a. 引用计数算法

给对象添加一个引用计数器,每当一个地方引用它时,数据器加1;当引用失效时,计数器减1;计数器为0的即可被回收。

b. 可达性分析

Java和C#都是使用根搜索算法来判断对象是否存活。通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。

在Java中哪些对象可以成为GC Root?

虚拟机栈(栈帧中的本地变量表)中的引用对象

方法区中的类静态属性引用的对象

方法区中的常量引用对象

本地方法栈中JNI(即Native方法)的引用对象

至少经过两次可达性分析后(标记两次),宣告对象死亡。第一次发现不可达之后,将会被第一次标记并且进行一次筛选,筛选条件为1.对象没有覆盖finalize()方法2.finalize方法被虚拟机掉用过。满足这两个条件中的一个则视为没有必要执行finalize

若有必要执行finalize方法,这个对象被放置在F-Queue队列中,并在稍后由一个由虚拟机自动建立的,优先级第的Finalizer线程去执行(只会触发不会去等待结果。原因:执行缓慢甚至出现死循环,导致其他对象一致处于等待)稍后gc将对F-Queue中对象进行第二次标记。只要这个对象与引用链上任何一个对象简历关联,就可以逃脱。

3.垃圾收集算法

a. 标记-清除算法(Mark-Sweep)

标记阶段:遍历所有的对象,在活动的对象头打上活动的标记 
清除阶段:再次遍历对象,检查对象头,对于没有活动标记的对象,将其内存空间释放;对于有活动标记的对象,去掉标记

b. 复制算法(Copying)

先将内存分为两个部分,From区和To区,两部分大小相等。对象分配时,只会在From区进行分配。复制算法可以分两步,第一步在From区中,找出所有活动的对象。第二步。复制算法会把这些活动的对象,复制到To区中,再将原有的From区全部清空,并交换两部分内存的职责,即一次GC后,原有的From区会成为To区,To区相反

c.标记整理算法

让所有存活的独享都向一段移动,然后直接清除掉端边界以外的内存。

d.分带收集算法

新生代使用复制算法,老年代使用标记清除或标记整理算法

六.直接内存

  直接内存不是虚拟机运行数据区的一部分,也不是java虚拟机规范中定义的内存区域。

在jdk1.4新加入了NIO类,引入了一种基于通道与缓冲去的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

七.对象的创建过程

1.首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用并且检查这个符号引用代表的类是否被加载、解析和初始化过。

 2.如果没有,那必须先执行相应的类加载过程(加载 链接 初始化)

3.加载完成后,接下来为新生对象在堆中分配内存(对象所需内存的大小在类加载完成后完全确定)。具体的分配方式有两种:一种是指针碰撞,一种是空闲链表

选择那种分配方式有java堆是否规整决定。

4.内存分配完成后,给分配的内存空间都初始化为零值

5.设置对象的对象头信息。

6.执行完new指令后,接

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值