JVM

JVM

  1. JVM整体
    JVM运行过程

不仅跨平台还跨语言

运行时数据区
定义:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域

类型:程序计数器,虚拟机栈,本地方法区,java堆,方法区(运行时常量池),直接内存

线程

线程私有 红色圈起来

线程共享 红线

方法区:class 静态遍历 常量

堆:几乎所有对象都在堆中进行分配

  1. 线程私有区域
    程序计数器
    指向当前线程正在执行的字节码指令的地址(时间片轮转的时候再切回来直接运行上次的末尾) 唯一不会 out of memory的区域

虚拟机栈
存储当前线程运行方法所需的 数据,指令,返回地址

栈帧

局部变量表

方法里面的 局部变量(八大基础数据类型) + 引用

操作数栈

方法的操作,执行。

动态连接

完成出口

大小限制 -Xss 1M 不同的虚拟机大小不一样

局部变量表和操作数栈配合使用

Java 解释执行是栈(操作数栈),C语言是寄存器运算

优点 缺点?

C语言快 基于硬件 移植性差(make install)编译成机器码

java 兼容性好,效率偏低

动态连接
再代码执行的时候,栈帧里面的动态连接决定你应该怎么工作(符号引用 到 具体引用)

多态

静态分派

动态分派

返回地址
方法执行完了就出栈

正常返回,调用程序计数器中的地址

异常返回, 异常处理表

-Xss
默认值取决于平台

本地方法栈
本地方法栈用来 保存native方法的信息

当一个JVM创建的线程调用native方法后,JVM不再为其虚拟机栈中创建栈帧,JVM只是简单的动态连接并直接调用native方法

虚拟机规范无强制规定,各版本虚拟机自由实现HotSpot直接把本地方法栈和虚拟机栈合二为一

  1. 线程共享区域
    方法区
    不同的版本有不同的实现。

<=jdk1.7 永久带 先划分堆中 新生带 和 老年带

=jdk1.8 元空间 机器内存---不受限制 不被清理

好处:方便扩展

坏处: 挤压堆空间

类信息

常量

静态变量

即时编译期编译后的代码

Java堆
对象实例(几乎所有)

数组

划分 连续的内存区域

Eden 新生待

From

To

Terured 老年带

Java堆的大小参数设置

—Xmx 堆区内存可被分配的最大上限

—Xms 堆区内存初始内存分配的大小

面试题:为什么不用一份,2个区(堆,方法区)分?

动静态分离

堆:对象,数组。频繁回收。

方法区:回收难度大。

  1. 直接内存
    不是虚拟机运行时数据区的一部分,也不是java虚拟机规范定义的内存区域

使用了NIO,这块区域会被频繁使用,在Java堆内可以用directByteBuffer对象直接引用并操作

这块内存不受Java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常

  1. 从底层深入理解运行时数据区

  2. 申请内存 把栈 方法区 堆 内存大小申请出来

  3. 类加载器 —class进入方法区

  4. 常量和静态变量放入方法区

  5. 虚拟机栈工作 main方法入虚拟机栈 main方法栈帧

  6. 栈帧方法执行 堆中new出对象 引用加入局部变量表

  7. 垃圾收集器 被调用,有的对象被回收到老年代

  8. 深入辨析堆和栈
    功能

以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量,以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放

而堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。

线程独享和线程共享

栈内存归属 于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。

堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问

空间大小

栈内存远远小于堆,栈的深度是有限的,可能发生OutofMemory

  1. 内存溢出
    栈溢出 递归调用

堆溢出

方法区溢出

本机直接内存溢出

  1. 虚拟机优化技术
    编辑优化技术

方法内联

栈的优化技术

栈帧之间的数据共享

常量池在方法区还是堆?

JDK1.8 运行时常量池(字符串部分 -- 放入堆)。静态(class) ---方法区

类成员变量(属性)放在堆中,属于实例里面的东西

jvm一开始就加载了异常类

类的卸载

类 所有实例,都被回收

加载的该类的classload已经被回收

