JVM的主要组成部分及其作用

JVM包含两个子系统和两个组件,两个子系统为ClassLoader(类装载),Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native interface(本地接口)

  • Class loader(类装载子系统):用于将类信息加载到运行时数据区的方法区中
  • Exceution engin(执行引擎): 执行classed中的指令
  • Native Interface(本地接口):与本地方法库交互,是其他编程语言交互的接口
  • Runtime data area(运行时数据区):就是我们所说的JVM内存

作用:首先编译器将源代码编译成字节码,然后JVM用类装载子系统将字节码加载到运行时数据区的方法区中。而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码解释成底层的系统指令。在交给CPU去执行,整个过程中需要调用其他语言的本地库接口来实现整个程序的功能

Java程序运行机制步骤

首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;

在利用编译器(javac命令),将源代码编译成字节码文件.class;

运行字节码的工作是由JVM中的解释器完成的(java命令);

类的加载就是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再对区内创建一个Java.lang.Class对象,用来封装类再方法区内的数据结构。

Java内存区域详解

运行时数据区域

线程私有的:

  • 本地方法栈
  • 虚拟机栈
  • 程序计数器

线程共有的:

  • 方法区
  • 直接内存(非运行时数据区的一部分)

程序计数器

程序计数器是运行时数据区的一小块内存空间,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

另外,未来线程切换后能恢复到正确的执行位置,每一条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。

从上面的介绍中我们知道了程序计数器主要有两个作用:

  • 在字节码解释器工作时,通过改变程序计数器的值来指定下一条需要执行的字节码指令。实现流程控制
  • 在线程切换时,根据程序计数器的值来保存线程的运行状态,切换回来时能够知道上一次线程运行到的位置。

*程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建开始,随着线程的结束死亡

Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。

栈是JVM运行时数据区域的核心,除了一些Native方法调用是通过本地方法栈实现的,其他所有的Java方法调用都是通过栈来实现的

方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

栈由一个个的栈帧组成,每个栈中都有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作

  • 局部变量表:定义为一个数字数组,主要用于存储方法中的参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型和对象引用和returnAddress类型。
  • 操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
  • 动态链接:主要服务一个方法需要调用其他方法的场景。在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在Class文件的常量池中,当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是将符号引用转换为调用方法的直接引用。

栈空间虽然不是无限的,但是一般正常调用的情况是不会出现问题的。不过,如果函数调用陷入无限循环的画,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度时,就会抛出StackOverFlowError

Java方法有两种返回方式,一种时return语句正常返回,一种是抛出异常,不管是哪种方式都会导致栈帧被弹出,也就是说,栈帧随着方法调用被创建,随着方法结束被弹出。

  • 当栈的大小不可以动态扩展时,当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError
  • 当栈运行动态扩展时,申请虚拟机栈的空间无法得到满足时,就会抛出OutOfMemoryError

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是,虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务

Java虚拟机所管理的内存中最大的一块,时Java所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

之所以说几乎所有的对象都在这里分配内存,是因为,JVM在发展至今的过程中,随着JIT编译器以及逃逸分析技术的发展,方法中未逃逸出方法的实例对象会通过标量替换分配在栈空间中。

Java堆时垃圾收集器管理的主要区域,因此也被称作GC堆,从垃圾回收的角度,由于现在收集器基本都采用分代垃圾手机算法,所以Java还可以细分未老年代和新生代,还可以细分为Eden,servivor,Old等空间。

JDK1.8之前堆空间分为一下三部分

  • 新生代
  • 老年代
  • 永久代

JDK1.8之后堆内存分为

  • 新生代
  • 老年代
  • 元空间(在直接内存中分配)

大部分情况,对象会现在eden区分配内存,经过一次新生代垃圾回收以后,如果对象还存活,则会进入servivor区域(s1/s0),且对象的年龄会加一,等到年龄加到15后(默认为15岁,实际上是当前所有对象的年龄的平均值),会被晋升到老年代中。

堆中最容易出现的OutOfMemoryError错误。

  • 当堆空间不足够放下心创建的对象时,会出现
  • 当花了很多时间在Gc上时依然只能回收很少空间时,会出现

方法区

方法区是JVM运行时数据区域的一块逻辑区域,是线程共享的内存区域。

方法区是一个抽象的概念,类似于接口和实现类中的接口。元空间和永久代就是不同JDK版本JVM对方法区的实现。

当虚拟机需要使用一个类时,它需要读取并解析Class文件获取相关的信息,在将信息存入到方法区。方法区会存储已被虚拟机加载的类信息,字段信息,方法信息,常量,静态变量、即时编译器编译后代码缓存等数据。

永久代是JDK1.8之前对于方法区的实现,永久代在运行时数据区中。所以永久代有一个固定大小的上限,无法进行调整。元空间是JDK1.8之后对方法区的实现。他被设置在直接内存中,受本机内存的限制,虽然元空间仍旧可能溢出,但是相比永久代概率要小的多了。

相对而言,垃圾收集行为在这个区域内是比较少出现的,但并非数据进入方法区后就永久存在了。

运行时常量池

Class文件中,除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的常量池表。*(每个类的常量池和运行时常量池不是一个东西,类被加载后,类常量池中的数据会被放到运行时常量池中,所以一个类有一个运行时常量池)

字面量是源代码中固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数,浮点数和字符串字面量,符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。

常量池表会在类加载后存放到方法区的运行时常量池中

既然运行时常量池是方法区的一部分,自然收到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError错误

字符串常量池

用于存放字符串常量,避免字符串对象的频繁创建,减少内存消耗。

HotSpot虚拟机中的字符串常量池的本质就是一个HashSet<String> ,字符串常量值保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。

JDK1.6,字符串常量池在永久代中

JDK1.7,字符串常量池在堆中

将字符串常量池移到堆中,是因为,永久代很少会进行垃圾收集,Java程序中通常会有大量创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时的回收字符串内存。

说一下堆和栈的区别

  • 可见性方面:堆对于整个应用程序都是共享的、可见的。栈只对应线程是可见的,它的生命周期和线程相同。
  • 存放的内容:堆存放绝大多数实例变量和数组,因此该区更关注的是数据的存储;栈存放的是栈帧,栈帧在方法被调用时创建,栈帧中有:局部变量表、操作数栈,动态链接、方法返回地址,该区更关注程序方法的执行
  • 内存方面:堆是不连续的,所以分配的内存是在运行期间确定的,因此大小不固定。一般堆大小远远大于栈。栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
  • 物理地址方面:堆的物理地址分配是不连续的。栈使用的是数据结构中的栈,先进后出的原则,物理地址分配时连续的。所以栈的性能比堆的快。

Java会存在内存泄漏吗?

内存泄漏是指:不再被使用的对象或者变量一直被占据着内存,得不到释放。理论上来说Java不会出现内存泄漏,因为Java中存在GC即垃圾回收机制,不再被使用的对象会被回收。但是,有可能出现一直情况,一个长生命周期对象中持有一个短生命周期对象,短生命周期的对象不在被使用了,但是长生命周期的对象持有短生命周期对象,所以短生命周期的对象得不到释放,就出现了内存泄漏。

虚拟机对象

