浅析Java虚拟机详解之概述、对象生存法则

Java与C++之间有一堵由内存分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

一、概述

Java堆和方法区这两个区域有着很显著的不确定性:

1、一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样
2、只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的

垃圾收集器所关注的正是这部分的内存该如何管理

二、对象已死?

1、引用计数法

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

引用计数器虽然占用了一些额外的内存空间来进行计数,原理简单,判定效率很高;

为什么主流Java虚拟机没有使用引用计数器来管理内存呢

引用计数法看似简单的算法有很多例外情况要考虑,必须配合大量额外处理才能保证正确的工作,比如单纯的引用计数很难解决对象之间互相循环引用的问题

引用计数器的缺陷

/**
 * @Author: yky
 * @CreateTime: 2020-12-13
 * @Description: 引用计数器的缺陷
 */
public class ReferenceCountingGC {
 public Object instance = null;
 private static final int _1MB = 1024 * 1024;
 /**
  * 这个成员变量唯一作用是占内存
  */
 private byte[] bigSize = new byte[2 * _1MB];
 public static void testGC(){
  ReferenceCountingGC objA = new ReferenceCountingGC();
  ReferenceCountingGC objB = new ReferenceCountingGC();
  objA.instance = objB;
  objB.instance = objA;
  //发生GC,objA、objB能否被回收
  System.gc();
 }
}

运行代码收查看日志信息发现,这两个对象均被回收虚拟机并没有因为这两个相互引用就放弃回收他们---->Java虚拟机并不是通过计数算法来判断对象是否存活的;

2、可达性分析算法

该算法的核心思想:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些结点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连(图论话来说从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的)

对象obj5obj6obj7虽然有关联,但是他们到GC roots不可达因此他们会被判定为可回收对象

在 Java 语言中,可作为 GC Roots 的对象包括以下几种

  • 虚拟机栈(栈中的本地变量表)中的引用对象,如各线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
  • 方法区中的类静态属性引用的对象,如Java类的引用类型静态变量;
  • 方法区中的常量引用的对象,如字符串常量里的引用;
  • 本地方法栈总JNI(Navicat方法)引用的对象;
  • Java虚拟机内部的引用
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;
  • 根据用户所选的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入;

无论通过哪种算法判断对象是否存活都和“引用”离不开关系。

1)强引用

是指在程序代码之间普遍存在的引用赋值,Object obj = new Object();这种引用关系。
无论什么情况下,只要强引用关系还在,垃圾收集器就不会回收掉被引用的对象;

2)软引用

用来描述一些还有用,但非必须的对象。只要软引用关联着的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围之中进行第二次回收;如果这次的回收还没有足够的空间,才会抛出内存溢出的异常;

JDK1.2后提供SoftReference类实现软引用:

Soft reference objects, which are cleared at the discretion of the garbage
collector in response to memory demand. Soft references are most often used
to implement memory-sensitive caches.

3)弱引用

弱引用也被用来描那些非必须对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是足够,都会回收掉只被弱引用关联的对象;

JDK1.2后WeakReference类用来实现弱引用:

Weak reference objects, which do not prevent their referents from being

made finalizable, finalized, and then reclaimed. Weak references are most

often used to implement canonicalizing mappings.

4)虚引用

也叫“幽灵引用”、“幻影引用”,最弱的一种引用关系

  • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获得一个对象的实例;
  • 为一个对象设置虚引用的唯一目的是为了能在这个对象被收集器回收时收到一个系统通知;

PhantomReference类来实现虚引用:

Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed. Phantom references are most often used to schedule post-mortem cleanup actions.

应用需要读取大量本地图片

如果每次读取图片都从硬盘读取,则会严重影响性能;解决方案:【软引用或者弱引用】

Map<String,SoftReference<BitMap>> imp = new HashMap<String,SoftReference<BitMap>>

4、生存还是死亡?