该类java.lone.class对象,没有任何地方被引用,无法通过反射访问该类的方法

  1. 虚拟机中对象创建的过程

  2. 对象的分配
    虚拟机遇到一条new指令时,首先检查是否被类加载器加载,如果没有,那必须先执行相应的类加载过程。

类加载就是把class加载到JVM的运行时数据区的过程(类加载后面有专门的专题讲)。

1)检查加载
首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用(符号引用 :符号引用以一组符号来描述所引用的目标),并且检查类是否已经被加载、解析和初始化过。

2)分配内存
接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

指针碰撞
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。

空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。

对象占的内存时连续的

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

如果是Serial、ParNew等带有压缩的整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效。

如果是使用CMS这种不带压缩(整理)的垃圾回收器的话,理论上只能采用较复杂的空闲列表。

并发安全
除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

CAS机制
解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;

分配缓冲
另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),JVM在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。

TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。

参数:

-XX:+UseTLAB

允许在年轻代空间中使用线程本地分配块(TLAB)。默认情况下启用此选项。要禁用TLAB,请指定-XX:-UseTLAB。

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

img

3)内存空间初始化
(注意不是构造方法)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4)设置
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes在Java hotspot VM内部表示为类元数据)、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

5)对象初始化
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化(构造方法),这样一个真正可用的对象才算完全产生出来。

  1. 对象的内存布局
    img

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:

对象头(Header)、

实例数据(Instance Data)

对齐填充(Padding)。

对象头包括两部分信息,

第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

如果对象是一个java数组,那么在对象头中还有一块用于记录数组长度的数据。

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

img

  1. 对象的访问定位
    建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。

  2. 句柄
    如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

  3. 直接指针
    如果使用直接指针访问, reference中存储的直接就是对象地址。

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

对Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的。

  1. 判断对象的存活
    在堆里面存放着几乎所有的对象实例,垃圾回收器在对对进行回收前,要做的事情就是确定这些对象中哪些还是“存活”着,哪些已经“死去”(死去代表着不可能再被任何途径使用得对象了)

  2. 引用计数法
    在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效时,计数器减1.

Python在用,但主流虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,这样做影响效率,

img

img

在代码中看到,只保留相互引用的对象还是被回收掉了,说明JVM中采用的不是引用计数法。

  1. 可达性分析
    (面试时重要的知识点,牢记)

来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

作为GC Roots的对象包括下面几种:

l 虚拟机栈(栈帧中的本地变量表)中引用的对象。

l 方法区中类静态属性引用的对象。

l 方法区中常量引用的对象。

l 本地方法栈中JNI(即一般说的Native方法)引用的对象。

l JVM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。

l 所有被同步锁(synchronized关键)持有的对象。

l JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等

l JVM实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代时)

以上的回收都是对象,类的回收条件:

注意 Class要被回收,条件比较苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):

1、 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。

2、 加载该类的ClassLoader已经被回收。

3、 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

4、 参数控制:

img

还有一个废弃的常量,这个是对象的回收非常相似,比如:假如有一个字符串“king”进入常量池。

  1. Finalize方法
    即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize中去拯救。

代码演示:

img

运行结果:

img

可以看到,对象可以被拯救一次 (finalize执行第一次,但是不会执行第二次)

代码改一下,再来一次。

img

运行结果:

img

对象没有被拯救,这个就是finalize方法执行缓慢,还没有完成拯救,垃圾回收器就已经回收掉了。

所以建议大家尽量不要使用finalize,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议大家忘了finalize方法!因为在finalize方法能做的工作,java中有更好的,比如try-finally或者其他方式可以做得更好**

  1. 各种引用
    强引用
    一般的Object obj = new Object() ,就属于强引用。在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象。

