一篇JVM

JDK1.8 Java虚拟机 Java Virtual Machine(JVM)>

1.JVM的位置:

JVM是运行再操作系统(windows、Linux、Mac)之上的

2.内存结构:

2.1.程序计数器:

  • 作用: 是记住下一条jvm指令的地址
  • 特点: 线程私有的,不会出现内存溢出的情况

2.2.虚拟机栈 (Java Virtual Machine Stack):

栈:线程运行需要的内存空间
栈帧(Stack Frame):每个方法运行时需要的内存
线程私有的,每个线程只能有一个活动栈帧,对应着往前正在执行的方法

2.2.1.栈内存溢出(StackOverflowError)

原因:

  • 栈帧过多导致栈内存溢出:最有可能的就是方法递归调用产生这种结果。
  • 栈帧过大导致栈内存溢出:可能是局部变量太多,太大造成,但比较少见

存放:

  • 参数、局部变量表、返回地址

2.3.本地方法栈:

功能和特点都类似虚拟机栈,都是线程私有的以及都能抛出StackOverflowError异常。
和java虚拟机栈的区别在于,本地方法栈运行的都是由非java语言编写的方法,也就是native方法,

2.4.堆(Heap):

特点:

  • 线程共享的,堆内存中需要考虑线程安全的问题
  • 有垃圾回收机制

存放:

  • new对象、数组、成员变量
  • 1.7以后存储静态变量、字符串常量池(String Pool)
2.4.1.堆内存溢出(OOM):

原因:

  • 设置的堆内存太小,可以通过-Xmx8m(这里是8M)配置堆内存大小
  • 由于设计不够合理导致系统需要过多的内存,需要通过设计减少内存的使用。
2.4.2.堆内存诊断
  • jps工具:查看当前系统中有哪些java进程
  • jmap工具:查看堆内存占用情况
  • jconsole工具:图形界面的。多功能的检测工具,可以连续监测

2.5.方法区:

在JDK1.8以前,方法区被称为永久代,而JDK1.8废除了永久代,使用元空间替代永久代,而元空间不在虚拟机内存中实现,而是在本地内存中实现
特点:

  • 线程共享的。
  • 元空间的大小默认就是机器本地的物理内存,但是可以用-XX:MaxMetaspaceSize=8M手动设置元空间的内存大小(1.8以后)
  • 虚拟机启动时创建

存储:

  • 类信息、运行时常量池、字段、方法和构造函数等等
2.5.1.元空间内存溢出

一般来说元空间内存不太会溢出,出现原因:

  • 手动设置的元空间内存太小
  • 虚拟机加载了太多类信息
2.5.2.运行时常量池

程序要运行,需要把java文件编译成二进制字节码,二进制字节码包括:类基本信息、常量池、类方法定义,虚拟机指令

  • 常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 当类被加载时,它的常量池信息会放到运行时常量池,并把里面的符号地址变为真实地址
2.5.3.StringTable

存储位置:

  • 1.6存在永久代中,1.7以后为了提高回收效率存在堆内存中

结构:

  • 存储结构为HashTable,也就是数组+链表的形式,不允许扩容

特性:

  • 常量池中的字符串只是符号,第一次用到时才变成对象

  • 字符串池是为了避免重复创建字符串对象

  • 字符串变量的拼接原理是StringBuilder(1.8)

  • 字符串常量的拼接原理是编译期优化

  • intern方法,主动将字符串池中还没有的字符串对象放入字符串池

String s1 = new ("a) + new String("b");  // 字符串池[a 、 b]; 堆:new String("a")、new String("b")、new String("ab")
String s2 = s1.intern(); // 把new String("ab")对象放到字符串池中,并将字符串池中的对象返回
// s1==s2  

调优:

  • -XX:StringTableSize=800000桶个数,桶的个数即数组长度,桶越多,越不容易造成hash冲突,链表的节点越少,查询的越快,效率越高

3.垃圾回收(GC)

GC在执行过程中,会暂停其他的所有线程,Stop-the-World(STW)。GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有 高吞吐 、低停顿 的特点。
推荐看《图解垃圾回收机制

3.1.如何判断对象是否需要回收

3.1.1.引用计数算法

如果一个对象没有被任何引用指向,则可视之为垃圾。
每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为0就代表该对象死亡。但是如果对象之间相互引用,就一直无法回收,可能导致内存问题。

3.1.2.可达性分析算发

扫描堆中的对象,看是否能够沿着GC Roots对象为起点的引用链找到该对象,找不到,表示可以回收
----------------《简单易懂的可达性分析算法

3.1.3.GC Roots

GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象 )。

  • 方法区的静态属性引用的对象
  • 常量池引用的对象
  • 方法栈中栈帧本地变量表引用的对象
  • 本地方法栈中引用的对象
  • 被同步锁持有的对象