对象的创建

  1. 类加载检查:虚拟机在遇到一条new指令后,会去检查这条指令对应的参数是否能够定位到运行时常量池中的类符号引用,定位到类符号引用后,检查这个类是否已经加载、解析和初始化过了,如果没有,那必须先进行相应的类加载过程。
  2. 分配内存:在类加载检查通过后,我们需要在堆上为对象分配内存,对象所需要的大小在类加载完成后就已经确定了,为对象分配内存的任务等同于在堆空间中划分出一块大小确定的内存空间。分配的方法有两种,1、指针碰撞法,2、空闲列表法,使用哪种方法取决于Java堆内存是否规整,而Java堆内存是否规整又取决于使用的垃圾回收器是否带有压缩整理功能。                                                                                                                                            分配内存的两种方法 1)指针碰撞法,这种分配方法要在规整的堆内存中使用(即没有内存碎片),所谓指正碰撞,就是将指针指向的位置移动到一个内存大小的位置即可。                                                           2)空闲列表法,这种分配方法,在堆内存不规整的情况下使用,虚拟机会维护一个空闲内存的列表,分配内存时,会找到一块空间足够的内存块来划分给对象实例,然后更新列表记录。
  3. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初值就可以直接使用,程序能够访问到这些字段的数据类型所对应的零值。
  4. 设置对象头:初始化零值完成后,虚拟机需要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据、对象的hash值、对象的GC分代年龄。这些信息存放在对象头中。另外根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方法。
  5. 执行init方法:在上面的工作都完成以后,从虚拟机的视角来看,一个实例对象已经产生了,但从Java程序的视角来说对象创建才刚刚开始,<init>方法还没有执行,所有的字段还都为0值。所以一般来说,执行new指令后会接着执行<init>方法,把对象初始化。

对象的内存布局

在Hotpot虚拟机中,对象的内存布局可以分为3块区域:对象头,实例数据、对齐填充。

Hotspot虚拟机机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(hash值,GC分代年龄、锁状态标志等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据是对象真正存储的有效信息。

对齐填充部分不是必然存在的,也没什么特殊含义,仅起占位作用。

对象的访问定位

建立对象就是为了使用对象,我们的Java程序通过栈上的对象引用数据来操作堆上的具体对象。对象的访问方式又虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针法。

对象的访问定位实际上就是通过栈中的对象引用数据找到堆中的对象。

  • 句柄法:使用句柄法,会在堆内存中划分出一块内存在作为句柄池,对象引用中存储的数据就是对象的句柄地址,而句柄中包含两个指针,一个指向堆内存中的实例本身,一个指向方法区中的类对象。
  • 直接指针法:对象引用的地址直接指向堆空间的对象,对象头中的类型指针指向方法区中的类对象。

使用句柄法的优势是对象引用中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中实例对象的地址。而对象引用本身不需要改变。使用直接指针法最大的好处是,只需要一次访问,他节省了一次指针定位的时间开销。

Hotspot虚拟机主要使用直接指针法来进行对象访问。

创建对象时JVM怎么处理并发安全问题

在多线程条件下,多个线程同时创建对象,分配内存时可能会出现,不同的对象分配到了同一块地址,这就造成了线程安全问题。想要解决这个问题有两种方法

  • 进行同步处理,对分配内存空间的动作加锁(CAS+失败重试来保证操作的原子性),这种方案每次都需要进行同步控制,是比较低效的
  • 在栈生命周期开始时,会在堆内存中分配一窥啊空间,即每个线程有一块属于自己的空间,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,会优先在自己线程对应的TLAB内分配。只有TLAB用完后,才需要同步锁

JVM垃圾回收详解

内存分配和回收原则

对象优选在Eden区分配

大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。

大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串,数组)

大对象直接进入老年代主要时为了避免大对象分配内存时由于分配担保机制带来的复制而降低效率。

长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该放在新生代,哪些对象应该放在老年代中。为了能做到这一点,虚拟机给每个对象一个对象年龄计数器,也就是对象头中的GC分代年龄。

大部分情况,对象都会被首先分配在Eden区中,如果对象在Eden出生并经过第一次MinorGC后仍能够存活,并且能被Survivor容器容纳的花,将会被移动到Survivor空间(s0/s1)中,并将对象年龄设置为1。

对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度,就会被晋升到老年代中,对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置(默认是15)(实际上是,HotSpot虚拟机会遍历所有的对象,按照年龄从小到大对占用的大小进行,当积累的某个年龄大小超过了survivur区的50%时(这个数值也可以通过-XX:TargetSurvivorRatio=percent来设置))取这个年龄和MaxTenuringThreshold中更小的一个值。

主要进行GC的区域

GC分类

1、PartialGC(部分GC)

  • YoungGC/MinorGC :只收集年轻代的GC
  • OldGC:只收集老年代的GC。只有CMS的concurrent collection是这个模式
  • MixedGC:收集整个年轻代和部分的老年代的GC,只有G1有这个模式

2、FullGC:包括年轻代,老年代、永久代(如果存在的话)

  • youngGC:当年轻代中的eden区分配满的时候触发,注意年轻代中有存活对象会晋升到老年代,所以youngGC触发以后,老年代的占用量通常会有所提高
  • fullGC:当准备要触发一次youngGC时,如果统计数据说之前youngGC的平均晋升大小比里面老年代剩余的空间大,则不会触发YoungGC而是转为触发FullGC;或者存在永久代时,要在永久代分配空间但是已经没有足够空间时,也要触发一次fullGC。调用System.gc()默认也是fullGC;

空间分配担保机制

空间分配担保是为了确保在YoungGC之前老年代本身还有容纳新生代所有对象的剩余空间。

在JDK1.6之前,进行youngGC之前,虚拟机会先查看老年代的最大可用连续空间是否大于新生代的所有对象总空间,如果这个条件成立,那这一次youngGC可以确保是安全的,如果不成立,虚拟机会先查看-XX:HandlePromotionFailure 确认空间分配担保是否开启,如果开启,只要老年代的最大连续可用空间超过历次youngGC晋升到老年代的平均大小,就进行youngGC。否则进行FullGC。JDK1.6之后,规则变为只要老年代的最大连续空间大于新生代对象总大小或者历次平均大小,就会进行YoungGC,否则进行FullGC。

JVM中的永久代会发生垃圾回收吗

永久代不会发生垃圾回收,但是永久代的垃圾会被回收,当永久代满了,或者超出了临界值,会触发FullGC,FullGC,是整个堆+方法区的GC,会一次性进行整个堆的垃圾回收。这也就是为什么正确的永久代 大小对避免FullGC是非常重要的原因。

死亡对象判断方法

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能在被任何途径使用的对象)。

引用计数法

给每个对象添加一个引用计数器。

每当一个引用引用这个对象,计数器加1,每当一个引用失效,计数器减1,任何时候计数器为0的对象就是不可能在被使用的。

这个方法实现简单,效率搞,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

可达性分析算法

这个算法的基本思想就是通过一系列称为GC Roots的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

可以被作为GCRoots的对象

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

引用类型总结

  1. 强引用:最普遍使用的引用,基本我们使用的引用都是强引用,如果一个对象具有强引用,那就绝对不会被垃圾回收器回收。当内存空间不足是,会抛出OutOfMemoryError也不会回收有强引用的对象。
  2. 软引用:当垃圾回收进行时,如果内存空间足够,就不会回收,一旦不足够,就会回收软引用引用的对象。软引用可以和一个引用队列联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
  3. 弱引用:这个引用和软引用的区别就是,只要发生垃圾回收,无论内存够不够,弱引用引用的对象都一定会被垃圾回收器回收。同样也可以配合引用队列使用。
  4. 虚引用:顾名思义,这个引用形同虚设,如果一个对象仅持有虚引用,那它就和没有任何引用一样。在任何时候都可能会被垃圾回收。虚引用主要用来跟踪垃圾被垃圾回收的活动。