软引用 SoftReference
一些有用但是并非必需,用软引用关联的对象,系统将要发生内存溢出( ,这些对象就会被回收(如果这次回收后还是没有足够的空间,才会抛出内存溢出)。参见代码:

VM参数 -Xms10m -Xmx10m -XX:+PrintGC

img

运行结果

img

例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。

弱引用 WeakReference
一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。

参看代码:

img

img

注意:软引用 SoftReference和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。

实际运用(WeakHashMap、ThreadLocal)

虚引用 PhantomReference
幽灵引用,最弱(随时会被回收掉)

垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。

14.学习垃圾回收的意义
Java与C++等语言最大的技术区别:自动化的垃圾回收机制(GC)

为什么要了解GC和内存分配策略

1、面试需要

2、GC对应用的性能是有影响的;

3、写代码有好处

栈:栈中的生命周期是跟随线程,所以一般不需要关注

堆:堆中的对象是垃圾回收的重点

方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点

  1. 对象的分配策略
    栈上分配
    跟随线程

没有逃逸
即方法中的对象没有发生逃逸。

逃逸分析的原理:分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用,比如:调用参数传递到其他方法中,这种称之为方法逃逸,甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。

从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。

如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高JVM的效率。

逃逸分析代码
public class EscapeAnalysisTest {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < 50000000; i++) {
allocate();
}
System.out.println((System.currentTimeMillis() - start) + " ms");
Thread.sleep(600000);
}

static void allocate() {
MyObject myObject = new MyObject(2020, 2020.6);
}

static class MyObject {
int a;
double b;

MyObject(int a, double b) {
this.a = a;
this.b = b;
}
}
}
这段代码在调用的过程中 myboject这个对象属于全局逃逸,JVM可以做栈上分配

然后通过开启和关闭DoEscapeAnalysis开关观察不同。

开启逃逸分析(JVM默认开启)

img

查看执行速度

img

关闭逃逸分析

img

查看执行速度

img

测试结果可见,开启逃逸分析对代码的执行性能有很大的影响!那为什么有这个影响?

逃逸分析
如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。

采用了逃逸分析后,满足逃逸的对象在栈上分配

img

没有开启逃逸分析,对象都在堆上分配,会频繁触发垃圾回收(垃圾回收会影响系统性能),导致代码运行慢

img

代码验证

开启GC打印日志

-XX:+PrintGC

开启逃逸分析

img

可以看到没有GC日志

关闭逃逸分析

img

可以看到关闭了逃逸分析,JVM在频繁的进行垃圾回收(GC),正是这一块的操作导致性能有较大的差别。

对象优先在Eden区分配
虚拟机参数:

-Xms20m

-Xmx20m

-Xmn10m

-XX:+PrintGCDetails

-XX:+PrintGCDetails 打印垃圾回收日志,程序退出时输出当前内存的分配情况

注意:新生代初始时就有大小

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

大对象直接进入老年代
-Xms20m

-Xmx20m

-Xmn10m

-XX:+PrintGCDetails

-XX:PretenureSizeThreshold=4m

-XX:+UseSerialGC

PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效。

最典型的大对象是那种很长的字符串以及数组。这样做的目的:

1.避免大量内存复制

2.避免提前进行垃圾回收,明明内存有空间进行分配。

长期存活对象进入老年区
如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1,对象在Survivor区中每熬过一次 Minor GC,年龄(对象头 age)就增加1,当它的年龄增加到一定程度( ),CMS是6时,就会被晋升到老年代中。

-XX:MaxTenuringThreshold调整

对象年龄动态判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

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

注意:内存移动是必须实打实的移动(复制),不能使用指针玩。

专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)

标记-清除算法(Mark-Sweep)
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

它的主要不足空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-整理算法(Mark-Compact)
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。

JVM中常见的垃圾收集器
分代收集的思想
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

请记住下图的垃圾收集器和之间的连线关系。

img

收集器 收集对象和算法 收集器类型 说明 适用场景
Serial 新生代,复制算法 单线程 简单高效; 适合内存不大的情况;
ParNew 新生代,复制算法 并行的多线程收集器 ParNew垃圾收集器是Serial收集器的多线程版本 搭配CMS垃圾回收器的首选
Parallel Scavenge 吞吐量优先收集器 新生代,复制算法 并行的多线程收集器 类似ParNew,更加关注吞吐量,达到一个可控制的吞吐量; 本身是Server级别多CPU机器上的默认GC方式,主要适合后台运算不需要太多交互的任务;