3.1.4.四种引用
  • 强引用:只要对象跟GC Roots有直接或间接强引用关系,就不能被回收
  • 软引用:由SoftReference创建的实例引用,就是软引用
		Student stu = new Student();
        SoftReference soft = new SoftReference(stu);

只有软引用在引用对象时,垃圾回收后,内存还是不足时,会被回收。
当引用的对象被回收后,SoftReference也是一个对象,SoftReference将会进入引用队列,可配合引用队列(ReferenceQueue )释放软引用本身

  • 弱引用:由WeakReference创建的实例引用,就是弱引用
		Student stu = new Student();
        WeakReference soft = new WeakReference(stu);

只有弱引用在引用对象时,不管内存够不够,都会发生回收。当引用的对象被回收后,WeakReference也是一个对象,WeakReference将会进入引用队列,可配合引用队列释放软引用本身

  • 虚引用:用PhantomReference 实现,
		Student stu = new Student();
        ReferenceQueue<Student> rq = new ReferenceQueue<>();
        PhantomReference<Student> pr = new PhantomReference<>(stu, rq);

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。由于get方法返回永远是null,无法通过虚引用来获取对象,所以必须配合引用队列使用

3.2.回收算法

3.2.1.标记清除算法

先检查对象是否可回收,并将可回收的对象标记出来,标记完毕后再进行回收
缺点:

  • 标记和清除两个过程的效率都不高
  • 清除后的空间不会进行移动,因此清除后会产生大量的不连续的空间碎片,空间碎片太多可能会导致以后再需要分配较大的内存时,无法找到足够大并且连续的内存空间而不得不提前进行垃圾回收
3.2.2.标记整理法

一样先检查对象是否可回收,并将可回收的对象标记出来,之后不是直接清除,而是让所有存活的内存都紧凑在一端,然后再清理掉边界的内存,该垃圾回收算法适用于对象存活率高的场景(老年代)
优点是不会产生空间碎片,但由于需要较多的复杂操作,效率将会变低

3.2.3.复制法

会将可用内存空间分成相等容量两块区域(From区To区),每次都先使用FROM区,等FROM区用完了,就将FROM区存活的对象复制到TO区,再将FROM区进行清除,清除完后再交换两个区的地址
优点也是不会产生空间碎片,实现简单,效率高。但缺点是会占用双倍的内存空间

3.2.4.分代回收法

在虚拟机中不同对象的生命周期是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。
分代回收是采用标记清除、标记整理、复制法三中算法结合的算法。
堆内存一般分为新生代(Young Generation)、老年代(Old Generation),用来存放不同生命周期的对象

3.2.4.1.新生代(Young Generation)

新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。
新生代内存按8:1:1的比例分为一个伊甸区(eden)两个幸存区(FROM区和TO区) 也叫Survivor区
新生代的GC叫MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。

3.2.4.2.老年代(Old Generation)

老年代存放的都是一些生命周期较长的对象,相对垃圾清理的频率比较低。老年代的内存比新生代内存要大。
当老年代满时会触发Major GC(Full GC)

3.3.回收类型

3.3.1.Minor GC

新生成的对象会先放在新生代中的eden区,当eden区的内存块满时,触发Minor GC,Minor GC对新生代进行回收,不会影响到年老代。
也会触发STW,但是时间比较短

  1. 先将Eden区中存活的对象复制到幸存区中的s0区,回收Eden区,存活的对象被标记为存活1次
  2. 下一次触发Minor GC时,会将Eden区和s0区中存活的对象复制到s1区中,然后清除Eden区和s0区,存活对象的存活次数+1,然后交换s1和s0区的地址(这里s0区和s1区就是From区To区,在幸存区中空的区就是TO区,有对象的就是FROM区)。
  3. Minor GC重复以上的过程,当其中有对象存活的次数到达15次时(-XX:MaxTenuringThreshold默认是15),就会从新生代晋升到老年代
  4. 随着MinorGC一次又一次的进行,老年代的对象越来越多,当老年代的空间不足时会先尝试触发Minor GC,如果仍空间不足,就会触发Full GC
3.3.2.Full GC

对整个堆进行回收,包括新生代和老年代,由于Full GC要对整个堆空间进行垃圾回收,所有要比Minor GC要慢,STW时间更长。
也可通过调用System.gc()触发Full GC。

4.垃圾回收器

4.1.串行垃圾回收器

