最近参考各种资料,尤其是《深入理解Java虚拟机 JVM高级特性和最佳实践》,大牛之作。把最近学习的Java虚拟机组成和垃圾回收机制总结一下。

你不会的都是新知识,学无止境,每天进步一点点。

一、认识Java虚拟机

在开始学Java之时,必做的一件事就是从Java官网下载并安装Java到我们的电脑之上,然后从HelloWorld开始走上编程的不归路。

上图中下载的Java安装包全称是Java SE Development Kit(单词依次翻译:Java 标准版本 开发 工具包),简称JDK,也就是供程序员使用的Java开发工具包。另外,JDK一般都是和它的2个小弟一起出现的,一个是JRE,一个是JVM,它们的关系如下图所示:

JDK=JRE+Java编译器、开发工具和更多的类库

简单来说JDK支持Java程序的开发。

JRE是Java Runtime Environment的简称

JRE=Java虚拟机+Java基础类库

是Java程序运行所需要的软件环境,简单的来说JRE支持Java程序的运行。图片中也提到了,JRE只支持java字节码的运行,是没办法把Java代码编译成.class文件的。

最内部也是最核心的就是Java虚拟机了,那么问题来了,什么是Java虚拟机?

Java虚拟机实际上是一种规范,是一种抽象机器,依附在真实的操作系统之上,这个规范描述了一个指令集,一组寄存器,一个堆栈,一个“垃圾堆”,和一个方法区。一旦一个Java虚拟机在给定的平台上运行,任何Java程序都能在这个平台上运行。

二、JVM运行时数据区

编写好的java代码交给java虚拟机,java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,称为运行时数据区,如下图所示。

图中绿色标记的区域是每个线程私有的,也就是线程安全的。灰色标记的区域是所有线程共享的,非线程安全。这幅图中我们只需要关注运行时数据区中的5个部分。

2.1 程序计数器

程序计数器的功能:

当前线程所执行的字节码的行号指示器。

.java文件编译成.class文件,最终交给jvm执行,.class文件也是要按逻辑找到行号执行的,程序计数器就是对当前线程应该执行哪一行字节码做指示的。假设线程A暂停,过一段时间后线程A继续执行,这时候线程A应该从哪个地方继续就是靠程序计数器来完成的。

每个线程一个程序计数器,不同线程之间的程序计数器互不影响。

2.2 虚拟机栈

虚拟机栈描述的是java方法执行的内存模型。

一个线程一个栈,一个方法一个栈帧。栈帧可以理解成栈中的一小块,一个栈中有多个栈帧。栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

如下,MethodStackTest类中有三个方法:methodA、methodB和methodC,methodA调用了methodB,methodB调用了methodC,在main方法中执行methodA。

代码及打印结果:

class MethodStackTest{
    public static void methodA(){

        methodB();
        System.out.println("Method A");
    }
    public static void methodB(){
        methodC();
        System.out.println("Method B");
    }
    public static void methodC(){
        System.out.println("Method C");
        //methodA();取消注释会出现栈内存溢出
    }
   public static void main(String[] args) {
      methodA();
   }
}
结果:

Method C
Method B
Method A

这个小例子有助于解释了栈的含义,A调用了B,B调用了C,只有C执行结束,B才会结束,最终B执行完成A才会结束。

另外,如果在C中再调用A的话就一直循环调用,超过虚拟机所允许的栈的深度以后就会抛出StackOverflowError异常,如果不能申请到足够的内存,就会抛出OutOfMemory异常。

2.3 本地方法栈

本地方法栈和虚拟机栈发挥的作用相似,本地方法栈为本地方法服务。

2.4 堆

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。堆被划分成三个不同的区域:新生代 ( Young )、老年代 ( Old )和永久区。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。Java堆也是垃圾回收的主战场,也被称为GC堆。

堆大小=新生代+老年代+永久区

新生代=Eden+From Survivor+To Survivor

默认:Eden:From:To=8:1:1

另外新生代和老年代的比例可以通过参数设置,并不一定是1:1。

2.5 方法区

方法区也被称为永久区,里面有类信息、常量、静态变量等数据,别名为非堆,为各个线程所共享。

三、垃圾回收机制

3.1 什么是垃圾?

Java中的垃圾是指已经分配内存但不再有任何引用(不完全准确,后面会再说)的对象,垃圾回收(GC)就是自动清理这部分内存。