虚引用和软引用和弱引用的一个区别在于:虚引用必须和引用队列联合使用。当一个垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象前把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否被加入了虚引用来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么久可以在所有的对象的内存被回收之前采取必要的行动。

为什么需要使用软引用

使用软引用可以构建敏感数据的缓存,假设有一个员工信息查询的实例。作为一个用户,我们在查询了一些用户之后,非常可能要回头查询查询过的用户。这时候通常有两种实现。

  1. 把过去查看过的员工信息保存在内存中,每一个存储了员工信息的Java对象的生命周期贯穿整个应用程序的始终
  2. 当用户开始查询其他员工的信息使,把存储了当前所查看的用户的Java对象的引用结束。使得垃圾回收器可以回收这个对象。当我们需要再次查看这个用户的数据时,重新构建这个对象即可

使用第一种会造成非常大的内存浪费,而实现第二种可能会出现对象的重复,即被关闭引用的对象还没有被垃圾回收器回收,就回头查看这个对象了,这时不论这个对象有没有被垃圾回收,我们都会再次创建这个对象。

我们知道,访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取哪些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序运行的速度(这就是软引用要解决的问题)。

使用软引用

SoftReference的特点是它的一个实例保存一个Java对象的软引用。这个软引用的对象不影响垃圾回收,当JVM即将出现OutOfMemoryError时,这个被软引用的对象就会被垃圾回收器收集。一个被软引用的对象如果还没有被垃圾收集器回收,我们就可以调用SoftReference.get()来获取这个对象的强引用。如果已经被收集了,get就返回Null。

使用软引用的方法

MyObject obj = new MyObject();

SoftReference ref = new SoftReference(ref);

此时obj持有强引用,当obj=null;时,ref就成为了一个软引用的对象。

MyObject aobj = (MyObject)ref.get();

使用ReferenceQueue(引用队列)清除失去了软引用对象的SoftReference

SoftReference对象除了具有保存软引用的特殊性以外,也具有Java对象的一般性。所以当被软引用的对象被垃圾回收器回收以后,虽然这个SoftReference对象的get方法返回Null,但是这个SoftRerence对象已经不在具有价值。需要一个适当的清除机制。避免大量SoftReference对象带来的内存泄漏,在Java.lang.ref包里还存在着ReferenceQueue,如果在创建时声明这个ReferenceQueue,即

ReferenceQueue queue=new ReferenceQueue();

SoftReference ref = new SoftReference(obj,queue);

那么当这个SoftReference对象所软引用的对象被收集后,这个没有用的SoftReference对象就会被加入到ReferenceQueue中去。用这个队列我们可以判断哪些软引用对象被垃圾回收器回收了,这时就可以回收这个SoftReference对象了。

弱引用的作用,看是存在两个GCRoot都对一个对象持有引用,如果有,可以用弱引用隔离开

虚引用的作用,在被引用对象被垃圾回收前,讲虚引用对象加入到引用队列中,根据引用队列中存不存在该虚引用对象来触发操作,如,回收对象前,根据虚引用队列中有没有该对象的虚引用来回收ByteBuffer分配的直接内存。

总结

软引用用于解决敏感数据的缓存问题。其引用的对象被回收时,软引用才会加入到引用队列中。

弱引用,用于解决当两个GCROOT持有一个对象的引用时出现的内存泄漏问题。同样是当引用对象被回收时才会将引用加入到引用队列中。

虚引用,在对象被回收前加入引用队列,且虚引用只能配合引用队列使用,通过判断引用队列中存不存在引用对象来触发操作。

若一个对象有多种引用,那怎么判断它的可达性呢?

确定一个对象的引用类型:

  • 在一条引用链中取最弱引用
  • 在多条引用链中取最强类型

由上图,15,链路中,最弱的是软引用,37链路中最弱的是弱引用。即对象5是软引用和弱引用中强的那一个,即软引用。

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

  • 这个类没有任何实例对象,即这个类的所有实例对象都被垃圾回收了
  • 这个类的ClassLoader已经被回收了
  • 这个类的Class对象没有在任何地方被引用,无法在任何地方使用反射获取这个类的对象

虚拟机可以对满足以上三个条件的无用类进行回收,仅仅是可以,不是一定会被回收

垃圾收集算法

标记-清除算法

该算法分为”标记“和”清除“阶段:首先标记出所有不需要回收的对象,在标记完成后同意回收掉所有没有被标记的对象。这个算法有两个问题

  • 效率低下
  • 会产生大量内存碎片

标记-复制算法

将内存分为两块,每次使用其中的一块,在其中的一块进行标记,然后将未标记的对象垃圾收集。然后进行复制,将未被收集的对象整理到另一块内存中。

这种算法解决了产生大量内存碎片的问题

标记-整理算法

标记过程还是一样,将不需要回收的对象标记,但是后续操作不是对可回收对象回收,而是让所有存活对象向一端移动,然后直接清除掉端边界以外的内存。

分代收集算法

当前虚拟机的垃圾收集采用分代收集算法,就是根据不同的分代,选用不同的垃圾收集算法。在新生代中,每次垃圾收集都会收集大量对象,所以选择标记-复制算法,只需要复制少量存活的对象即可。老年代使用标记清除或者标记整理。因为老年代对象的存活率是比较高的。

垃圾收集器

Serial收集器(串行收集器)

只有一个线程进行垃圾回收。它的单线程不仅仅意味着只有一个线程进行垃圾回收,而是,在进行垃圾回收时,所有的其他线程都必须暂停等待垃圾回收线程结束。

优点:简单高效

去点:运行过程中会出现停顿

ParNew收集器

ParNew收集器就是Serial收集器的多线程版本,除了使用多线程进行垃圾手机外,其余行为都与Serial收集器完全一致,但是由于线程的增多,停顿时间减短。

新生代采用标记-复制,老年代采用标记-整理

Parallel Scavenge收集器

和ParNew收集器一样,也是使用标记复制算法的多线程收集器,它看上去几乎和ParNew一样。

ParallelScavenge收集器的关注点是吞吐量(高效率利用CPU)。CMS等垃圾收集器的关注点更多的是用户现场的停顿时间(提高用户体验)

新生代采用标记-复制,老年代采用标记-整理

JDK1.8默认使用Parallel Scavenge收集器,年轻代使用ParallelScavenge,老年代使用Parallel Old

Serial Old收集器

Serial收集器的老年代版本

Parallel Old收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和标记-整理算法

CMS收集器