在进行过可达性分析后的对象也不一定是非死不可的,该对象进行可达性分析后,发现没有与GC Roots相连接的引用链

  • 这个对象就会第一次被标记起来;对对象是否必要执行finalize()方法进行判断(已经被虚拟机调用过finalize()方法或者没有覆盖finalize()方法都认为是没有必要执行该finalize()方法)
  • F-Queue队列中存放该对象,优先级较低的Finalizer线程会去执行它;Gc 会对这个队列里面的对象再进行一次标记,如果在finalize方法中,对象没有自己自救的话,它就会被标记回收
  • finalize方法自救自己的办法是:重新与引用链上面的任何一个对象建立连接;如把自己this赋值给某个类或对象的成员变量
/**
1.对象可以在GC时自救
2.自救的办法只有一次,因为一个finalize方法最多只能被调用一次
**/
public class FinalizeEscapeGC {

 public static FinalizeEscapeGC SAVE_HOOK = null;
 public void isAlive(){
  System.out.println("yes,I am still alive :)");
 }

 @Override
 protected void finalize() throws Throwable {
  super.finalize();
  System.out.println("finalize method executed !");
  FinalizeEscapeGC.SAVE_HOOK = this;
 }

 public static void main(String [] args) throws InterruptedException {
  SAVE_HOOK = new FinalizeEscapeGC();
  //对象第一次成功拯救自己
  SAVE_HOOK = null;
  System.gc();
  //因为finalize优先级很低,所以延迟0.5s以等待它;
  Thread.sleep(500);
  if(SAVE_HOOK != null){
   SAVE_HOOK.isAlive();
  }else{
   System.out.println("no, i am dead :(");
  }

  //下面这段代码再执行一遍,验证对象是不是可以成功
  SAVE_HOOK = null;
  System.gc();
  Thread.sleep(500);
  if(SAVE_HOOK != null){
   SAVE_HOOK.isAlive();
  }else{
   System.out.println("no, i am dead :(");
  }
 }

}

结果如下:

finalize method executed !
yes,I am still alive :)
no, i am dead :(

  • 并不鼓励使用这种办法来拯救对象,它的运行代价高昂,不确定性大,无法保证顺序;
  • finalize方法能做的所有工作,try-finally也可以做的更好,更及时,所以希望忘记这个方法的存在;

5、回收方法区

很多人认为方法区(或者HotSpot虚拟机中的元空间或永久代)是没有垃圾收集行为的,《Java虚拟机规范》中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

方法区的垃圾收集主要回收两部分:废弃的常量和不再使用的类型;

  • 回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例:

假如一个字符串“Java”已经进入了常量池中,但是当前系统没有任何一个字符串对象的值是“Java”,换句话说是没有任何String对象引用常量池中的“Java”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“Java”常量就会被系统清理出常量池。

  • 常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单。而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“被允许”,而不是和对象一样,不使用了就必然会回收。在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证不会被方法区造成过大的内存压力。

到此这篇关于Java虚拟机详解之概述、对象生存法则的文章就介绍到这了,更多相关Java虚拟机内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入解析java虚拟机

    java虚拟机是什么 "java虚拟机"可能指以下三种东西:1).抽象规范:2).一个具体的实现:3).一个运行中的虚拟机实例: java虚拟机生命周期 启动当启动一个java程序时,一个虚拟机实例诞生.虚拟机实例通过调用某个初始类的public static void main(String[] args)方法来运行一个java程序.任何拥有这样一个main方法的类都可以作为java程序运行的起点,所以必须要告诉虚拟机初始类的名称,整个程序将从它的main方法开始运行.消亡初始类的m

  • 优化Java虚拟机总结(jvm调优)

    堆设置 -Xmx3550m:设置JVM最大堆内存为3550M. -Xms3550m:设置JVM初始堆内存为3550M.此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存. -Xss128k:设置每个线程的栈大小.JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K.应当根据应用的线程所需内存大小进行调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右. -Xmn2g:设置堆

  • Java虚拟机最多支持多少个线程的探讨

    McGovernTheory在StackOverflow提了这样一个问题: Java虚拟机最多支持多少个线程?跟虚拟机开发商有关么?跟操作系统呢?还有其他的因素吗? Eddie的回答: 这取决于你使用的CPU,操作系统,其他进程正在做的事情,你使用的Java的版本,还有其他的因素.我曾经见过一台Windows服务器在宕机之前有超过6500个线程.当然,大多数线程什么事情也没有做.一旦一台机器上有差不多6500个线程(Java里面),机器就会开始出问题,并变得不稳定. 以我的经验来看,JVM容纳的

  • Java虚拟机JVM性能优化(一):JVM知识总结

    Java应用程序是运行在JVM上的,但是你对JVM技术了解吗?这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化.之后的文章会讲JVM性能优化,包括最新的JVM设计--支持当今高并发Java应用的性能和扩展. 如果你是一个开发人员,你肯定遇到过这样的特殊感觉,你突然灵光一现,所有的思路连接起来了,你能以一个新的视角来回想起你以前的想法.我个人很喜欢学习新知识带来的这种感觉.我已经有过很多次这样

  • Java虚拟机JVM之server模式与client模式的区别

    JVM client模式和Server模式区别 JVM Server模式与client模式启动,最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升. JVM工作在Server模式下可以大大提高性能,Server模式下应用的启动速度会比client模式慢大概10%,但运行速度比Client VM要快至少有10倍 当不指定运行模式参数时,虚拟机启动检测主机是否为服务器,如果是,则以Server模式启动,否则以client模式启动,J2SE5.0检测的根据是

  • Java虚拟机JVM优化实战的过程全记录

    前言 Java虚拟机是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java 最具吸引力的特性之一.Java虚拟机是通过在实际的计算机上仿真模拟各种计算机功能模拟来实现的,通过Java虚拟机,您只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行. 最近在看JVM群里有人发了一个GC情况,让人帮忙看优化的,于是我也凑热闹发了出来想让群里的大神们指导优化一下,以下是优化过程记录. 一开始我贴了下面的两张图 jstat看GC记录

  • java虚拟机指令dup详解

    本文实例为大家介绍了java虚拟机指令dup,供大家参考,具体内容如下 举个例子: public class ExceptionTest{ void cantBeZero(int i) throws Exception{ throw new Exception(); } } 上面代码编译后的字节码指令如下: void cantBeZero(int) throws java.lang.Exception; descriptor: (I)V flags: Code: stack=2, locals=

  • 浅析Java虚拟机详解之概述、对象生存法则

    Java与C++之间有一堵由内存分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 一.概述 Java堆和方法区这两个区域有着很显著的不确定性: 1.一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样 2.只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的 垃圾收集器所关注的正是这部分的内存该如何管理 二.对象已死? 1.引用计数法 在对象中添加一个引用计数器,每当有一个地方引用它

  • java 线程详解及线程与进程的区别

    java  线程详解及线程与进程的区别 1.进程与线程 每个进程都独享一块内存空间,一个应用程序可以同时启动多个进程.比如IE浏览器,打开一个Ie浏览器就相当于启动了一个进程. 线程指进程中的一个执行流程,一个进程可以包含多个线程. 每个进程都需要操作系统为其分配独立的内存空间,而同一个进程中的多个线程共享这块空间,即共享内存等资源. 每次调用java.exe的时候,操作系统都会启动一个Java虚拟机进程,当启动Java虚拟机进程时候,Java虚拟机都会创建一个主线程,该线程会从程序入口main

  • Java StackOverflowError详解

    StackOverflowError 原因 : 函数调用栈太深了,注意代码中是否有了循环调用方法而无法退出的情况 原理 StackOverflowError 是一个java中常出现的错误:在jvm运行时的数据区域中有一个java虚拟机栈,当执行java方法时会进行压栈弹栈的操作.在栈中会保存局部变量,操作数栈,方法出口等等.jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误. 典型的例子: public class StackOverFl

  • java  线程详解及线程与进程的区别

    java  线程详解及线程与进程的区别 1.进程与线程 每个进程都独享一块内存空间,一个应用程序可以同时启动多个进程.比如IE浏览器,打开一个Ie浏览器就相当于启动了一个进程. 线程指进程中的一个执行流程,一个进程可以包含多个线程. 每个进程都需要操作系统为其分配独立的内存空间,而同一个进程中的多个线程共享这块空间,即共享内存等资源. 每次调用java.exe的时候,操作系统都会启动一个Java虚拟机进程,当启动Java虚拟机进程时候,Java虚拟机都会创建一个主线程,该线程会从程序入口main

  • Java BigDecimal详解_动力节点Java学院整理

    1.引言 借用<Effactive Java>这本书中的话,float和double类型的主要设计目标是为了科学计算和工程计算.他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的.然而,它们没有提供完全精确的结果,所以不应该被用于要求精确结果的场合.但是,商业计算往往要求结果精确,例如银行存款数额,这时候BigDecimal就派上大用场啦. 2.BigDecimal简介 BigDecimal 由任意精度的整数非标度值 和32 位的整数标度 (scale) 组

  • JAVA 注解详解及简单实例

    JAVA 注解详解及简单实例 何为注解 注解(Annotation)又称为元数据,在JDK1.5后引入,它的作用是: 生成文档  这是注解的原始用途,可以通过注解生成JavaDoc文档 跟踪代码的依赖性  可以通过注解替代配置文件,简化项目的配置.现有的许多框架都采用这个功能减少自己的配置. 编译检查  在编译时进行格式检查,例如@Override 基础注解 Java目前内置了三种标准注解,以及四种元注解.四种元注解负责创建其他的注解. 三种标准注解 @Override,表示当前的方法覆盖超类中

  • Java总结篇系列:Java泛型详解

    一. 泛型概念的提出(为什么需要泛型)? 首先,我们看下下面这段简短的代码: public class GenericTest { public static void main(String[] args) { List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); for (int i = 0; i < list.size(); i++) { S

  • Java 基础详解(泛型、集合、IO、反射)

    计划把 Java 基础的有些部分再次看一遍,巩固一下,下面以及以后就会分享自己再次学习的一点笔记!不是有关标题的所有知识点,只是自己觉得模糊的一些知识点. 1.对于泛型类而言,你若没有指明其类型,默认为Object: 2.在继承泛型类以及接口的时候可以指明泛型的类型,也可以不指明: 3.泛型也数据库中的应用: 写一个 DAO 类对数据库中的数据进行增删改查其类型声明为 <T> .每张表对应一个类,对应每一张表实现一个类继承该 DAO 类并指明 DAO 泛型为该数据表对应的类,再实现一个与该表匹

  • 9种Java单例模式详解(推荐)

    单例模式的特点 一个类只允许产生一个实例化对象. 单例类构造方法私有化,不允许外部创建对象. 单例类向外提供静态方法,调用方法返回内部创建的实例化对象.  懒汉式(线程不安全) 其主要表现在单例类在外部需要创建实例化对象时再进行实例化,进而达到Lazy Loading 的效果. 通过静态方法 getSingleton() 和private 权限构造方法为创建一个实例化对象提供唯一的途径. 不足:未考虑到多线程的情况下可能会存在多个访问者同时访问,发生构造出多个对象的问题,所以在多线程下不可用这种

  • Java基础详解之包装类的装箱拆箱

    一.包装类 概念: Java提供了两个类型系统,基本数据类型和引用数据类型,使用基本数据类型在于效率,然而很多情况下回创建对象使用,因为对象能做更多的功能. 所以可以使用一个类,把基本数据类型包装起来,在类中定义一些方法,这就叫做包装类.我们可以用这种方法来操作这些数据类型 基本类型 对应包装类(位于java.lang中) byte Byte short Short int Integer long Long float Float double Double char Character bo

随机推荐