3.2 两种垃圾判断算法

3.2.1 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

这种算法的有点事实现简单,判定效率也高,但是主流虚拟机并没有采用,主要是对于循环引用的对象无法进行回收。

3.2.3 可达性分析法

主流的JVM回收垃圾主要采用可达性分析法,算法的核心思想是选定一系列GC ROOTS对象作为起始点,从节点开始向下搜索,搜索路径称为引用链。一个对象到GC ROOTS没有任何引用链证明对象不可用。

可作为GC ROOTS的对象:

  1. 虚拟机栈中引用的对象
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法中JNI引用的对象

3.3 四种垃圾回收算法

3.3.1 标记-清除法(Marked-Sweep)

标记清除算法是最基础的收集算法,其他收集算法都是基于这种思想。标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。

它的主要缺点:

  1. 标记和清除过程效率不高 。
  2. 标记清除之后会产生大量不连续的内存碎片。

3.3.2 复制算法(Copying)

它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次理掉。这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。

主要缺点:内存缩小为原来的一半。

3.3.3 标记-整理法

标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。

主要缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。

3.3.4 分代算法

分代算法就是根据堆内存中新生代和老年代对象的特点,不同的区域采用不同的垃圾回收算法。新生代大多数对象“朝生夕死”,每次都有大量对象被回收,因此采用复制算法,上图堆内存中FROM和TO就是为了复制算法而设计的。老年代对象存活率较高,没有额外空间进行分配担保,使用“标记-清理”或者“标记整理”算法。

3.4 七个垃圾收集器

七种作用于不同分代的垃圾收集器:



垃圾收集器这部分没有写的太详细,这里有一篇已经整理好的:http://www.jianshu.com/p/50d5c88b272d,其内容也是根据《深入理解Java虚拟机 JVM高级特性和最佳实践》整理的。

3.4.1 Serial收集器

Serial收集器是最基本、发展历史最悠久的单线程收集器,最大的特点是Stop The World,垃圾回收时要终止用户进程,等GC结束用户进程方可继续。

JVM参数-XX:+UseSerialGC

3.4.2 ParNew收集器



ParNew收集器是Serial收集器的多线程版本。

3.4.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

3.4.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

3.4.5 Parallel Old收集器

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

3.4.6 CMS收集器

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

3.4.7 G1收集器

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

3.4 GC范围

按范围分,GC有Minor GC、Major GC和Full GC。

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,从老年代回收内存被称为Major GC,同时回收新生代和老年代称为Full GC。

四、内存分配策略

4.1 对象优先分配在Eden区

对象优先在新生代Eden中分配,Eden中没有足够空间进行分配时,虚拟机发起一次Minor GC。

4.2 大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象,典型的是长字符串和数组,这种大对象会被分配到老年代之中。

4.3 老不死的对象进入老年代

新生代中的对象经过不断GC仍然存活,达到一定的“年龄”就会进入老年代,每经历一次Minor GC,年龄+1,达到15时(默认)就会进入老年代。晋升老年代的年龄阈值可以通过-XX:MaxTenuringThreshold参数指定。

4.4 动态对象年龄判定

JVM也不是严格执行4.3中的年龄要求,Survivor空间相同年龄所有对象大小的总和大于等于Suivivor空间的一半,年龄大于或等于该年龄的对象就会直接进入老年代,无需达到XX:MaxTenuringThreshold的要求年龄。

4.5 空间分配担保

Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果是,GC是确保安全的。JVM继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则正常进行一次YGC,尽管有风险(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多);

如果小于,或者HandlePromotionFailure设置不允许空间分配担保,这时要进行一次Full GC。

五、小结

JVM这一部分初步整理这么多,欢迎批评指正。