收集器 收集对象和算法 收集器类型 说明 适用场景
Serial Old 老年代,标记整理算法 单线程 Client模式下虚拟机使用
Parallel Old 老年代,标记整理算法 并行的多线程收集器 Parallel Scavenge收集器的老年代版本,为了配合Parallel Scavenge的面向吞吐量的特性而开发的对应组合; 在注重吞吐量以及CPU资源敏感的场合采用
CMS 老年代,标记清除算法 并行与并发收集器 尽可能的缩短垃圾收集时用户线程停止时间;缺点在于: 1.内存碎片 2.需要更多cpu资源 3.浮动垃圾问题,需要更大的堆空间 重视服务的响应速度、系统停顿时间和用户体验的互联网网站或者B/S系统。互联网后端目前cms是主流的垃圾回收器;
G1 跨新生代和老年代;标记整理 + 化整为零 并行与并发收集器 JDK1.7才正式引入,采用分区回收的思维,基本不牺牲吞吐量的前提下完成低停顿的内存回收;可预测的停顿是其最大的优势; 面向服务端应用的垃圾回收器,目标为取代CMS

并行:垃圾收集的多线程的同时进行。

并发:垃圾收集的多线程和应用的多线程同时进行。

注:吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间)

垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间

垃圾回收器工作示意图
Serial/Serial Old
最古老的,单线程,独占式,成熟,适合单CPU 服务器

-XX:+UseSerialGC 新生代和老年代都用串行收集器

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old

-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old

ParNew
和Serial基本没区别,唯一的区别:多线程,多CPU的,停顿时间比Serial少

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old

Parallel Scavenge(ParallerGC)/Parallel Old
关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

Concurrent Mark Sweep (CMS)
img

收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

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

l 初始标记-短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。

l 并发标记-和用户的应用程序同时进行,进行GC Roots追踪的过程

l 重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

l 并发清除

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

-XX:+UseConcMarkSweepGC ,表示新生代使用ParNew,老年代的用CMS

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

G1
img

-XX:+UseG1GC

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

内存布局:在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

n 新生代GC

回收Eden区和survivor区,回收后,所有eden区被清空,存在一个survivor区保存了部分数据。老年代区域会增多,因为部分新生代的对象会晋升到老年代。

n 并发标记周期

初始标记:短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,产生一个全局停顿,都伴随有一次新生代的GC。

根区域扫描:扫描survivor区可以直接到达的老年代区域。

并发标记阶段:扫描和查找整个堆的存活对象,并标记。

重新标记:会产生全局停顿,对并发标记阶段的结果进行修正。

独占清理:会产生全局停顿,对GC回收比例进行排序,供混合收集阶段使用

并发清理:识别并清理完全空闲的区域,并发进行

n 混合收集

对含有垃圾比例较高的Region进行回收。

G1当出现内存不足的的情况,也可能进行的FullGC回收。

G1中重要的参数:

-XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间。

-XX:ParallerGCThreads:设置GC的工作线程数量

Stop The World现象
GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数。

内存泄漏和内存溢出辨析
内存溢出:实实在在的内存空间不足导致;

内存泄漏:该释放的对象没有释放,多见于自己使用容器保存元素的情况下。

未来的垃圾回收
ZGC通过技术手段把stw的情况控制在仅有一次,就是第一次的初始标记才会发生,这样也就不难理解为什么GC停顿时间不随着堆增大而上升了,再大我也是通过并发的时间去回收了

关键技术

有色指针(Colored Pointers

加载屏障(Load Barrier

常量池与String
常量池有很多概念,包括运行时常量池、class常量池、字符串常量池。

虚拟机规范只规定以上区域属于方法区,并没有规定虚拟机厂商的实现。

严格来说是静态常量池和运行时常量池,静态常量池是存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。
运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。
这两个常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。

字面量:

给基本类型变量赋值的方式就叫做字面量或者字面值

比如:int i=120; long j=10L;

符号引用:包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。

直接引用:具体对象的索引值。

重要其实还是字符串。

String 对象是如何实现的?

原文地址:https://www.cnblogs.com/AronJudge/p/14617695.html