JVM部分知识点总结

JVM内存模型

JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面,
class 类信息常量池(static 常量和 static 变量)等放在方法区
new:
1、方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字节码)等数据
2、堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要在堆上分配
3、栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所
以还是一个指向地址的指针
4、本地方法栈:主要为 Native 方法服务
5、程序计数器:记录当前线程执行的行号

栈区

栈区分为java虚拟机栈区和本地方法栈
重点是java虚拟机栈,它是线程私有的,与线程具有相同的生命周期
每个方法执行都会生成一个栈帧,用于存放局部变量表、操作栈、动态链接、方法出口等,每个方法从被调用到执行完毕,对应着一个栈帧在虚拟机中从入栈到出栈的过程。
通常所说的栈指的就是局部变量表部分,存放编译期间可知的8种基本数据类型,及对象引用和指令地址。局部变量表实在编译期间分配完成,但进入一个方法时,这个栈中的局部变量分配内存大小是确定的。

会有两种异常StackOverFlowError和OutOfMemoryError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时,抛出OutOfMemoryError。

本地方法栈为虚拟机使用到的本地方法服务(native)

堆区

堆为所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。
堆区是gc的主要区域,通常情况下分为两个区块年轻代和老年代。更细一点年轻代又分为Eden区存放最学创建的对象,From survivor和To survivor保存gc后幸存下来的对象,默认情况下各自占比为8:1:1.

方法区

被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)。
垃圾回收很少光顾这个区域,不过也是需要回收的,主要针对常量池回收,类型卸载。
常量池用于存放编译期生成的各种字节码和符号引用,常量池具有一定的动态性,里面可以存放编译期生成的常量;运行期间的常量也可以添加进入常量池中,比如string的intern()方法。

程序计数器

当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。

Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。

唯一一块Java虚拟机没有规定任何OutofMemoryError的区块。

堆里面的分区:Eden,survival (from+ to),老年代,各自的特点

一、jvm中堆空间可以分成三个大区:

新生代,老年代,永久代(java8 取消了永久代,采用了 Metaspace);

二、新生代可以划分为三个区:

Eden区,两个幸存区(分为from区和to区);
内存回收时,如果用的是复制算法,从from区复制到to区,当经过一次或多次GC之后,存活下来的对象会被移动到老年区,当JVM内存不够用的时候,会触发Full GC,清理JVM老年区。
当新生区满了之后会触发Mirror GC,先将存活的对象放到其中一个Survive区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把Eden区进行完全的清理,然后整理内存。那么下次GC的时候,就会使用下一个Survive,这样循环使用,如果有特别大的对象,新生代放不下,就会直接加入老年代。因为JVM认为,一般大对象的存活时间比较久。

三、对象创建方法,对象内存内存分配,对象的访问定位

创建:

1、类加载检查

JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,首先检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类的加载过程。

2、对象分配内存

对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。
根据Java堆中是否规整有两种内存的分配方式:
指针碰撞(Bump the pointer)
JAVA堆中的内存是规整的,所有用过内存放在一边,未分配的内存放在另一边,两块内存中间使用一个指针作为分界点的指示器,分配内存就是将指针往空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器
空闲列表(Free List)
JAVA堆中的内存不是规整的,已使用的内存和空间的内存相互交错,就没办法简单的进行指针碰撞了。虚拟机必须维护一张表,记录哪些内存块可用,当需要分配内存的时候,就在表中选择一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器。

3、并发处理

对象创建在虚拟机是非常频繁的行为,即使是仅仅修改一个指针的指向的位置,在并发情况下也并不线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来A对象分配的内存的情况。
同步
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性(乐观锁)
本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存(TLAB)。哪个线程要分配内存,就在哪个的线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

4、内存空间初始化

虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行。
内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。

5、对象设置

虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

6、执行init

在上面的工作都完成之后,从虚拟机的角度看,一个新对象已经产生了,但是从java程序的角度看对象的创建才刚刚开始,init()方法还没执行,所有的字段都还是零。
所以一般来说,执行new指令之后会接着执行init()方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。
访问定位:句柄或直接指针。

四、GC的两种判定方法:引用计数算法和可达分析算法

1、引用计数算法:指的是给对象添加一个引用计数器,当对象被其他地方引用计数器就+1,如果引用失效就-1,当为0就会回收但是JVM没有使用这种算法,因为无法判定相互循环引用(A引用B,B引用A)的情况。
2、可达分析算法:通过一系列的称为”GC Roots“的对象作为起点,从这些点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
目前Java中可作为GC Root的对象有:
1.虚拟机栈中引用的对象(局部变量表)
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)。
java中存在的四种引用
(1)强引用
只要引用存在,垃圾回收器永远不会回收
(2)软引用
非必须引用,内存溢出之前进行回收