单线程的垃圾回收器,在单核CPU的环境下,效率更高,串行的垃圾收集器有两种,SerialSerial Old
新生代采用Serial,用的是复制算法,老年代使用Serial Old,用的是标记整理法
-XX:+UseSerialGC=Serial+SerialOld

4.2.吞吐量优先回收器

多线程的垃圾回收器,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),也就是高效率利用cpu时间,但由于回收的时候是多线程的一起回收的,那么在垃圾回收时CPU使用率会升高,可以通过-XX:ParallelGCThreads参数调整垃圾回收时线程的数量。
-XX:MaxGCPauseMillis 可以设置最大停顿时间
-XX:GCTimeRatio 设置吞吐量大小
XX:+UseAdaptiveSizePolicy 随着GC,会动态调整新生代的大小,Eden,Survivor比例等,以提供最合适的停顿时间或者最大的吞吐量

  • Parallel Scavenge: 针对新生代的垃圾回收器,采用**“复制”算法**
  • Parllel Old: 针对老年代的垃圾回收器,采用标记整理算法

4.3.时间响应优先

  • ParNew: Serial收集器的多线程版本,默认开启的收集线程数和cpu数量一样,用于新生代收集,复制算法
  • Concurrent Mark Sweep(CMS): 并发的垃圾回收器,是一种以获得最短回收停顿时间为目标的收集器,作用于老年代,采用标记清除算法 ,它的运作过程分为 4 个步骤:
    1. 初始标记(CMS initial mark)仍会触发STW,但仅标记GC Roots对象,速度很快。
    2. 并发标记(CMS concurrent mark)GC Roots Tracing,可以和用户线程并发执行。
    3. 重新标记(CMS remark)在并发标记过程中,有些对象可能产生变动,所以要重新标记,这个阶段的时间一般会比初标记稍长一些,但远比并发标记的时间短。也会触发STW
    4. 并发清除(CMS concurrent sweep)清除对象,可以和用户线程并发执行。

时间响应优先
缺点:
由于CMS是并发的,所以堆CPU的资源很敏感;
因为是基于标记清除算法,所以不可避免会产生空间碎片;
清除的过程也是并发的,所以在清除对象时,还会产生新的垃圾,这些垃圾被称为浮动垃圾,而且只能通过下一次GC清除这些垃圾

4.3.1.G1(Garbage First)

在JDK1.9中,G1成了默认回收器,并且废弃了CMS回收器。可以通过-XX:+UseG1GC参数指定使用G1回收器。
可以通过-XX:MaxGCPauseMillis设定最大STW停顿时间,默认250,不一定能达到,但G1会尽可能的达到设定的停顿时间。
G1的堆区在分代的基础上,引入了分区(Region) 的概念。G1将堆分成了许多大小相等的Region,各个Region被标记为 E(Eden区)、S(Survivor区)、O(老年代) 和 H(Humongous巨型对象区域),估计每个Region中垃圾的比例,优先回收垃圾多的Region,所有被叫做Garbage First
G1无需回收整个堆,而是选择一个Collection Set(CS)。

  • 回收阶段
    1. 初始标记(Initial Marking)同CMS,会触发STW,但仅标记GC Roots对象,速度很快。
    2. 并发标记(Concurrent Marking)是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
    3. 最终标记(Final Marking)在并发标记过程中,有些对象可能产生变动,所以要重新标记,也会触发STW,但是可并行执行
    4. 筛选回收(Live Data Counting and Evacuation)对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来确定回收计划。

特点:
能充分利用多CPU、多核环境下的硬件优势;
可以并行来缩短(Stop The World)停顿时间;
也可以并发让垃圾收集与用户程序同时进行;
分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象;
应用场景可以面向服务端应用,针对具有大内存、多处理器的机器;
采用标记-整理 + 复制算法来回收垃圾

4.4.小结

垃圾回收器分类作用位置使用算法特点
Serial串行新生代复制算法响应速度优先
Serial Old串行老年代标记整理法响应速度优先
Parallel并行新生代复制算法吞吐量优先
Parllel Old并行老年代标记整理法吞吐量优先
ParNew并行新生代复制算法响应速度优先
CMS并发老年代标记清除法响应速度优先
G1并行新生代复制算法响应速度优先

5.类加载机制

编写的.java文件经过java编译器编译成.class文件,class文件中保存着一些虚拟机指令,将.class加载到虚拟机内存的过程叫类加载。

5.1.类加载过程

5.1.1.加载(Loading)

虚拟机将类信息加载到元空间(方法区),并利用字节码文件在堆内存中创建一个Class对象,用来做方法区中类信息数据的接口。