CMS(Concurrent mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS收集器是HotSpot虚拟机时第一款真正意义上的并发收集器,它第一次实现了垃圾收集线程和用户线程同时工作

CMS使用标记-清除算法。

它的运行过程分为四个步骤:

  • 初始标记:暂停所有线程,记录下与Root对象相连的对象,速度很快
  • 并发标记:同时开启GC和用户线程,一边进行用户线程,一边进行标记,但是这个阶段不能保证标记了所有的对象。因为用户线程可能会不断的更新引用域
  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记变动产生的那一部分对象的标记记录,这一阶段比初始标记稍长,远远比并发标记时间短。
  • 并发清除,开启用户线程,同时GC线程开始对未标记的区域做清扫。

优点:并发收集,低停顿

缺点:对CPU资源敏感,无法处理浮动垃圾,使用的标记-清除算法会产生大量内存碎片。

G1收集器

G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高的概率满足GC停顿时间的要求的同时,还具备高吞吐量性能特征。

特点:

  • 并发与并行:G1能充分利用CPU多核环境下的硬件优势,使用多个CPU来缩短停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器任然可以通过并发的方式来执行用户线程。
  • 分代收集:G1能处理整个堆,但是任然保留了分代的概念
  • 空间整合:与CMS的标记-清理算法不同,G1从整体上来看是基于标记整理算法实现收集器,从局部上是几句标记-复制算法。
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,除了降低停顿时间以外, G1还建立了可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内。

收集步骤

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

详细介绍一下CMS垃圾回收器

CMS即(Concurrent mark sweep)并发标记清理。是一款以牺牲吞吐量为代价来获取最短停顿时间的垃圾回收器,CMS垃圾回收器负责老年代的垃圾收集,可以配合ParNew或者Serial垃圾收集器,进行整堆的垃圾收集。这个垃圾收集器是第一款实现并发垃圾清理的垃圾收集器,并发即垃圾清理线程和用户线程同时运行。具体的垃圾清理分为四步:

  • 初始标记:这个阶段会触发stw,会有一个短暂的停顿,这个阶段主要用来标记被GCRoot对象直接引用的对象
  • 并发标记:这个阶段标记线程和用户线程一起运行,标记会根据初始标记阶段标记的对象,顺着引用链继续标记对象。由于是和用户线程一起运行,对象的引用关系很可能会改变,即可能出现漏标,多标的问题。
  • 重新标记:这个阶段会触发stw,这个阶段主要是对并发标记阶段产生的浮动垃圾进行标记。
  • 并发清理:这个阶段用户线程和垃圾清理线程一起运行,清理未被标记的对象。

CMS垃圾回收器,使用三色标记法进行垃圾回收,在并发阶段可能会出现漏标和多标的情况:

  • 多标,即在并发标记过程中,一个对象已经被标记过了(变成黑色了),但是用户线程将他的引用切断,使这这对象变成垃圾,但是这个垃圾已经被标记过了,此时这个对象就被称为浮动垃圾。浮动垃圾的处理比较容易,并发标记阶段产生的浮动垃圾重新标记阶段会进行标记。并发清理阶段产生的浮动垃圾下一次GC会进行清理
  • 漏标,即在标记过程中,一个未被标记的对象(白色)的引用被改成了一个已经被标记的对象(黑色)上,黑色的对象不会被扫描,即这个白色的对象没有机会被扫描。白色的对象会被当做垃圾清理。这种情况就是严重的bug。有两种方式解决。1、增量更新:一个被标记过的对象被添加了新的引用,那么这个对象会变成灰色。2、原始快照:将被改变引用的未标记的对象记录下来,在并发标记结束后再进行一次重新扫描。扫描记录下来的对象。

CMS垃圾回收器使用标记-清除算法,这种算法会产生大量内存碎片,但是CMS垃圾回收器不能使用标记-整理,因为再并发清理阶段,用户线程和清理线程一起运行,整理和用户线程很可能出现线程安全问题。

缺点

  • 无法处理浮动垃圾
  • 对CPU资源敏感,即CMS是一个注重用户体验的垃圾回收器,STW时间很短,但是并发的垃圾清理模式需要消耗线程资源,吞吐量会降低,引用程序会变慢。
  • 会产生大量内存碎片

新生代垃圾回收器和老年代垃圾回收器有哪些

  • 新生代:Serail回收器、ParNew回收器、Parallel Scavenge回收器
  • 老年代:CMS回收器、Serail Old回收器、Parallel Old回收器
  • 整堆回收器:G1

简述分代垃圾回收器是怎么工作的

我们知道方法区,在JDK1.8的实现是元空间,已经不在堆内存中了,所以现在的堆内存分为新身带和老年代。新生代和老年代的内存占比为1:2;而新生代又被分为Eden和Survivor区,Survivor区又被分为S0和S1,几乎所有的对象都会在新生代的Eden区分配内存。在经过垃圾回收之后,Eden存活下来的对象会被移动到Survivor区中,并且该对象的分代年龄+1,默认是对象的分代年龄加到15之后,该对象会被移动到老年代,但是具体这个年龄是要经过计算的。而不同的JVM会根据不同的垃圾回收器在标记清除、标记复制、标记整理算法中进行选择来进行垃圾清理。

MinorGC和FullGC的触发时机

MinorGC:MinorGC又称YoungGC,即堆内存中年轻代的垃圾回收。当我们分配内存到年轻代中,发生内存不足时就会触发MinorGC,因为年轻代的对象会频繁创建和死亡,所以MinorGC的频率也会很高。但是时间很短。

FullGC:FullGC指的是整个堆和方法区的垃圾收集。当出现一下情况时会出现FullGC

  • System.gc();方法
  • 当年轻代晋升对象到老年代,老年代的内存不足时,触发FullGC(这其中涉及到空间分配担保机制
  • 当经过一次MinorGC,eden区的对象晋升到Survivor区,Survivor区空间不足,尝试晋升到老年代,又放不下,触发FullGC
  • 永久代/元空间(方法区),空间不足,触发FullGC
  • 创建大对象,如数组时,会直接在老年代分配内存,大对象使用的是连续的内存空间,当连续内存空间不足时,触发FullGC
  • 使用CMS垃圾收集器,当老年代浮动垃圾过多时,会触发FullGC

如何选择合适的垃圾收集算法

JVM给了三种选择:串行收集器(年轻代:Serial,老年代:Serial Old),并行收集器(年轻代:Parallel Scavenge/PraNew,老年代:Parallel Old),并发收集器(老年代:CMS),但是串行收集器只适用于小数据量的情况,所以这里的选择主要整堆并行收集器和并发收集器。默认情况下,JDK5之前都是使用串行收集器,如果想使用其他收集器需要再启动时加入相应的参数。JDK5之后,JVM会根据当前系统配置进行判断。

吞吐量优先的并行收集器

并行收集器主要以到达一定吞吐量为目标,适用于科学技术和后台处理

响应时间优先的并发处理器

并发收集器主要是保证系统响应的时间,减少STW的时间,使用于应用服务器,电信领域。

字符串常量池(StringTable)为什么会影响YGC

jdk1.7之后,字符串常量池移到了堆中,所以字符串常量会在堆中分配内存,所以YoungGC需要对StringTable做扫描,以保证新生代的String代码不会被回收。这也就说明了字符串常量池为什么会影响YGC,当字符串常量池过大时扫描的时间就会变长。

*字符串常量池底层是一个HashTable的结构

StringTable甚么时候清理

YoungGC时不会清理StringTable,这也就是为什么大量intern方法会影响YGC的原因。但是FullGC,以及CMS垃圾收集器的CMSGC会收集字符串常量池。

类加载过程详解

类的生命周期

加载-》连接-》初始化-》使用-》卸载

连接又可以细分为验证-》准备-》解析

类加载过程

系统加载Class类型的文件主要三步:加载,连接,初始化。连接又分为三步,验证,准备,解析。

加载

类加载的第一步:

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

相当于在内存中将代码段和数据段关联起来,组织好Class对象的内存空间,作为对象成员和方法的引用入口,并将.class及方法载入方法区。相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为开发人员既可以是使用系统的类加载器来完成加载,也可以自定义自己的泪加载器来完成加载。

连接

验证

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如开头的魔数是不是cafebabe
  • 元数据验证:对字节码描述的信息进行语义分析
  • 字节码验证:通过数据流和控制流分析
  • 符号引用验证:确保解析动作能正确执行

准备

准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段,这些内存都将在方法区中分配。

  1. 这时候进行内存分配的仅包括类变量(即静态变量,被static修饰),而不包括实例变量。
  2. 从概念上讲,类变量所使用的内存都应当在方法区中分配,不过JDK1.8之后,方法区的实现为元空间,字符串常量池和静态变量都被移动到了堆里。原本永久代的模式下,是完全符合这种模式的。所以实际上方法区存放基本数据类型和对象引用。

这里的设置初始值,是指设置为初始零值,而不是赋值为(static修饰)类变量声明时显式赋的值。将显式赋的值赋予变量是初始化阶段进行。

注意:static final修饰的变量不会再这个阶段被赋予0值,final修饰的值只能赋值一次,所以final修饰的变量无论是再声明时,还是再初始化时都必须显式的赋值,否则编译无法通过。这个static final变量会在初始化阶段赋值。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用限定符7类符号引用进行

符号引用就是一组符号来描述目标,可以是任意字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

综上,解析阶段就是将类常量池中的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

初始化

初始化阶段是执行初始化方法<clinit>()方法的过程,是类加载的最后一步,这一步JVM才开始真正执行类中定义的Java程序代码

*<clinit>()方法是编译之后自动生成的

对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化:

  • 当遇到new、getstatic、putstatic、invokestatic这4条直接码指令时。比如new一个类,读取一个静态字段,或者调用一个静态方法时。
  • 使用反射调用创建类的实例时,如果类还没有初始化,则需要先触发初始化。
  • 初始化一个类,如果其父类还没有初始化,要先初始化其父类
  • 当虚拟机启动时,用户需要定义一个要执行的主类,包含main方法的那个类,虚拟机会先初始化这个类
  • MethodHandle和VarHandle可以看做是轻量级反射调用机制,而要想使用这两个调用,就必须先使用findStaticVarHandle来初始化要调用的类。

卸载

卸载类即堆内存中Class对象被GC

需要三个条件

  • 该类没有任何实例对象
  • 该类的类加载器已经被清理
  • 该类没有在其他任何地方被引用

描述一下JVM加载Class文件的原理机制

Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而他的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们不关心类的加载,因为基本上都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。

  • 隐式装载:程序在运行过程中,当碰到new、getstatic、putstatic、invokestatic等指令时,就需要使用类加载器进行类的加载,这一步是JVM自己做的,不是我们主动发起的。
  • 显式装载:通过class.forName()等方法,显式加载所需要的类。

Java类的加载是完全动态的,它并不会一次性将类全部加载后运行,只会将一些基类完全加载到JVM中,至于其它类,只有在需要用某个类时,才会加载这个类,这是为了节省内存开销。

类加载器

JVM中内置了三个重要的ClassLoader,除了BootstrapClassLoader其他类加载器均有Java实现且全部继承自java.lang.ClassLoader

  1. BootstrapClassLoader(启动类加载器):最顶端的加载类,用C++实现,负责加载%JAVA_HOME%/lib目录下的jar包或者被-Xbootclasspath参数指定的路径中的所有类
  2. ExtensionClassLoader(扩展类加载器):主要负责加载%JRE_HOME%/lib/ext目录下的jar包和类,或被系统变量所指定的路径下的jar包。
  3. AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类
  4. 用户自定义类加载器,通过继承Java.lang.ClassLoader类的方式实现

什么是双亲委派模型

JVM中的类加载器有BootStrapClassLoader、ExtensionClassLoader、AppClassLoader、用户自定义类加载器。这几个类加载器有层次结构,每种类加载器负责加载自己层次里的类。所谓的双亲委派模型就是,我们加载一个类时,会优先把这个类交给层次比较高的类加载器进行加载,当该类加载器在他的搜索范围内找不到加载的类时,会把类向下传递。这种模式是为了防止类的重复加载。提高类加载的正确性以及性能。

常见的JVM调优方法有哪些?

调优工具

jconsole、jProfile,VisualVM

Dump线程详细信息:查看线程内部运行情况

死锁检查

查看堆内存、对象信息查看:数量、类型等

线程监控

线程信息监控:系统线程数量

线程状态监控:各个线程都处在什么样的状态下

热点分析

cpu热点:检查系统哪些方法占用了大量的CPU时间

内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)

内存泄漏检查

JVM调优的最终目的

调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。jvm的调优也不例外,JVM调优主要是针对垃圾收集器的收集性能优化令运行在虚拟机上的应用性能能够使更少的内存以及延迟获得更大的吞吐量。当然这里的最少指的是最优选择,而不是越少越好。

三个评估标准

  • 吞吐量
  • 停顿
  • 内存占用

这三个属性中,其中任何一个属性性能获得提高几乎都是以另外一个或两个属性性能的损失作为代价的,不可兼得。具体后一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求在确定。

调优三原则

  1. MinorGC回收原则:MinorGC每次都要尽可能的回收多的垃圾对象,以减少程序发生FullGC的频率
  2. GC内存最大化原则:处理吞吐量和延迟问题的时候,垃圾处理器能使用的内存(堆空间)越大,垃圾收集的效果越好,应用程序也会越来越流畅。
  3. GC调优3选2原则:在性能属性中,吞吐量、停顿延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。

类文件结构

  1. 无符号数,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数
  2. 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所以表都已“_info”结尾,由多个无符号数或者其他表构成的复合数据类型
名称 类型 数量
magic(魔数) u4 1
minor_version(副版本号) u2 1
major_version(主版本号) u2 1
constant_pool_count(常量池计数器) u2 1
constant_poll(常量池表) cp_info constant_pool_count-1
access_flags(访问修饰符类型) u2 1
this_class(类索引) u2 1
super_class(父类索引) u2 1
interfaces_count(接口计数器) u2 1
interfaces(接口表) u2 interfaces_count-1
fields_count(字段计数器) u2 1
fields(字段表) field_info fields_count-1
methods_count(方法计数器) u2 1
methods(方法表) method_info methods_count-1
attributes_count(属性计数器) u2 1
attributes(属性表) attributes_count attributes_count-1

User user = new User() 做了什么操作,申请了哪些内存

  1. 如果该类还没有加载,new指令会触发类的加载,即加载,连接,初始化操作。会在方法区中添加一个运行时常量池,会在堆空间中创建一个Class对象。
  2. 在堆内存中创建一个User对象,即类加载检查,分配内存,初始化零值,设置对象头和执行初始化方法。
  3. 如果这段代码在方法中,还会创建一个引用,分配在栈内存上
  4. 将栈内存中的符号引用换成直接引用

JVM性能调优都做了什么

JVM性能调优

一、JVM内存模型及垃圾收集算法

1、根据Java虚拟机规范,JVM将内存划分为:

  • 年轻代 New
  • 老年代 Tenured
  • 永久代 Perm

其中年轻代和老年代属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm不属于堆内存,由虚拟机直接分配,但是可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小

2、垃圾回收算法

垃圾回收算法可以分为三类,都基于标记-清除(复制)算法:

  • Serial算法(串行算法)
  • Parallel算法(并行算法)
  • Concurrent算法(并发算法)

JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如:如果一个机器多于一个核,会对年轻代选择并行算法。

稍微解释下的是,并行算法是用多线程进行垃圾回收,会说期间会暂停程序的执行,而并发算法,也是多线程回收,但是钱江不停止应用执行。所以,并发算法适用于交互性较高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个较大的老年代,这反过来跟并行算法相比吞吐量相对较低。

垃圾回收动作何时执行?

  • 当年轻代内存满时,会引发一次MinorGC,该GC仅回收年轻代,年轻代满是指Eden区满了,Survivor满不会引发GC
  • 当老年代满时会引发FullGC,FullGC会回收堆内存和方法区
  • 当方法区满时也会引发FullGC,会导致类信息的卸载

何时会抛出OutOfMemoryError,并不是内存被耗空才抛出

  • JVM98%的时间都花费在内存回收
  • 每次回收的内存小于2%

满足这两个条件将触发OutOfMemoryError,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump

二、内存泄漏及解决方法

1、系统崩溃前的一些现象:

  • 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
  • FullGC的次数越来越多,最频繁时间间隔不到1分钟就进行一次FullGC
  • 老年代的内存越来越大并且每次FullGC后老年代没有内存被释放

之后系统会无法响应新的请求,逐渐到达OutOfMemeryError的临界值

2、生成堆的dump文件

通过JMX的MBean生成当前的Heap信息,大小为3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件

3、关于内存泄漏的问题分析

1)为什么崩溃前垃圾回收的时间越来越长