JVM运行时数据区和垃圾回收机制的更多相关文章

  1. Java内存管理:Java内存区域 JVM运行时数据区

    转自:https://blog.csdn.net/tjiyu/article/details/53915869 下面我们详细了解Java内存区域:先说明JVM规范定义的JVM运行时分配的数据区有哪些, ...

  2. JVM 运行时数据区 (三)

    JVM运行时数据区 运行时数据区由 程序计数器.java虚拟机栈.本地方法栈.堆.方法区 组成: 1.程序计数器 每一个Java线程都有一个程序计数器,用于保存程序执行到当前方法的哪一个指令,它是线程 ...

  3. JVM总结(一):概述--JVM运行时数据区

    大三下,趁着寒假重温一遍JVM,准备在一个系列来总价一下学习JVM的整个过程.争取在接下来的一个星期内更新完这一个系列,然后回家过年. JVM运行时数据区 线程私有的数据区 程序计数器 虚拟机栈 本地 ...

  4. Jvm运行时数据区

    一:运行时数据区 Java虚拟机在执行Java程序的过程中会把它管理的内存分为若干个不同的数据区域.这些区域有着各自的用途,一级创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户 ...

  5. JVM运行时数据区及对象在内存中初始化的过程

    JVM运行时数据区 Java虚拟机所管理的内存区域,也称为运行时数据区,分为以下几个运行时数据区,如图所示 程序计数器:当前程序所执行字节码的行号指示器 程序计数器(Program Counter R ...

  6. JVM运行时数据区与JVM堆内存模型小结

    前提 JVM运行时数据区和JVM内存模型是两回事,JVM内存模型指的是JVM堆内存模型. 那JVM运行时数据区又是什么? 它包括:程序计数器.虚拟机栈.本地方法栈.方法区.堆. 来看看它们都是干嘛的 ...

  7. Jvm运行时数据区 —— Java虚拟机结构小记

    关于jvm虚拟机的文章网上都讲烂了.尤其是jvm运行时数据区的内容. 抱着眼见为实的想法,自己翻了翻JVM规范,花了点时间稍微梳理了一下. 以下是阅读Java虚拟机规范(Java SE 8版)的第二章 ...

  8. Java中的字符串常量池和JVM运行时数据区的相关概念

    什么是字符串常量池 JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池 工作原理 当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量 ...

  9. JVM 运行时数据区(二)

    @ 目录 运行时数据区 共享区 堆区 方法区 隔离区 虚拟机栈 栈帧 本地方法栈 程序计数器 运行时数据区 JVM 运行时数据区主要分为5块 方法区 JDK1.8以后叫做元数据区(Metaspace) ...

随机推荐

  1. raid 简单了解

    独立硬盘冗余阵列(RAID, Redundant Array of Independent Disks),旧称廉价磁盘冗余阵列(Redundant Array of Inexpensive Disks ...

  2. php-fpm 信号

    使用信号之前,需要先确保php-fpm.conf 里面有配置pid,默认是被注释掉的. ;pid = run/php-fpm.pid 文件在 php安装目录/var/run/php-fpm.pid 信 ...

  3. Centos下ftp协议连接远程ftp server主机

    环境说明 [root@Check3 ~]# cat /etc/redhat-release CentOS release 6.9 (Final) [root@Check3 ~]# uname -a L ...

  4. Painter's Problem

    Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 5378   Accepted: 2601 Description There ...

  5. 打开Mac OSX原生的NTFS功能

    插上磁盘 从finder或者使用以下命令查看到磁盘的Volume Name: diskutil list /dev/disk0 #: TYPE NAME SIZE IDENTIFIER 0: GUID ...

  6. Linux命令:chmod、chgrp、chown的区别

    chmod是更改文件的权限: chgrp只是更改文件的属组: chown是更改文件的属主与属组. 1.chmod:更改文件的权限 文件权限的设置方式有两种,分别是数字和标记. mode : 权限设定字 ...

  7. Android studio 2.3安装遇到的问题

    我的安装系统环境:windows 10+jdk1.8.0_111 . 1.在安装Android studio 2.3之前,请安装最新的java jdk.Android studio的安装包里是不包含j ...

  8. Quartz(自动任务)中的触发器Trigger

    1.Quartz中的触发器TriggerJob 包含了要执行任务的逻辑,但是 Job 对何时该执行却一无所知.这个事情留给了 Trigger.Quartz Trigger 继承了抽象的 org.qua ...

  9. Swoft 快速上手小贴士

    IDE一定要装注解插件PHP Annotations Request和Response里的with...开头的方法会clone $this, 而不是修改本实体, 所以设置Cookie之类的时候要$re ...

  10. .Net Core 修改默认的启动端口

    今天无意中发现一个变化,因为很久没看.net core的项目了,发现项目启动的默认端口已经不是5000了,记得很清楚,最早那还是.net core 1.x版本的时候,每次启动都会默认是5000端口号, ...