JVM原理

JVM体系结构

JVM的体系结构包括四部分,类装载子系统,执行引擎子系统,本地方法接口和运行时数据区。运行时如图
在这里插入图片描述
程序计数器(Program Counter Register)(线程私有):
1.作用:由于Java虚拟机的多线程机制是通过线程轮流切换和分配CPU执行时间来实现的,为了线程切换后能够恢复到正确的执行位置,每条线程都有独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
2.是一块较小的内存空间,随线程创建而创建,是当前线程所执行字节码的行号指示器。如:线程正在执行的是Java方法,那么计数器记录的就是当前正在执行的虚拟机字节码指令的地址;线程正在执行的是Native方法,则这个计数器值为空(Undefined)。
3.是唯一一个Java虚拟机规范中没有规定任何OutOfMemoryError的区域。

Java虚拟机栈(Java Virtual Machine Stacks)(线程私有):
1.作用:为虚拟机执行java方法(字节码)服务,是描述Java方法执行的模型:每个方法执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
异常:1、StackOverflowError:当线程访问的栈深度大于虚拟机允许的深度时抛出该异常;
2、OutOfMemoryError:如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,就会抛出该异常。

本地方法栈(Native Method Stack)(线程私有)
作用:和虚拟机栈类似,java虚拟机栈是为虚拟机执行Java方法服务的,本地方法栈是为虚拟机使用到的Native方法服务。
Java虚拟机规范没有对本地方法栈中的方法使用的语言、使用方法与数据结构没有强制规定,可以自由实现。Sun HotSpot直接把本地方法栈和虚拟机栈合二为一。
异常:StackOverflowError和OutOfMemoryError。(原理和虚拟机栈一样)

Java堆(Java Heap)(线程共享)
作用:被所有线程共享,Java虚拟机启动时创建,几乎所有的对象实例都存放在堆中。
特点:虚拟机管理的内存中最大的一块;是GC管理的主要区域,所以又名“GC堆(Garbage Collection Heap)”;逻辑上连续,物理上可以不连续;可以动态扩展;如果堆中没有内存完成实例分配,并且堆也无法扩展时,抛出OutOfMemoryError。

方法区(Method Area)(线程共享)
作用:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量(final)、静态变量(static)、即时编译器编译后的代码等数据。
Sun HotSpot虚拟机中把方法区叫做永久代(Permanent Generation)。
异常:受到方法区的限制,抛出OutOfMemoryError。

运行时常量池(Runtime Constant Pool,简称 RCP)
作用:是方法区的一部分,即为方法区的运行时常量池,常量池是这个类型(类)用到常量的一个有序的集合,这些常量包括:所有基本类型、String、其他类型(类和接口的全限定名)、其他方法(方法的名称和描述符)、其他字段(字段的名称和描述符)的符号引用。

类加载机制

下图为java源码被编译成class字节码,加载到java虚拟机的生命周期
在这里插入图片描述

加载

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,实现这个动作的代码称为“类加载器”,JVM提供了3种类加载器:
1、启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
2、扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
3、应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
双亲委派模型
JVM基于上述类加载器,通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
双亲委派模型
双亲委派模型,当一个类加载器收到类加载任务,它自身不会加载这个类,而是优先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
实现双亲委派的代码都集中在ClassLoader#loadClass()方法之中。将统计部分的代码去掉之后,简写如下:

publicabstractclassClassLoader{

publicClass<?> loadClass(String name) throwsClassNotFoundException {

returnloadClass(name, false);

}

protectedClass<?> loadClass(String name, booleanresolve) throwsClassNotFoundException {

synchronized(getClassLoadingLock(name)) {

Class<?> c = findLoadedClass(name);

if(c == null) {

try{

if(parent != null) {

c = parent.loadClass(name, false);

} else{

c = findBootstrapClassOrNull(name);

}

} catch(ClassNotFoundException e) {

}

if(c == null) {

c = findClass(name);

}

}

if(resolve) {

resolveClass©;

}

returnc;

}

}

protectedClass<?> findClass(String name) throwsClassNotFoundException {

thrownewClassNotFoundException(name);

}

}

首先,检查目标类是否已在当前类加载器的命名空间中加载(即,使用二元组<类加载器实例,全限定名>区分不同类)。

如果没有找到,则尝试将请求委托给父类加载器(如果指定父类加载器为null,则将启动类加载器作为父类加载器;如果没有指定父类加载器,则将应用程序类加载器作为父类加载器),最终所有类都会委托到启动类加载器。

如果父类加载器加载失败,则自己加载。

默认resolve取false,不需要解析,直接返回。

GC相关

垃圾回收器
收集并删除未引用的对象。可以通过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集那些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。

JVM将堆分成了二个大区新生代(Young)和老年代(Old),新生代又被进一步划分为Eden和Survivor区,而Survivor由FromSpace和ToSpace组成,也有些人喜欢用Survivor1和Survivor2来代替。这里为什么要将Young划分为Eden、Survivor1、Survivor2这三块,给出的解释是  
“Young中的98%的对象都是死朝生夕死,所以将内存分为一块较大的Eden和两块较小的Survivor1、Survivor2,JVM默认分配是8:1:1,每次调用Eden和其中的Survivor1(FromSpace),当发生回收的时候,将Eden和Survivor1(FromSpace)存活的对象复制到Survivor2(ToSpace),然后直接清理掉Eden和Survivor1的空间。

新生代:新创建的对象都是用新生代分配内存,Eden空间不足时,触发Minor GC,这时会把存活的对象转移进Survivor区。
老年代:老年代用于存放经过多次Minor GC之后依然存活的对象。

新生代的GC(Minor GC):新生代通常存活时间较短基于Copying算法进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

老年代的GC(Major GC/Full GC):老年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。

GC是在什么时候,对什么东西,做了什么事情?
什么时候
  从字面上翻译过来就是什么时候触发我们的GC机制
  ①在程序空闲的时候。  
  ②程序不可预知的时候/手动调用system.gc()。关于手动调用不推荐
  ③Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
异常:  
创建对象是新生代的Eden空间调用Minor GC;当升到老年代的对象大于老年代剩余空间Full GC;GC与非GC时间耗时超过了GCTimeRatio的限制引发OOM。

什么东西
  从字面的意思翻译过来就是能被GC回收的对象都有哪些特征
  ①超出作用域的对象/引用计数为空的对象。
  引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
  ②从GC Root开始搜索,且搜索不到的对象
  跟搜索算法:以一系列名为 GC Root的对象作为起点,从这些节点开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链的时候,则就证明此对象是不可用的。
  这里会提出一个思考,什么样的对象能成为GC Root : 虚拟机中的引用的对象、方法区中的类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中jni的引用对象。
  ③从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象。

做什么
  不同年代、不同种类的收集器很多,不过总体的作用是删除不使用的对象,腾出内存空间。补充一些诸如停止其他线程执行、运行finalize等的说明。

什么时候容易发生内存泄露

①静态集合类像HashMap、Vector等

②各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

③监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

当一个URL被访问时,内存申请过程如下:
A. JVM会试图为相关Java对象在Eden中初始化一块内存区域
B. 当Eden空间足够时,内存申请结束。否则到下一步
C. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收), 释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
D. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
E. 当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)
F. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值