• 《Java虚拟机》必知必会——十四个问题总结(内存模型+GC)


    引用:https://www.2cto.com/kf/201608/543537.html

    一、Java概述

    1、Java相较于PHP、C#、Ruby等一样很优秀的编程语言的优势是什么?

    (1)体系结构中立,跨平台性能优越。Java程序依赖于JVM运行,javac编译器编译Java程序为平台通用的字节码文件(.class),再由JVM与不同操作系统匹配,装载字节码并解释(也有可能是编译,会在第三个问题中说到)为机器指令执行。

    (2)安全性优越。通过JVM与宿主环境隔离,且Java的语法也一定程度上保障了安全,如废弃指针操作、自动内存管理、异常处理机制等。

    (3)多线程。防止单线程阻塞导致程序崩溃,分发任务,提高执行效率。

    (4)分布式。支持分布式,提高应用系统性能。

    (5)丰富的第三方开源组件Spring、Struts、Hibernate、Mybatis、Quartz等等等等。

    2、字节码是什么?.class字节码文件是什么?

    (1)字节码是包含Java内部指令集、符号集以及一些辅助信息的能够被JVM识别并解释运行的符号序列。字节码内部不包含任何分隔符区分段落,且不同长度数据都会构造成n个8位字节单位表示。

    (2).class里存放的就是Java程序编译后的字节码,包含了类版本信息、字段、方法、接口等描述信息以及常量池表,一组8位字节单位的字节流组成了一个字节码文件。

    3、JVM是什么?HotSpot虚拟机有什么特点?

    JVM全称Java Visual Machine,Java虚拟机。是Java程序的运行环境,主要负责装载字节码文件,并解释或编译成对应平台的机器指令执行。

    我们使用最多的是JDK缺省自带的HotSpot虚拟机,使用解释器加编译期并存架构方案。一开始的时候使用解释器,使编译未结束时就可以解释字节码为本地机器指令执行,提高效率。编译器用在HotSpot的热点探索功能上,在存在频繁调用的方法或循环次数较多的代码时,就会把这类代码块标记为“热点代码”,通过内嵌的双重JIT(Just in time compiler)将字节码直接编译成对应机器指令,以提高效率。


    二、Java内存模型

    1、PC计数器

    线程私有,用于记录当前线程正在执行字节码的地址,如果执行的是native本地方法,PC计数器为空。

    2、Java栈

    线程私有,也叫作Java虚拟机栈,用于存储栈帧,栈帧的入栈出栈过程即方法调用到执行结束的过程。栈帧中主要存放方法执行所需的局部变量表(包括局部变量的声明数据类型、对象引用等)、操作数栈、方法出口等信息。

    3、本地方法栈

    与Java栈功能类似,只是用于存储native本地方法的相关信息。

    4、Java堆

    线程公用,用于存放对象实例,包括数组,也叫GC区,是GC主要工作的区域。也正是如此,由于GC频率过快与效率不高,堆区的可能成为JVM性能瓶颈,于是考虑到性能,堆区不再是对象内存分配的唯一选择。这里就涉及到了对象的逃逸分析与栈上分配。

    逃逸分析就是用来分析对象的作用域是否在方法内部,当方法返回了当前类实例对象、方法中为当前类成员变量赋值、方法中引用当前类成员变量的值时就会发生逃逸,依然在堆上分配内存。但当对象的作用域就在方法内时,比如在方法内创建了该类的实例,没有返回、没有引用,则这种情况就直接在Java栈上分配内存,随着栈帧的出栈释放空间,减轻了堆区GC的压力。

    5、方法区

    线程公用,存储了每一个Java类的结构信息,比如:字段、各种方法的字节码内容数据、运行时常量池等。方法区也被称为永久带。一般没有显示要求,GC只对方法区中的常量池回收以及类型卸载。

    6、运行时常量池

    属于方法区的一部分,类加载器将类的字节码文件加载如JVM中后,会把字节码文件中的常量池表转化为运行时常量池。


    三、Java垃圾回收机制

    1、常见的标记可用对象的算法有哪些?

    (1)引用计数法:每个对象都创建一个私有的引用计数器,当该对象被其他对象引用时(出现在等号右边),引用计数器加1;当不再引用时,引用计数器减一;当引用计数器为0时,对象即可被回收。这种方式存在着当两个对象互相引用时,二者引用计数器值都不为0无法被回收的问题;

    (2)根搜索算法:JVM一般使用的标记算法,把对象的引用看作图结构,由根节点集合出发,不可达的节点即可回收,其中根节点集合包含的如下5种元素:

    1、Java栈中的对象引用;

    2、本地方法栈中的对象引用;

    3、运行时常量池的对象引用;

    4、方法区中静态属性的对象引用;

    5、所有Class对象;

    2、常见的垃圾回收算法有哪些?JVM使用哪种?

    (1)标记-清除算法:分两个阶段执行,第一个阶段标记可用对象,第二个阶段清除垃圾对象;这个方法很基础简单,但效率低下,而且会产生内存碎片(不连续的内存空间),无法再次分配给较大对象。

    (2)复制算法:被广泛用于新生代对象的回收。将内存分为两个区域,新对象都分配在一个区域中,回收时将可用对象连续复制到另一个区域,回收完成后,新对象分配在有对象的区域,循环往复。这种算法不会产生内存碎片,且效率较高,但因为同时只有一个区域有效,会导致内存利用率不高。

    (3)标记-整理算法:被应用于老年代对象的回收。这种算法与标记清除算法类似,第一个阶段标记可用对象,第二个阶段将可用对象移动到一段连续的内存上,解决了标记-清除算法会产生内存碎片的缺点。

    (4)分代回收算法:在HotSpot虚拟机中,基于分代的特点(堆内存可进一步分为年轻代、老年代,老年代存放存活时间较长的对象),JVM GC使用分代回收算法。

    年轻代使用复制算法:分为一个较大的Eden区与两个较小的、等大小的Survivor区(From Space与To Space),比例一般是8:1:1。新对象都分配在Eden区,当GC发生时(新生代的GC一般叫做Minor GC),将Eden区与From区中的可用对象复制到To区中,From Space与To Space互换名称,循环方法。直到发生如下两种情况,对象进入老年代:

    1' From区内的对象已经达到存活代数阀值(经过GC的次数达到设定值),GC时不会进入To区中,直接移动至老年代;

    2' 在回收Eden区与From区后,超出To区可容纳范围,则直接将存活对象移动至老年代。

    老年代使用标记-整理算法:当老年代满的时候,会触发Full GC(新生代与老年代一起进行GC)。

    3、常见的垃圾回收器有哪些?有什么特点?适合应用与什么场景?

    (1)Serial收集器

    年轻代采用复制算法、串行回收、与“Stop the world”机制(GC时停止其他一切工作),适用于单核CPU环境,绝对不推荐应用于服务器端。

    Serial提供了老年代的回收器Serial Old,采用标记-整理算法,其他特性与新生代一致。

    Serial+Serial Old适合客户端场景。

    (2)ParNew收集器

    相当于Serial的多线程版本,并行回收,年轻代同样采用复制算法与“Stop the world”机制,适用于多核CPU、低延迟环境,推荐应用于服务器场景。

    (3)Parallel收集器

    与ParNew类似,复制算法、并行回收、“Stop the world”机制,但是与ParNew不同,Parallel可以控制程序吞吐量大小,也被称为吞吐量优先的垃圾收集器。

    与Serial类似,Parallel也有老年代版本,Parallel Old,同样采用标记整理-算法。

    Parallel+Parallel Old非常适用于服务器场景。

    (4)CMS收集器

    与Parallel的高吞吐对应,CMS就是为高并发、低延时而生的。采用标记-清除算法、并行回收、“Stop the world”。因为采用了标记-清除算法,会产生大量内存碎片,要慎重使用。

    (5)G1收集器

    是一款基于并行、并发、低延时、暂停时间可控的区域化分代式垃圾回收器。

    具有革命意义的设计,放弃了堆区年轻代、老年代的划分方案,而是将堆区或分成约2048个大小相同的独立Region块。

    4、GC的优化方案?

    基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。其中需要注意,JVM进行次GC的频率很高,但因为Minor GC占用时间极短,所以对系统产生的影响不大。更值得关注的是Full GC的触发条,具体措施包括以下几个方面:

    (1)不要显式调用System.gc()

    调用System.gc()也仅仅是一个请求(建议)。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。但即便这样,很多情况下它会触发Full GC,也即增加了间歇性停顿的次数。

    (2)尽量减少临时对象的使用

    临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,也就减少了Full GC的概率。

    (3)对象不用时最好显式置为Null

    一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

    (4)尽量使用StringBuffer,而不用String来累加字符串

    由于String是常量,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

    (5)能用基本类型如Int,Long,就不用Integer,Long对象

    基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

    (6)尽量少用静态对象变量

    静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

    (7)分散对象创建或删除的时间

    集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行Full GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

    5、Java即使有了GC也会出现的内存泄漏情况?举例说明。

    1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。

    Static Vector v = new Vector();

    for (int i = 1; i<100; i++)

    {

    Object o = new Object();

    v.add(o);

    o = null;

    }

    在这个例子中,代码栈中存在Vector对象的引用v和Object对象的引用o。在For循环中,我们不断的生成新的对象,然后将其添加到Vector对象中,之后将o引用置空。问题是当o引用被置空后,如果发生GC,我们创建的Object对象是否能够被GC回收呢?答案是否定的。因为,GC在跟踪代码栈中的引用时,会发现v引用,而继续往下跟踪,就会发现v引用指向的内存空间中又存在指向Object对象的引用。也就是说尽管o引用已经被置空,但是Object对象仍然存在其他的引用,是可以被访问到的,所以GC无法将其释放掉。如果在此循环之后,Object对象对程序已经没有任何作用,那么我们就认为此Java程序发生了内存泄漏。

    2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

    3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

  • 相关阅读:
    gradle build scan 插件使用
    gradle 配置java 项目maven 依赖
    gradle 插件
    gradle java 简单项目使用
    gradle 命令行
    gradle wrapper 简单使用
    gradle 安装试用
    linux 使用asciinema 进行命令行屏幕录制共享
    ansible安装基本使用
    ansible playbook 使用
  • 原文地址:https://www.cnblogs.com/xxj-bigshow/p/8632649.html
Copyright © 2020-2023  润新知