JVM萌新入手大全

6 篇文章 0 订阅

JVM 结构图

JVM

JVM脑图

在这里插入图片描述

内存泄漏

在这里插入图片描述

可达性分析用什么数据结构比较好

(JVM)运行时数据区

(JVM)创建对象的过程

(JVM)判断一个对象是否被回收

(JVM)新生代和老年代用的垃圾回收策略

对象的创建与内存分配

String存储在常量池

解析

将一部分(编译期可知,运行期不可变)的符号引用转为直接引用

当一个类被加载时,该类所用到的别的类的符号引用都会保存在常量池,实际代码执行的时候,首次遇到某个别的类时,JVM会对常量池的该类的符号引用展开,转为直接引用,这样下次再遇到同样的类型时,JVM就不再解析,而直接使用这个已经被解析过的直接引用。

分配

分派与面向对象的三个基本特征之一:多态有关,如重载和重写。

解析和分派不是对立关系,如重载静态方法,既发生了解析,又发生了静态分派。

  • 静态分派

依据静态类型来定位方法执行版本的分派动作称为静态分派。
和方法重载有关,重载方法的参数类型是依据静态类型,而静态类型编译期已知。

  • 动态分派

运行期根据实际类型确定方法执行版本的分派过程称为动态分派

私有线程区域:
  • 栈:函数当前运行过程中的一些函数变量。存对象的引用类型和地址
  • 本地方法栈:存放C++运行时的native栈。
  • 程序计数器:指向当前程序运行的位置。
线程共享区域:
  • 堆:存对象(最终),老年代。
  • 方法区:存储元数据信息,在JDK1.7前作永久代,1.8以后改为元数据空间,存储静态变量和常量、类加载器。

Java的基础数据和指针都是值类型,所以直接存到内存里面去,不是去存地址寻址。

双亲委派机制的好处

保证java核心库的安全性(例如:如果用户自己写了一个java.lang.String类就会因为双亲委派机制不能被加载,不会破坏原生的String类的加载)

请解释StackOverflowError和OutOfMemeryError的区别?

无论是本地方法栈,还是虚拟机栈,都是线程私有的,当一个线程启动的时候,jvm就会给这个线程分配一个栈,然后每当你调用一个方法的时候,jvm就会往这个栈压入一个栈帧,方法调用完毕,栈帧出栈。注意,一个栈能容纳的栈帧是固定的,有默认的值,当然你也可以通过-Xss去调。但这个栈里面的引用所指向堆内存空间是可以扩展的。简而言之,一个栈的栈帧数目是确定的,而与该栈相关联的堆内存是可以动态扩展的,这些是前提。

  • stackoverflowerror

前面说到,当一个线程启动的时候,jvm就会给这个线程分配一个栈,随着程序的执行,会不断执行方法,因此栈帧会不断入栈和出栈。然后,一个栈所能容纳的栈帧是有限的,当栈帧的数量超过了栈所允许的范围的时候(比如递归调用),就会抛出stackoverflowerror异常。

  • outofmemoryerror

程序在执行的过程中,需要不断的在堆内存new对象,每new一个对象,就会占用一段内存,当对没有足够的内容分配给对象示例时,就会抛出outofmemoryerror异常

在JVM中,如何判断一个对象是否死亡?

引用计数法
  • 给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被再使用的。主流的JVM里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象间的互循环引用的问题。
可达性分析法

通过一些列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从GC Roots 到这个对象是不可达),则证明此对象是不可用的。所以它们会被判定为可回收对象(例如图B中的对象既是不可达的)。

在可达性分析算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:

  • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有 覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
  • 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会,稍候GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalie()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将会被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
判断对象是否存活与“引用”有关

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

  • 强引用:就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:用来描述一些还有用但并非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
  • 弱引用:用户描述非必须对象的。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用:一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时刻得到一个系统通知。

双亲委派模型

什么是双亲委派模型

双亲委派模型是描述类加载器之间的层次关系。它要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。(父子关系一般不会以继承的关系实现,而是以组合关系来复用父加载器的代码)

工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(找不到所需的类)时,子加载器才会尝试自己去加载。