根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记,清除(复制),标记部分只要内存大小固定时间是时间不变的,变得是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间的延长。所以垃圾回收时间也可以作为内存泄漏的依据。

2)为什么FullGC的次数越来越多

因为内存的积累,逐渐耗尽了老年代的内存,导致新对象没有分配的空间,从而导致频繁的垃圾回收

3)为什么老年代占用的内存越来越大

因为年轻代的对象无法被回收,导致晋升到老年代的对象越来越多。

三、性能调优

除了内存泄漏外,我们还发现CPU长期不足3%,系统吞吐量不足,针对当前服务器来说,是严重的资源浪费。

针对以下几个方面进行JVM调优

  • 线程池:解决用户响应时间长的问题
  • 连接池
  • JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量
  • 程序算法:改进程序逻辑算法提供性能

JVM参数:

在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用,必须仔细调优才能获得最佳性能。通过设置我们希望达到一下目标:

  • GC的时间足够短
  • GC的次数足够少
  • 发生FullGC的周期足够长

前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。

(1)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小最大之间收缩而产生额外的时间,我们通常把最大、最小设置为相同的值

(2)年轻代和老年代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比例NewRadio来调整两者之间的大小,也可以针对回收代来设置其绝对大小,-XX:newSize -XX:MaxNewSize 同样,为了防止年轻代的收缩,我们通常会把两者设置同样时间