Object obj=new Object();
SoftReference<Object> sf=newSoftRerence<Object>(obj);
obj=null;
sf.get();//有时会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然这个对象被标记为需要回收的对象时,则返回null;
软引用主要用于用户实现类似缓存的功能,在内存不足的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真实的来源查询这些数据。
(3)弱引用
第二次垃圾回收时回收,可以通过如下代码实现

Object obj=new Object();
   WeakReference<Object> wf=newWeakReference<Object>(obj);
   obj=null;
   wf.get();//有时会返回null
    wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被标记为即将回收的垃圾,可以通过弱引用的isEnQueues方法返回对象是否被垃圾回收器标记。
(4)虚引用
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现

Object obj=new Object();
PhantomReference<Object> pf=newPhantomReference<Object>(obj);
obj=null;
pf.get();//永远是返回null
pf.isEnQueued();//返回从内从中已经删除

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。

JVM的垃圾回收机制

GC的垃圾回收算法

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

标记清除算法主要分为”标记“和”清除“两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
标记清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下较为高效,但由主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量的内存碎片,空间碎片太多可能会导致之后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾手机操作。

复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次使用其中一块。但这一块的内存用完之后,就将还存活着的对象复制到另外一块上面,然后再把使用过的内存空间一次清理掉。这样使得每次你是对整个半区进行内存回收,内存分配时就不用考虑内存碎片的复杂情况。
优点:按顺序分配内存即可,实现简单,运行高效
缺点:将内存缩小为原来的一般,代价太大。

标记-整理算法

标记整理算法是根据老年代对象的生存特点,分为”标记“和”清除“两个阶段:首先标记所有需要回收的对象,在标记完成后将所有存活的对象统一移到一侧,然后直接清除掉边界以外的内存。

分代收集算法

分代收集算法根据对象存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用合适的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,必须使用”“标记-清除”或“标记-整理”算法完成回收。

常见GC收集器及特点

串行垃圾回收器(Serial Garbage Collector)

并行垃圾回收器(Parallel Garbage Collector)

并发标记扫描垃圾回收器(CMS Garbage Collector)

G1垃圾回收器(G1 GarbageCollector)

并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
1、当标记的引用对象在tenured区域;
2、在进行垃圾回收的时候,堆内存的数据被并发的改变。

并发标记扫描垃圾回收器 使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记扫描垃圾回收器是更好的选择相比并发垃圾回收器。

G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域。

Minor GC与Full GC

从年轻代空间(包括Eden和Survivor区域)回收内存被称为Minor GC。
Major GC是清理永久代。
Full GC是清理整个堆空间–包括年轻代和永久代。

虚拟机类加载机制

类加载过程

类的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备、解析三个部分统称为连接。
类的生命周期
类加载分为五个过程: 加载、验证、准备、解析、初始化。

加载:

在加载阶段,虚拟机主要完成三件事:
1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
3、在JAVA堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。

验证:

验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:
1.文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
2.元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
3.字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
4.符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。

准备:

准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:
pirvate static int size = 12;
那么在这个阶段,size的值为0,而不是12。 final修饰的类变量将会赋值成真实的值。

解析:

解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。

初始化:

在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
至于使用和卸载阶段阶段,这里不再过多说明,使用过程就是根据程序定义的行为执行,卸载由GC完成

类加载器

双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader。
类加载器按照层次,从顶层到底层,分为以下三种:
(1)启动类加载器(Bootstrap ClassLoader)
这个类加载器负责将存放在JAVA_HOME/lib下的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
(2)扩展类加载器(Extension ClassLoader)
这个加载器负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
(3)应用程序类加载器(Application ClassLoader)
这个加载器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Classpath)上所指定的类库,可直接使用这个加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载。

类加载的双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是都使用组合关系来复用父加载器的代码。
工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是将请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个请求时,子类加载器才会尝试自己去加载。
好处:
Java类随着它的类加载器一起具备了一种带有优先级层次关系。例如类Object,它放在rt.jar中,无论那一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因为Object类在程序的各种类加载器环境中都是同一个类。
判断两个类是否相同是通过classloader.class这种方式进行的,所有哪怕同一个class文件被两个classloader加载,那么他们也是不同的类。

实现自己的加载器:只需要进程ClassLoader,并覆盖findClass方法。
在调用loadClass方法时,会先根据委派模型在父加载器中加载,如果加载失败则会调用自己的findClass方法来完成加载。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值