Loading

JVM

程序计数器:

程序计数器属于线程的私有内存区域,记录当前线程的运行位置,以供多线程运行时上下文切换,字节码解释器通过程序计数器的增减来执行顺序分支循环等结构。独立于堆之外,因此程序计数器是唯一不会导致OutOfMemoryError的区域。

虚拟机栈:

JAVA中说得栈其实就是虚拟机栈,线程私有,每个java方法在调用时会创建一个栈帧,保存局部变量表 操作数栈 动态链接 返回地址 等信息。

  • 局部变量表:基本类型以及引用类型,引用类型是指堆中对象的引用。

Stackoverflowerror指的是虚拟机栈不被允许动态扩展大小时,线程请求栈深度超过了虚拟机栈的最大深度

OutOfMemoryError是虚拟机栈允许动态扩展大小时,无法申请到内存空间。

本地方法栈:同虚拟机栈类似。在Hotspot中合二为一。

方法如何调用?

每一次方法调用,都会向虚拟机栈压入一个栈帧,而每次return或抛出异常,都会导致栈帧弹出。

堆:

线程共用,也就是线程不安全。是虚拟机最大的一块内存空间,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。是垃圾收集器管理的主要区域,因此也被称作GC堆。根据垃圾回收机制,将堆分为新生区、老生区。

最容易出现两类OutOfMemoryError,一类是垃圾回收代价太高、另一类是内存不够。

静态常量池:以class文件形式保存的常量,在类加载后,变为运行时常量池。

方法区(元空间、永久代):

线程共用,存放运行时常量池、静态变量、类信息等数据,在Hotspot中的实现之前为永久代,被元空间取代。元空间可以指定初始和最大空间。

元空间使用直接内存,若不指定最大空间,则元空间会尽可能使用物理内存,减少内存溢出

直接内存:

是在虚拟机之外的内存,由NIO类使用native方法分配,然后通过在堆中的一个引用直接内存的buffer访问

 

对象的创建:

  1. 虚拟机在遇到new的时候,会首先在方法区中寻找该对象的类信息是否加载,若没有加载,则首先要加载该类。
  2. 给对象分配内存,有两种方式。

指针碰撞:堆内存规整时,使用的内存和没使用的内存中间有个边界指针,把该指针往没使用过的内存上移动要分配内存的大小那么长的位置就行。

空闲列表:堆内存不规整时,虚拟机维护一个列表记录哪些区域是空闲的,分配的时候在列表上中找一块足够大的,然后更新记录表。

  1. 给分配的内存空间初始化为0。
  2. 执行初始化方法。

 

对象的内存布局:

对象的内存区域分为:对象头、实例数据、对齐填充。

对象头:一部分是对象的运行时数据,hashcode、分代年龄、锁状态等、一部分指向类原始数据。

实例数据:对象包含的真正的数据。

对齐填充:占位作用,使得对象占用空间为8字节的整数倍。

 

对象访问定位:

句柄:在堆中分配一块句柄池,reference指向这个句柄,句柄包含两个指针,一个指向对象实例数据、一个指向类型数据。

直接指针: reference直接指向对象实例。

前者:对象的内存空间被移动时只需要更改句柄而不需要更改reference

后者:节省了一次寻址。

 

字符串常量池:

对于编译期可确定值的字符串,也就是字符串常量,jvm会将其存入字符串常量池,在堆中。其他常量池在方法区

并且拼接得到的字符串也在编译器就存入了常量池.

常量折叠:将在编译期内可确定值的常量存入常量池:

  • 基本数据类型以及字符串常量
  • final 修饰的基本数据类型和字符串变量
  • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

因此,要 尽量避免使用new在堆上创建字符串,而使用双引号,可以引入编译器的优化。

 

String s = new String("abc");这句话创建了几个字符串对象?

会创建 1 或 2 个字符串:

  • 如果字符串常量池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。
  • 如果字符串常量池中没有字符串常量“abc”,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

垃圾回收

堆是垃圾收集器管理的主要区域,因此也被称作GC堆。为了更好的分配、回收内存,由分代垃圾回收算法分为新生代(Eden、From、To)和老生代。