在 java.lang.ClassLoader 中的 loadClass() 方法中实现该过程。

为什么使用双亲委派模型

像 java.lang.Object 这些存放在 rt.jar 中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的 Object 类都是同一个。

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在 classpath 下,那么系统将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证。

小心踩雷,一次Java内存泄漏排查实战

Java内存泄漏分析系列之一:使用jstack定位线程堆栈信息

找到内存泄漏的对象了,在项目里全局搜索对象名,它是一个 Bean 对象,然后定位到它的一个类型为 Map 的属性。%CPU占用率比较高,可能是内存泄露

这个 Map 根据类型用 ArrayList 存储了每次探测接口响应的结果,每次探测完都塞到 ArrayList 里去分析。由于 Bean 对象不会被回收,这个属性又没有清除逻辑,所以在服务十来天没有上线重启的情况下,这个 Map 越来越大,直至将内存占满。内存满了之后,无法再给 HTTP 响应结果分配内存了,所以一直卡在 readLine 那里。而我们那个大量 I/O 的接口报警次数特别多,估计跟响应太大需要更多内存有关

ThreadLocal的Key为弱引用,Value为强引用,故不用被GC回收,需要调用remove()方法处理相应的内存泄漏问题。

  • thread dump 主要记录JVM在某一时刻各个线程执行的情况,以栈的形式显示,是一个文本文件。通过对thread dump文件可以分析出程序的问题出现在什么地方,从而定位具体的代码然后进行修正。thread dump需要结合占用系统资源的线程id进行分析才有意义。
  • heap dump 主要记录了在某一时刻JVM堆中对象使用的情况,即某个时刻JVM堆的快照,是一个二进制文件,主要用于分析哪些对象占用了太对的堆空间,从而发现导致内存泄漏的对象。

类加载机制

Java类加载机制,你理解了吗?

classloadertype

  • 加载:通过类的全限定名获取二进制流,加载入方法区,生成一个代表这个类的java.lang.Class对象。
  • 验证:确保class字节流符合规定且安全。
  • 准备:类变量赋初值(准确说是置零)。
  • 解析:符号引用转换为直接引用。
  • 初始化:调用类的()方法,初始化类的资源和变量。
    class loader

类加载顺序

  • 加载方法不等于执行方法,初始化变量则会赋值
  • 类加载顺序应为 加载静态方法-初始化静态变量-执行静态代码块
  • 实例化时 先加载非静态方法-实例化非静态变量-执行构造代码块-执行构造函数

组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

Java NEW一个对象的过程

对象的创建、布局和访问过程

一、类加载过程(第一次使用该类)

Java是使用双亲委派模型来进行类的加载的,所以在描述类加载过程前,我们先看一下它的工作过程:

双亲委托模型的工作过程是:如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此。

因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。

使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

加载

由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例

验证

格式验证:验证是否符合class文件规范

语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)

操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)

准备

为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)

被final修饰的static变量(常量),会直接赋值

解析

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。解析需要静态绑定的内容。(所有不会被重写的方法和域都会被静态绑定)

以上2、3、4三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。

初始化(先父后子)

  • 为静态变量赋值

  • 执行static代码块

注意:static代码块只有jvm能够调用

如果是多线程需要同时初始化一个类,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

因为子类存在对父类的依赖,所以类的加载顺序是先加载父类后加载子类,初始化也一样。不过,父类初始化时,子类静态变量的值也有有的,是默认值。

最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

二、创建对象

在堆区分配对象需要的内存

分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量

对所有实例变量赋默认值

将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值

执行实例初始化代码

初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法

如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它

需要注意的是,每个子类对象持有父类对象的引用,可在内部通过super关键字来调用父类对象,但在外部不可访问

垃圾回收

GC

  • GC Root本地方法栈,方法区,栈不能被删除

删除方法

  • 标记清理会产生内存碎片
  • 标记整理(删了后面的顶上来,减少内存碎片),前移空间移动代价太大
  • 复制算法(分为两个区),不直接删除,不被删除的复制到新区,需要2倍的内存

实际:

Minor GC当在 Eden 区分配内存不足时,则会发生 minorGC ,由于 Java 对象多数是朝生夕灭的特性,所以 minorGC通常会比较频繁,效率也比较高。
  • 年轻代:E区(伊甸园,满了触发YoungGC,用复制算法),,两个Survive区(S0.S1) 8:1:1,两个S区交替工作(E+S1到S0,E+S0到S1)。每次Young GC完年龄会加一,满15岁就直接都去老年代区了。ParNew垃圾收集器(复制)。
Full GC
  • 老年代:只有一块,存满15岁到去老年代区的对象。和大对象,Old满了就和年轻的一起Full GC,发生STOPPED WORLD,整个Java程序直接暂停,就用标记清理或者标记整理。CMS垃圾收集器(标记清理)。

代码中某个位置读取数据量较大,导致系统内存耗尽,从而导致Full GC次数过多,系统缓慢;导出jstack和内存信息,然后重启系统,尽快保证系统的可用性。

和GC Root无关的才能被删除

什么时候会触发Full GC?

  • 调用 System.gc() 方法时,会建议JVM进行Full GC,此方法不建议使用。
  • 新生代使用的是复制算法,为了内存利用率,只使用其中一个 Survivor 空间来做轮换备份,因此如果大量对象在 Minor GC 后仍然存活,导致 Survivor 空间不够用,就会通过分配担保机制,将多出来的对象提前转到老年代,此时如果老年代的可用内存小于该对象的大小,就会触发 Full GC。
  • 当老年代中最大可用的连续空间小于历代晋升到老年代的对象的平均大小时,会触发Full GC 来让老年代腾出更多的空间。

如何优化GC?

  • 尽量不要创建过大的对象或数组。
  • 通过虚拟机的 -Xmn 参数适当调大新生代的大小,让对象尽量在新生代被回收掉。
  • 通过 -XX:MaxTenuringThreshold 参数调大对象进入老年代的年龄。

怎么避免 FULL GC

  • 判断当前新生代的对象是否能够全部顺利的晋升到老年代,如果不能,就提早触发一次老年代的收集

JVM调优

JVM频繁FULL GC

发现是获取接口下的图片尺寸,用完没释放,然后用了fulsh(),直接把堆打满。

解决方法: 只获取文件的头信息就可以获取图片的尺寸信息了。

几款GC回收器的比较

垃圾收集器

老年代收集器:Serial Old、Parallel Old、CMS;

如何选择垃圾回收器

吞吐量一般单线程的垃圾回收器最高,延迟的话类似G1, ZGC这种

定时任务,这个,这个,高吞吐,g1吧

从名字(包括“Mark Sweep”)上就可以看出,CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)
  • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
  • 其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间。

  • CMS收集器无法处理浮动垃圾
  • CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC
整堆收集器:G1

标记整理

  • 可以防止雪崩

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

避免FULL GC

导致CMS Full GC的可能原因主要有两个:Promotion Failure和Concurrent Mode Failure,前者是在年轻代晋升的时候老年代没有足够的连续空间容纳,很有可能是内存碎片导致的;后者是在并发过程中jvm觉得在并发过程结束前堆就会满了,需要提前触发Full GC。CMS的Full GC是一个多线程STW的Mark-Compact过程,,需要尽量避免或者降低频率。
G1的初衷就是要避免Full GC的出现,Full GC会会对所有region做Evacuation-Compact,而且是单线程的STW,非常耗时间。导致G1 Full GC的原因可能有两个:1. Evacuation的时候没有足够的to-space来存放晋升的对象;2. 并发处理过程完成之前空间耗尽。这两个原因跟CMS类似。

垃圾回收器比较: G1 vs CMS

CMS和G1区别

JVM面试经典(BAT必面)

深入理解JAVA虚拟机学习笔记(一)JVM内存模型

深入理解JAVA虚拟机学习笔记(二)垃圾回收策略

《深入理解Java虚拟机》学习笔记

面试官:给我说说你对Java GC机制的理解?

GC垃圾回收机制详解

Java中的四种引用类型(强、软、弱、虚

JVM解决Redis问题

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值