(3)年轻代和老年代设置多大才算合理?这个问题没有答案,不然就不需要调优了。

  • 更大的年轻代必然导致更小的老年代,大的年轻代会延长minorGC的周期,但会增加每次GC的时间;小的老年代会导致更频繁的FullGC
  • 更小的年轻代必然会导致更大的老年代,小的年轻代会导致MinorGC很频繁,但是每次GC的时间缩短;大的老年代会减少FullGC的频率
  • 如何选择应该依赖应用程序对象生命周期的分步情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,老年代应该适当增加。但是很多应用都没有这样明显的特性,在抉择时应该根据以下两点
  1. 本着FullGC尽量少的原则,让老年代尽量缓存常用对象,JVM默认比例为1:2也是这个道理。
  2. 通过观察应用一段时间,看其在峰值时老年代会占用多少内存,在不影响FullGC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给老年代至少预留1/3的增长空间。

(4)在配置较好的机器上(比如多核,大内存),可以为老年代选择并行收集器:-XX:+UserParallelOldGC,默认为Serial收集器

(5)线程堆栈的设置:每个线程会默认开启1M的栈用于存放栈帧。对大多数应用而言这个默认值太大了,一般256k就足够用,理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但实际上还受限于操作系统。

调优方法

调优有以下原则:

  • 多数的Java应用不需要在服务器上进行GC优化;
  • 多数导致GC问题的Java应用,都不是我们参数设置错误,而是代码问题
  • 在应用上线之前,先考虑将机器的JVM参数设置到最优
  • 减少创建对象的数量
  • 减少使用全部变量和大对象
  • GC优化是到不得已才采用的手段
  • 在实际使用中,分析GC情况代码比优化GC参数要多的多

GC优化的目的有两个:

  1. 将转移到老年代的对象数量降低到最小
  2. 减少FullGC执行时间

为了达到以上目的:

  • 减少使用全局变量和大对象
  • 调整新生代的大小到最合适
  • 设置老年代的大小为最合适
  • 选择合适的GC收集器与算法

Java ClassLoad 机制详解

java.lang.ClassLoader 类介绍

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对于的字节代码,然后从这些字节代码中定义出一个Java类,即Class类的一个实例。除此之外ClassLoader还负责加载Java

类加载的代理模式

类加载器在尝试自己去查找某个类的字节码并定义他,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,一次类推。

Java虚拟机是如何判断两个类是否是相同的

虚拟机不仅要看类的全面是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后得到的类,也是不同的。

这种代理模式,也就是双亲委派机制。这种代理模式有什么好处

  1. 节约系统资源。只要这个类被加载过了,就不会再次加载。
  2. 保证Java核心类库的类型安全。所有的Java应用都至少需要引用Java.lang.Object类,也就是说在运行的时候,Java.lang.Object这个类需要被加载到Java虚拟机中。如果这个加载过程Java应用自己的类加载器来完成的话,很可能就存在多个版本的Object类,这些类是不兼容的。通过代理模式,对于核心类库的加载工作是有引导类加载器来统一完成,保证java应用所使用的都是同一个版本的Java核心类库的类,是互相兼容的。

JVM如何直接分配内存,new对象如何不分配在堆而是栈上,常量池解析

JVM直接内存

直接内存不是JVM运行时数据区的一部分,也不是JVM规范中定义的内存区域,在jdk1.4中新加入了NIO类,引入了一种基于,Buffer(缓冲),Channel(通道),Selector(选择器)的IO方法,它可以使用native库函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象来作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

直接内存(堆外内存)于堆内存比较

  1. 直接内存申请空间耗费更高的性能,当频繁申请到一定量时由为明显
  2. 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

分析:

从数据流的角度来看

非直接内存作用链:

本地IO-》直接内存-》非直接内存-》直接内存-》本地IO

直接内存作用链

本地IO-》直接内存-》本地IO

直接内存使用场景

  • 有很大的数据需要存储,它的生命周期很长
  • 适合频繁的IO操作,例如网络并发场景

浅谈HotSpot逃逸分析

JIT