大部分情况,对象都会首先在 Eden 区域分配,若Eden区耗尽,触发一次Minor GC,Eden区被清空,如果对象还存活,则会进入s区,年龄+1,s区中分为两块轮换存放,用来增加对象年龄,当它的年龄增加到一定程度,就会进入老年代中。

也有例外,若某对象应当从Eden挪到survivor,但虚拟机发现survivor放不下该对象,那么该对象就会提前进入老年代,分配担保机制。需要大块连续内存的对象如字符串、数组等,会直接进入老年代,以避免分配担保机制不必要挪动的开支。除了Minor GC,也有Major GC,也就是针对老年代。而上述两种都属于partial GC,当然也有Full GC,当老年代的连续空间小于新生代对象或分配担保失败 ,触发Full GC。

 

垃圾回收算法:

  • 标记清除:标记所有不需要被清除的对象,然后清除所有没有被标记的对象。
  • 标记复制:将内存区域一分为二,标记所有不需要被清楚的对象,并将其复制到空的那一块,然后将当前块清空。像from to
  • 标记整理:标记后让存活对象向一端集中,然后清理边界外的内存
  • 分代收集:根据各代特点选取合适的算法,如新生代每次收集都会存在大量垃圾,使用标记复制。老圣代的对象存活率很高,也没有额外空间可以使用分配担保,使用整理或清楚。

 

垃圾回收器:

是垃圾回收算法的具体实现。

  • Serial:单线程收集,新生代使用标记复制,老生代使用标记整理,在工作时会停止JVM所有的线程。
  • Parnew: serial的多线程实现。
  • Parallel:与Parnew相似,但能够维持较高的CPU吞吐。
  • CMS:能够获得最短的用户线程停顿时间。基本上实现了垃圾回收和用户线程的并发。使用标记清除算法,会产生大量碎片。
  • G1:既能满足高吞吐 也能满足低停顿时间。可以设置预估停顿时间,根据这个时间,维护一个优先列表,力争在预估时间内收取最大块的垃圾。标记复制。
  • G1可独立管理整个堆,摒弃了传统分代区域,G1中的新生代老年代在物理上不是连续的,而维护了多个相同大小的独立区域,这些区域可以独立进行垃圾回收。

如何判断对象已经死亡?

引用计数法:

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。也就是两个对象相互引用,计数器值都不为0,那么两个对象都不会被回收。

可达性分析算法:

通过根节点GC root向下搜索,结点走过的路径称为引用链,引用链到达不了的对象,也就是不可用的对象,应当被回收。

哪些对象可作为GC roots

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

 

强引用(StrongReference)

最普遍的引用。垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)

如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

 

4.虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

 

内存泄露的根本原因:持有强引用的变量没有被释放

  • 解决方法:都说java是不能手动进行内存管理的,其实间接上来讲是可以的,手动标记,将不需要的引用类型变量赋值为null,解除强引用。
    • 这个刷题的时候经常用到,有时候能带来可观的内存使用量减少。

 

如何判断一个常量是废弃常量?当不存在任何对象引用该常量时。

如何判断一个类是无用的类?

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

 

 

类加载机制:

 

 加载:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

验证:加载未完成,验证可能开始。验证文件结构、 语义等是否符合java规范、是否能正常工作。

准备:在方法区分配类静态变量的内存,并给这些内存赋0.

解析:将符号引用解析为方法区中的常量引用

初始化:如果该类具有父类就进行对父类进行初始化,执行其静态代码块和静态成员变量初始化。

 

类加载器:

  • BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
  • ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
  • AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

 

双亲委派模型 :

是默认使用的类加载机制。系统首先判断类如果没有加载过,会把加载请求提交给父类加载器,因此加载时都会最终被送至顶层的BootstrapClassLoader,当父类加载器无法加载,再依次向下处理。也就是从下往上判断类是否加载过,而从上往下去尝试能否加载。

优势:保证所有的类都在统一模型下进行加载,保证了JAVA程序的正常运行,避免类的重复加载,也保证了核心API的安全。

posted @ 2022-04-02 21:17  吉比特  阅读(73)  评论(0编辑  收藏  举报