5.1.2.链接(Linking)

第二大阶段,链接又分为三个小阶段

  1. 验证(Verifivation)
    验证类是否符合JVM规范,安全性检查,例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型、语义分析、字节码校验等
  2. 准备(perparation)
    为static分配空间,然后设置默认值,赋值在初始化阶段,如果static是final的基本类型或者字符串常量,那么编译阶段值就确定了,赋值会在准备阶段完成
  3. 解析(Resolution)
    在未解析之前,对象之间的引用的是一种符号引用,并没有什么实际的意义,所以需要将常量池中的符号引用解析为直接引用,直接引用可以直接指向目标对象的内存地址。
5.1.3.初始化(Initialization)

初始化即调用() v ,虚拟机会保证这个类的构造方法的线程安全。如果在准备阶段static并不是final的基本类型,则会在这个阶段进行赋值

public class Loading {

    public static void main(String[] args) {
        System.out.println(A.str);   // 不会初始化
        System.out.println(A.i);    // 不会初始化
        System.out.println(A.class);  // 不会初始化
        System.out.println(new A[10]);  // 不会初始化

        System.out.println(A.i1);   // 会初始化
        System.out.println(B.i2);     // 子类初始化时,如果父类未初始化,父类会先初始化
        System.out.println(B.a);   // 子类访问父类静态变量,值初始化父类
    }

}


class A {
    final static int i = 20;   // 在准备阶段就已经赋值,不会初始化
    final static String str = "str";   // 在准备阶段就已经赋值,不会初始化
    final static Integer i1 = 10;  // 准备阶段未赋值,会初始化
    static int a = 30;    // 准备阶段未赋值,会初始化
    static {
        System.out.println("A init");
    }

}


class B extends A {
    static int i2 = 10;
    static {
        System.out.println("B init");
    }
}

5.2.类加载器

在java虚拟机中又3中类加载器:Bootstrap加载器、Extension加载器、System记载器
各个加载器各司其职,加载不同范围的类

名称范围说明
Bootstrap加载器JAVA_HOME/jre/lib无法直接访问
Extension加载器JAVA_HOME/jre/lib/ext上级为Bootstrap,显示为null
Application加载器classpath上级为Extension

5.3.双亲委派模式

所谓双亲委派模式,就是指调用类加载器的loadClass方法时,查找类的规则:
一个加载器在加载时,会先找上级加载器,如果存在上级加载器,就让上级器去加载,如果没有上级加载器或是上级加载器没找到类,就由自己加载,如果找不到类,就会报ClassNotFound异常

  • 优势
    通过这种层级关可以避免类的重复加载,当上级已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,由上往下加载,不会使JDK的类和自己写的类混淆

5.4.自定义加载器

步骤:

  1. 继承ClassLoader父类
  2. 重写findClass方法,注意不是loadClass,否则不会走双亲委派模式
  3. 读取类文件的字节码
  4. 调用父类的defineClass方法来加载
  5. 使用者调用该类加载器的loadClass方法

6.内存模型(Java Memory Model)

java内存模型,简称JMM。比如JVM在执行i = i +1操作的过程中,首先需要从主内存中读取i到工作内存中,再在工作内存中进行+1的操作,然后再写回主内存。
在这个过程中,如果是多线程环境下,就可能会造成一些线程安全问题,也就涉及到了JMM的三大特性。

6.1.JMM三大特性

  1. 原子性:一个或多个操作,要么全部执行,要么全部不执行。
  2. 可见性:只要有一个线程对共享变量的值做了修改,其他线程都将马上收到通知,立即获得最新值。
  3. 有序性:java指令在编译时经过优化,可能会出现重排序现象,有序性就是确保虚拟机禁止指令重排序。volatile 和synchronized 都可以确保有序性

7.VM参数

VM参数参考

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=n
晋升详情-XX:+PrintTenuringDistribution
打印GC详情-XX:+PrintGCDetails
Full GC前Minor GC-XX:+ScavengeBeforeFullGC

问题

垃圾回收是否涉及栈内存

不涉及,因为每个栈帧在方法执行完成之后会自动出栈

栈内存的分配越大越好吗

如果栈内存太大,可运行的线程数就会变小
一般采用系统默认的大小,但可以通过-Xss1024k手动分配栈的内存大小

JVM内存【线程数*(最大栈容量)+最大堆值+其他内存(忽略不计或者一般不改动)】= 机器最大内存 - 直接内存

对象都是优先分配在年轻代上的吗?

不是。当新生代内存不够时,老年代分配担保。而大对象则是直接在老年代分配。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值