即时编译技术(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善编译语言性能的技术。在HotSpot实现中有多种选择:C1,C2、C1+C2,分别对应Client、Server和分层编译。

  1. C1编译速度快,优化方式比较保守
  2. C2编译速度慢,优化方式比较激进
  3. C1+C2在开始阶段采用C1编译,当代码运行到一定热度之后采用C2重新编译

逃逸分析

逃逸分析并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其他优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸

  1. 方法逃逸,当一个对象在方法中定义以后,作为参数传递到其他方法中。
  2. 线程逃逸,如类变量和实例变量,可能被其他线程访问到。

如果不存在逃逸行为,可以使用,栈上分配,标量替换、同步消除解决问题

同步消除

线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,无法被其他线程访问到,那该对象的读写就不会存在竞争,则可以消除该对象的同步锁。通过-XX:EliminateLocks消除

标量替换

  1. 标量是指不可分割的量,如Java中基本数据类型和reference类型,相对的一个数据可以继续分解,称为聚合量
  2. 如果把一个对象拆散,将其成员变量恢复到基本类型来访问,就叫标量替换。(其实就是,不去创建这个对象,而是把这个对象的所有成员都创建出来,代替这个对象的使用)
  3. 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化以后,并不直接生成该对象,而是在栈上创建若干各成员变量;通过-XX:EliminateAllocations可以开启标量替换,-XX:+PrintEliminateAllocations查看标量替换情况

栈上分配

故名思意就是在栈上分配对象内存,其实目前HotSpot并没有实现真正意义上的栈上分配,实际上是标量替换。

编译阈值

即时编译器JIT只在代码段执行足够次数才会进行优化,在执行过程中不断收集各种数据,作为优化的决策,所以在优化完成之前,对象依然会在堆上分配内存。

那么一段代码需要执行多少次才会触发JIT优化呢?通常这个值由-XX:CompileThreshold参数进行设置

  1. 使用client编译器时,默认为1500
  2. 使用server编译器时,默认为10000

意味着如果方法调用次数或者循环次数达到这个阈值就会触发标准编译,更改CompileThreshold标志的值,将使用编译器提早(或延迟编译)。

Java常量池

Java中的常量池,实际上分为两种型态:静态常量池,运行时常量池

  1. 静态常量池,即.class文件中的常量池表,常量池表中包含,UTF-8字符串(即你声明的变量,方法,类等的名字),字面量(即你在方法中定义的基本数据类型,或者字符串数据类型的数值),符号引用(类、接口、方法、变量、字段)等,这个常量池表占用类文件的绝大部分空间
  2. 运行时常量池,类被加载到方法区时,会在方法区中创建一个该类对应的运行时常量池,保存类文件中的常量池表的数据。我们常说的常量池就是指运行时常量池。

由于运行时常量池在方法区中,我们可以通过JVM参数:-XX:PermSize,-XX:MaxPermSize来设置方法区大小,从而限制常量池的大小

数组多大放在JVM老年代

Java对象内存分配策略

Java技术体系中,所提倡的自动内存管理最终可以归结为自动化的解决了两个问题:给对象分配内存,回收分配给对象的内存

对象分配内存,往大方向将,就是在堆上分配,对象主要在新生代的Eden区上分配,如果启动了本地线程分配缓存,将按线程在Eden区中的TLAB上分配内存。少数情况也可能直接分配在老年代中,分配的规则不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置

下面的分配策略基于Serila/SerialOld垃圾收集器(ParNew/SerialOld收集器效果也一直)下的内存分配。

1、对象优先在Eden分配

大多数情况下,对象在新生代Eden中分配。当Eden区没有足够的空间进行分配时,会触发与一次MinorGC

2、大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组

大对象对虚拟机的内存分配来说就是一个坏消息(比遇到一个大对象更坏的消息就是遇到很多朝生夕死的大对象,写程序时应该避免),经常出现大对象容易导致内存还有不少空间时 就提前触发垃圾收集(FullGC)。

JVM提供一个-XX:PretenureSizeThreshold参数,令大于这个设置的对象直接在老年代分配。这样做的目的是为了避免,Eden区及两个Survivor区之间发生大量的内存复制。

3、长期存活的对象将进入老年代

4、动态对象年龄判定

为了能更好的适应不同程序的内存状况,虚拟机并不是永远的要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代。

  • 动态对象年龄判定:晋升之前,会遍历一次对象,看对象的分代年龄,只要某一个年龄在所有对象中占了50%,这个年龄就会成为新的晋升年龄

5、空间分配担保

在发生MinorGC之前,会先去检测老年代中剩下的内存够不够分配年轻代晋升的对象,如果不够,去查看空间担保分配机制有没有开启,如果开启了,就看老年代中剩下的内存够不够以往晋升的平均值,如果不够,触发FullGC,如果够直接将对象晋升(这种情况会冒一定风险)。JDK1.8,空间分配担保默认开启。

如果一个对象想在GC中生存一次怎么办

重写这个类的finalize()方法即可,当一个对象通过GCRoot不可达时,也就是这个对象将被垃圾回收器回收的时候,如果这个对象的类重写了finalize()方法,这个对象会被放入FQueue队列中,由其他线程执行他的finalize()方法,执行完finalize()方法之后,GC会判断这个对象是否可达,如果依然不可达,就将这个对象清除。

System.gc

  • system.gc其实是做一次full gc
  • system.gc会暂停整个进程
  • system.gc一般情况下我们要禁掉,使用-XX:+DisableExplicitGC
  • system.gc在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent来做一次稍微高效点的GC(效果比FullGC好)
  • system.gc最常见的场景是RMI/NIO下的堆外内存分配

system.gc中的问题

  • 为什么CMS GC下-XX:+ExplicitGCInvokesConcurrent这个参数加了之后会比真正的FullGC好?
  • 它如何做到暂停整个进程?
  • 堆外内存分配为什么有时候要配合System.gc?

JDK中的System.gc()是native方法

使用-XX:+DisableExplicitGC会使System.gc方法失效

使用-XX:+ExplicitGCInvokesConcurrent,会打开并行FullGC,注意设置并行FullGC不可以设置-XX:+DisableExplicitGC

并行FullGC相对于正常的FullGC效率高在哪里

VM Thread 做什么的?

"VM Thread" 是 JVM 自身启动的一个线程, 它主要用来协调其它线程达到安全点(Safepoint). 需要达到安全点的线程主要有: Stop the world 的 GC, 做 thread dump, 线程挂起以及偏向锁的撤销.
一般在 Thread dump 中, 它的表现为下面的样子, 没有线程栈信息:

1、STW

在JVM中有一个线程VMThrea,它会不断轮询它的队列,这个队列里主要存的是一些VM_operation的动作,比如最常见的就是内存分配失败要求做GC操作的请求等。在对GC这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不在执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的Stop the word(STW)

2、CMS GC

CMSGC,分为两种模式BackgroundGC和ForeGroundGC

  • BackGroundGC:后台GC,不影响正常业务线程跑,触发条件比如说old的内存占比超过多少的时候就可能触发一次backgroundGC。这个过程会经历CMSGC的所有阶段。效率相对来说比较高。但是foregroundGC不一样,它发生的场景比如业务线程请求分配内存,但是内存不够了,于是可以触发一次CMS gc,这个过程就必须要等内存分配到了业务线程才能继续往下走,因此整个过程必须是STW的。但是为了提高效率,它并不是每个阶段都走,只走其中一些极端。

如何触发栈、堆、方法区的内存溢出

  • 栈:写一个方法递归调用自己
  • 堆:写一个死循环一直创建对象
  • 方法区:使用String.intern()触发

为什么用元空间来代替永久代

元空间和永久代是方法区在不同jdk版本的不同实现。他们最大的区别是,永久代在运行时数据区,而元空间在直接内存中。

元空间的容量只受可用的本地内存限制。

代替原因:

  • 字符串存在永久代中,容易出现性能问题和内存溢出
  • 类及方法的信息等比较很难确定其大小,因此对于永久代的大小指定很困难,太小容易出现永久代溢出,太大则容易出现老年代溢出
  • 永久代会为GC带来不必要的复杂度,且回收效率偏低

栈是运行时的单位,而堆是存储的单位

栈解决的是运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。

在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,所以里面存储的信息都是跟当前线程相关的。包括局部变量表,操作数栈,动态链接,方法返回信息;而堆只负责存储对象信息。

为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗

  1. 从软件设计角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
  2. 堆与栈的分离,使得堆中的内容可以被多个栈共享,即多个线程访问一个对象。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(共享内存),另一方面,堆中从共享常量和缓存可以被所有栈访问,节省了空间。
  3. 栈因为运行时的需要保存了系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需要记录堆的一个地址即可。
  4. 面向对象就是栈和堆的完美结合。

为什么不把基本类型放在堆中呢

堆中存储的是对象,栈中存储的是基本数据类型和对象引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是栈中的数据都是固定的,比如对象引用就是一个4字节的数据。这就是堆栈分离的好处。

基本数据类型占用的空间基本是1-8字节,这个内存空间是比较少的。引用数据类型放在堆内存中是因为引用数据类型需要动态增长。而基本数据类型的内存大小不会表,所以把基本数据类型存在堆中没有意义,还会浪费空间(需要一个引用引用该数据)。

Java中的参数传递时传值呢还是传引用

明确一点,Java中只存在值传递,涉及到参数传递的,就是调用方法,而方法是在栈中运行的。栈中只有基本数据类型和对象引用。即没有对象,所以无法做到传引用。对象引用的传递,可以理解称一个引用值的传递,不是引用的传递。

堆和栈中,栈是程序运行最根本的东西,程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得Java的垃圾回收成为可能。

SafePoint是什么

字面意思,安全点。JVM中有个VMThread, 它主要用来协调其它线程达到安全点,比如垃圾收集线程,当垃圾收集线程启动时,需要其他的业务线程进入安全点。VMThread才能执行。

KYLO的JVM知识总结相关推荐

  1. jvm面试2 jvm如何加载java代码? JVM知识重点:内存模型和GC

    jvm如何加载java代码? native方法forName0 JVM知识重点:内存模型和GC' 注意:jvm是一个内存中的虚拟机 下面是Class类中,我们常用的forName方法 @CallerS ...

  2. 你不得不了解的jvm知识,还不看?

    前言:一个丰富的jvm知识图谱

  3. 这份美团架构师讲解的JVM知识,让我疫情期间,成功拿下阿里offer

    写在前面 话不多说,直奔主题 每一个Java 开发人员都知道字节码由JRE (Java运行时环境)执行.但许多人不知道JRE是Java虚拟机(JVM)的实现, 它负责分析字节码.解析并执行代码.作为一 ...

  4. 关于Jvm知识看这一篇就够了

    2016年左右的时候读了周志明<深入理解Java虚拟机:JVM高级特性与最佳实践>,读完之后受益匪浅,让我对Java虚拟机有了一个完整的认识,这是Jvm书籍中最好的读物之一. 后来结合实际 ...

  5. JAVA之JVM知识汇总

    Java虚拟机(JVM)你只要看这一篇就够了!_Java笔记-CSDN博客_jvm JVM GC 机制与性能优化_橙子wj的博客-CSDN博客 为什么新生代内存需要有两个Survivor区_橙子wj的 ...

  6. JVM知识(三):内存模型和可见性

    这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况.来聊聊java线程对一个变量的更新怎么通知另一个线程,及volatile的作用和 ...

  7. Java必突-JVM知识专题(一): Java代码是如何跑起来的+类加载到使用的过程+类从加载到使用核心阶段(类初始化)+类加载的层级结构+什么是JVM的内存区域划分?Java虚拟机栈、Java堆内存

    前言: 该章节知识点梳理:本文主要是入门和了解jvm,不做深入 1.Java代码是如何运行起来的? 2.类加载到使用的过程? 3.验证准备和初始化的过程? 4.类从加载到使用核心阶段:初始化.类加载器 ...

  8. JVM知识梳理(二)之垃圾收集器与内存分配策略

    目录 一.如何判断一个对象已死? 1.引用计数器算法 2.可达性分析 一次对象自我拯救的演示 二.垃圾收集算法 1. 分代收集理论 2. 标记-清除算法 3. 标记-复制算法 4. 标记-整理算法 三 ...

  9. Android程序员需掌握的JVM知识

    什么是JVM JVM 全称 Java Virtual Machine,它是一种规范.JVM 是一个虚拟化的操作系统,类似于 Linux 或者 Windows 的操作系统,只是它架在操作系统上,接收字节 ...

  10. Java三大版本,JDK,JER,JVM知识

    JavaSE: 标准版,基础核心版. 是为开发普通的桌面和商务应用程序提供的可解决的方式,是后面两个技术的基础,可以完成一些桌面应用程序的开发. JavaME: 小型版,是为了开发电子消费产品和嵌入式 ...

最新文章

  1. 边缘计算:5G 时代的万亿市场
  2. auto make System.map to C header file
  3. Git remote 修改源
  4. python最简单的架构_Python实现简单状态框架的方法
  5. 攻防世界-Misc-_Aesop_secret
  6. js css加载器,webpack的CSS加载器的使用
  7. Packet Capture
  8. Jeecg 切换默认首页方法
  9. Java运行时出现”the serializable class drawline does not declare a static final serialversio”...
  10. Redis分布式锁及分区
  11. 安装bootcamp时提示“找不到$winpedriver$文件夹,请验证该文件夹是否和bootcamp处于同一文件夹内?”...
  12. Atitit .html5刮刮卡的gui实现总结
  13. fiddler限速_fiddler限制网速
  14. Vmware 安装安卓x86虚拟机并运行APP
  15. 读书笔记《计算机是怎样跑起来的》
  16. amd速龙黑苹果内核补丁_替换AMD内核安装10.9.2超级懒人版成功
  17. 滚滚长江东逝水历史的天空
  18. linux电脑开机慢,Ubuntu 启动速度慢的解决方法
  19. 路由器WiFi密码更改及隐藏操作
  20. SSM项目大作业——网上订餐系统

热门文章

  1. JLINK V9项目启动【jlink接口定义】【开启VCOM(虚拟串口)功能】
  2. 小米8 青春版root时无法检测到手机
  3. pdf打印机如何加入背景
  4. 您有一张H5新年贺卡未领取
  5. 基于朴素贝叶斯分类器的西瓜数据集 2.0 预测分类_朴素贝叶斯算法知识点总结...
  6. 如何在微信小程序中使用echarts绘制地图(微信小程序制作疫情数据地图)
  7. Django搭建后台管理系统
  8. 吴恩达机器学习和深度学习视频和笔记
  9. 华硕电脑开启无线服务器,华硕ASUS路由器无线中继模式设置步骤图解
  10. usb3.0传输速度测试软件,Win10系统如何测试usb3.0设备传输速度