jvm类加载

自己之前复习记录的随笔,没有什么格式 请勿嫌弃

双亲委派机制的好处是安全

双亲委派机制为什么安全 前面谈到双亲委派机制是为了安全而设计的,但是为什么就安全了呢?举个例子,ClassLoader加载的class文件来源很多,比如编译器编译生成的class、或者网络下载的字节 码。而一些来源的class文件是不可靠的,比如我可以自定义一个java.lang.Integer类来覆盖jdk中默认的Integer类,例如下面这样:

package java.lang;

/**

  • hack */ public class Integer { public Integer(int value) { System.exit(0); } }

初始化这个Integer的构造器是会退出JVM,破坏应用程序的正常进行,如果使双亲委派机制的话该Integer类永远不会被调用,以为委托BootStrapClassLoader加载后会加载JDK中的Integer类而不会加载自定义的这个

jvm 是一个进程 jvm怎么确定是同一个类的唯一性:类的全限定类名 和其 类加载器 确定了 这个类,如果类加载器不一样,在jvm 中,这两个不是同一个类 堆和栈 1.栈:栈是解决程序运行的问题, 2.堆:堆是用来解决数据存储问题 一个线程会有一个线程栈,内 线程独有,堆是线程共享的, 堆存对象 栈存基本数据类型和堆中对象的引用(基本数据类型是固定的不会出现引用,所以放在了栈中),局部变量 如果堆中的成员变量是对象,这个成员变量也是放在堆中的 方法区中 存的是常量和常量的引用。被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 class的成员变量,是放在 堆内存中的 局部变量和基本数据类型:是放在了栈中的。jvm会把【-128-127】之间的数 放在常量池里,如果一个new integer(2),对象本身是放在了常量池中的, final 修饰的都是常量。类编译的时候会吧 类,方法,接口中的常量,和string 的直接量放到常量池中 调用一个类的常量,并不会导致初始化该类。(final修饰的类变量且在编译时已经赋值) class demo(){ String s = “abc “;//成员变量,对象本身和对象的引用变量 都放在了 堆内存中,abc是直接量,放在常量池 Public void mm(){ String b = “efg”; //变量b 在栈中,efg是直接量, 在编译的时候放入常量池 Int I = 12; //基本数据类型,12是值,直接赋给了i,放在了栈中 Integer in = new integer(1); //局部变量in放在了 栈中,new integer(1)的对象(是常量)放在了 常量池中 } } 比如,局部变量:int i =10,i 放在了栈中 全局变量:static: int I=10, i 和10本身都是在 方法区 方法区中 放置的是,在编译的时候 的类变量,常量,静态变量,是在最开始的时候加入的 。当然也可以通过string。intern的方法加入。所以 运行时产生的对象 都放在了栈里面 栈代表了处理逻辑,堆代表了数据。面向对象就是堆栈的结合,比如一个对象,对象本身是放在堆中国年作为数据,对象的行为(方法)放在了栈中,作为运行逻辑 常量池:

什么是常量

用final修饰的成员变量表示常量,值一旦给定就无法改变!

final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
复制代码

常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名,字段名称和描述符,方法名称和描述符 2.基本数据类型的包装类和常量池 (1)默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 例如:Integer i1=40,编译时会变成i1=Integer.valueOf(40);,从而使用常量池中的对象。 (2)string 的“” 和new “ ”是直接从常量池拿对象,通过“ ” 和+号 ,产生的字符串会被放入常量池 new 是新生成对象 堆栈:http://pengjiaheng.iteye.com/blog/518623 http://www.jianshu.com/nb/1659284 1.JDK>JRE>JVM>JIT, JIT分为client模式和server模式两种,代码如果执行频率过高就会被JIT编译器转换成二进制执行, JVM是Java跨平台的依赖,JVM在不同平台中开辟一块属于自己的内存,独立运行自己的任务, JRE除了包含JVM以外,还包括SUN开发出来的接口与实现方便程序猿们利用JAVA语言开发,除了需要了解开放的部分Code以外,我们通常需要熟悉核心的API来满足我们开发的需求, JDK除了包含JRE还包括一些自带的命令,如将JAVA代码转换成class文件的javac命令、执行class文件的java命令、将注解变成API的javadoc命令、反编译javap、将java文件转换成jar包的jar命令,以及一些故障检测工具和命令等等.....

加载顺序:jar包等 2.java 字节码 是介于java 语言和机器语言之间的中间语言,是部署java代码的最小单位。java编译器把java语言变成jvm 可以理解的java字节码,字节码与平台无关,运行在jvm上

3.java class Jvm规定了一个方法的大小不能超过65535字节 jvm对低版本的class文件保持向后兼容,即低版本的可以运行在高版本上

5.运行时数据区 java 运行时会把它管理的内存划分为 Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域 。 1.程序计数器: 程序计数器可以看做是当前线程所执行的字节码的行号指示器。在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 线程私有的内存区域:保证线程轮流切换之后能恢复到正确的执行位置。 线程执行的是java方法,计数器记录的是正在执行的虚拟机字节码指令的地址。如果是native方法,值为空 唯一一个没有任何outofmemory的区域 native 方法:就是用java调用非java代码的接口,一个native方法 就是这样一个java方法:该方法的实现由非java 语言实现,比如c

通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

2.java 虚拟机栈 (1)Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 (2)线程私有的,它的生命周期与线程相同。

(3)主要关注的stack栈内存, 虚拟机栈中局部变量表部分。局部变量表存放了编译时期可知的各种基本数据类型和对象引用。 局部变量表所需的内存空间在编译时期完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 (4)异常情况: •如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常; •如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

3.本地方法栈 (1)同虚拟机栈。区别不过是虚拟机栈为虚拟机执行Java方法服务(也就是字节码),而本地方法栈为虚拟机使用到的Native方法服务。 (2)异常也类似: 同虚拟机栈相同,Java虚拟机规范对这个区域也规定了两种异常情况StackOverflowError 和 OutOfMemoryError异常。

4.java堆 (1)线程共享的内存区域,虚拟机创建时使用,用来存放实例对象。几乎所有对象实例都在这里分配内存。所有线程共享的一块内存 (2)Java堆是垃圾回收器管理的主要区域,因此也被称为"GC堆"。 从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代、老生代; 从内存分配的角度看,线程共享的Java堆可能划分出多个线程私有的分配缓冲区(TLAB); (3)java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。可扩展 (4)异常:在堆上没有完成内存分配,而且堆上也不能再扩展的时候,会抛出OutOfMemoryError异常; (5)内存泄露和内存溢出 Java堆内存的OOM异常是非常常见的异常情况,重点是根据内存中的对象是否是必要的,来弄清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow). •内存泄露:指程序中一些对象不会被GC所回收,它始终占用内存,即被分配的对象引用链可达但已无用。(可用内存减少) •内存溢出:程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。 •内存泄露是内存溢出的一种诱因,不是唯一因素。

5.方法区 (1)所有的线程共享的一块内存区域。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 (2)不需要连续的内存,可以选择固定大小或者可扩展之外,还可以选择不实现垃圾回收。 hotspot

这区域的内存回收目标主要是针对常量池的回收和类型的卸载,一般而言,这个区域的内存回收比较难以令人满意,尤其是类型的回收,条件相当苛刻,但是这部分区域的内存回收确实是必要的。 (3)方法区无法满足内存分配的需求时,将抛出OutOfMemoryError异常。 (4)运行时常量池 Class文件中有一部分是常量池于存放编译期生成的各种字面量和符号引用(常量和常量对象的引用,如a=‘111’),这部分内容将在类加载后进入方法区的运行时常量池中存放 动态性:不需要一定在编译期才产生,运行期间也可以加入常量池,比如String类的intern()方法

String类的intern()方法 String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此字符串的引用。

类加载机制:(类加载是在方法区中生成Class对象,之后所有类的实例化对象都通过这个Class对象产生) 1.定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。Java可以动态扩展的语言特性就是依赖运行期间动态加载和动态链接这个特点实现的。 1.5 三种机制: 全盘负责:加载一个类时把这个类依赖的引用的都加载 父类委托:加载一个类时先让父类加载器加载,如果加载不了才从自己的类路径加载 缓存机制:加载过的class 被缓存,先读缓存,没有的时候在加载 2.类的生命周期: 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。 。 这些阶段通常是相互交叉的进行,比如加载未完成但是连接可能已经开始进行 3.类加载的时机 主动引用:5种情况对类进行初始化 1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 2.反射的时候如果没有初始化 3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 4.虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类 5.如果一个java.lang.invoke.MethodHandle实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化 接口的初始化: 和类的初始化一致,区别:接口在初始化时,并不要求其父接口都全部完成了初始化,只有在真正用到父接口的时候才会初始化(如使用接口的静态变量)

被动引用:不进行初始化 1.通过子类引用父类的静态字段,不会导致子类初始化。 2.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

三、类加载的过程 虚拟机的类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型, 下面我们来详细了解类加载的全过程,也就是加载、验证、准备、解析和初始化这五个阶段的过程。 (1)加载:虚拟机需要完成以下三件事情: 1)通过一个类的全限定名来获取定义此类的二进制字节流。 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。:把二进制字节流按照虚拟机所需的格式存储在方法区 3)在内存中实例化一个java.lang.Class对象,这个对象将作为程序访问方法区中这些类型数据的外部接口 注意:可控;可以用系统的类加载器完成,也可以自定义的类加载成器,去控制字节流的获取方式。

(2)验证:重要但不一定是必要的阶段 验证是连接阶段的第一步,目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 整体上看,验证阶段会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。 符号引用验证:是在虚拟机将符号引用转化为直接引用的时候进行校验,这个转化动作是发生在解析阶段。符号引用可以看做是对类自身以外(常量池的各种符号引用)的信息进行匹配性的校验。

(3)准备:为类变量分配内存并设置类变量初始值 ,这些内存都将在方法区中进行分配。 ((1))类变量(被static修饰的变量),不包括实例变量,实例变量将会在对象实例化时随着对象一起被分配在Java堆中。 ((2))初始值为默认的零值,而不是设置的值。特殊情况为ConstantValue属性:final定义的属性,准备阶段会被赋值 A.例如public static int value = 123 ;value在准备阶段后的初始值是0而不是123,因为此时尚未执行任何的Java方法,而把value赋值为123的putStatic指令是程序被编译后,存放在类构造器()方法之中,把value赋值为123的动作将在初始化阶段才会执行。 b.例如public static final int value = 123 编译时javac将会为value生成ConstantValue属性,在准备阶段将变量赋值为123。 (4)解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 符号引用(Symbolic Reference): •符号引用以一组符号来描述所引用的目标,引用的目标并不一定已经加载在内存中 直接引用(Direct Reference): •直接引用可以是直接指向目标的指针、能间接定位到目标的句柄,如果有了直接引用,那引用的目标必定已经在内存中存在。对于同一个符号引用可能会出现多次解析请求,虚拟机可能会对第一次解析的结果进行缓存。

个人理解:一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类加载器加载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。

(5)初始化:真正开始执行类中定义的Java程序代码,初始化阶段是执行类构造器()方法的过程。 ()方法:作用为所有类变量(static)的赋值和静态语句块(static{}块) 访问限制:静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量,可以赋值,不能访问。

(2)()不需要显式调用父类的clinit,虚拟机保证 父类的()方法会在子类的()方法执行之前执行完毕, (3) ()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。 (6) 多线程:如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的()方法,其它线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,那么就可能造成多个进程阻塞。

类加载器:把类的字节码文件加载到内存中 Java采用双亲委派机制来实现类的加载:双亲委派机制能很好地解决类加载的统一性问题

距离:比如两个类A和类B都要加载System类:

如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。 如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了。

能不能自己写个类叫java.lang.System?

答案:通常不可以,但可以采取另类方法达到这个需求。 解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。

双亲委派机制:特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

类的唯一性:类的全限定类名 和其 类加载器 确定了 这个类,如果类加载器不一样,在jvm 中,这两个不是同一个类

4.java的动态装载特性:他会在第一次引用一个class的时候对他进行装载和连接,而不是在编译期进行 特点:

层级结构:Java里的类装载器被组织成了有父子关系的层级结构。Bootstrap类装载器是所有装载器的父亲。
代理模式:基于层级结构,类的装载可以在装载器之间进行代理。当装载器装载一个类时,首先会检查它是否在父装载器中进行装载了。如果上层的装载器已经装载了这个类,这个类会被直接使用。反之,类装载器会请求装载这个类。
可见性限制:一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。
不允许卸载:类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器。
复制代码

流程:类加载器被请求装载的时候,先在缓存里查看这个类是否已经被自己装载过了,如果没有的话,继续查找父类的缓存,直到在bootstrap类装载器里也没有找到的话,它就会自己在文件系统里去查找并且加载这个类

启动类加载器(Bootstrap class loader):这个类装载器是在JVM启动的时候创建的。它负责装载Java API,包含Object对象。和其他的类装载器不同的地方在于这个装载器是通过native code来实现的,而不是用Java代码。(具体的什么类)
扩展类加载器(Extension class loader):它装载除了基本的Java API以外的扩展类。它也负责装载其他的安全扩展功能。
系统类加载器(System class loader):如果说bootstrap class loader和extension class loader负责加载的是JVM的组件,那么system class loader负责加载的是应用程序类。它负责加载用户在$CLASSPATH里指定的类。
用户自定义类加载器(User-defined class loader):这是应用程序开发者用直接用代码实现的类装载器。
复制代码

如果自定义一个类 在哪里加载? system加载器

自定义类加载器:场景:比如网络传过来的字节码,加密了 继承classloader 类, 重新findclass方法 URLClassLoader() 从远程主机 加载

启动(Bootstrap)类加载器 :引导类装入器是用 本地代码 实现的类装入器,它负责将 <Java_Runtime_Home>/lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

标准扩展(Extension)类加载器 :扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器 :系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

6.Java堆中对象创建、布局、访问全过程 new关键字生成实例化对象时,先通过ApiClassloader判断testdemo类的Class对象是否被加载,如果未加载,则加载,最后通过这个Class对象在堆中创建一个testdemo实例对象

类加载检查:检查这个指令的参数是否能在常量池中定位到一个类的符号引用,符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程。
为对象分配内存
对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
2.1 根据Java堆中是否规整有两种内存的分配方式:
2.2 分配内存时解决并发问题的两种方案:
        指针碰撞(Bump the pointer):(并发下非线程安全)
    Java堆中的内存是规整的,用过的内存都放在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。
        空闲列表(Free List):
    不是规整的,已使用的内存和空闲的内存相互交错。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
        同步:
            对分配内存空间的动作进行同步处理
            每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。
内存空间初始化
将分配到的内存空间都初始化为零(不包括对象头)
作用:保证了对象的实例的字段在使用中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。默认值
对象设置
对对象进行必要的设置,哪个类的实例、类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
<init>
在上面的工作都完成之后,从虚拟机的角度看,一个新的对象已经产生了。
但是从Java程序的角度看,对象的创建才刚刚开始<init>方法还没有执行,所有的字段都还是零。
二、对象的内存布局
复制代码

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头:
HotSpot虚拟机的对象头包括两部分信息。
1.1 对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
1.2 类型指针,即指向它的类数据的指针,确定这个对象是哪个类的实例。
(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考 三对象的访问定位)
实例数据:
对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。。
对齐填充:占位符
如果实例数据部分 没有对齐,通过对齐填充来补全。因为对象的起始地址必须是8字节的整数倍,对象头部分正好是8字节的倍数
三、对象的访问定位:使用句柄和直接指针
复制代码

Java程序需要通过栈上的引用数据来操作堆上的具体对象。

使用句柄:
Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。


优势:引用中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而引用本身不需要修改。
直接指针:
如果使用直接指针访问,引用中存储的直接就是对象地址。

优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。(例如HotSpot)
复制代码

垃圾回收 一,哪些内存需要回收 二,什么时候回收 三,怎么回收

一、GC的工作区域(哪些内存需要回收?) 1.线程私有的不是工作重点:程序计数器、虚拟机栈和本地方法栈。栈帧中:(局部变量表(各种基本数据类型和对象引用)、操作数栈、动态链接、方法出口,)内存大小已知(每个栈帧中分配多少内存基本上是在类结构确定下来时就已知的) 2.重点关注:堆和方法方法区的部分:因为一个接口中的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,所以垃圾回收器所关注的主要是这部分的内存。 二、垃圾对象的判定(什么时候回收?):Java堆中存放着几乎所有的对象实例,怎么判定有用

  1. 判断对象是否存活的算法:

    引用计数算法 给对象添加一个引用计数器,每当有个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的。 (1)优点:引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的选择. (2)缺点:Java虚拟机并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。 对象objA和objB都有字段instance,赋值令objA.instance = objB;以及objB.instance = objA;,除此之外,这两个对象再无任何其他引用,实际上这两个对象已经不可能再被访问,但是因为它们互相引用着对方,导致它们的引用计数值都不为0,引用计数算法无法通知GC收集器回收它们。 可达性分析算法 这种算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。 Java语言是通过可达性分析算法来判断对象是否存活的。

    在Java语言里,可作为GC Roots的对象包括下面几种: 虚拟机栈(栈帧中的本地变量表)中引用的对象。 方法区中的类静态属性引用的对象。 方法区中的常量引用的对象。 本地方法栈中JNI(Native方法)的引用对象。

  2. 正确理解引用: 传统定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。 我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。 引用类型:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

    强引用:直接引用。类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 软引用:还有用但并非必需的对象。软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。提供了SoftReference类来实现软引用。 弱引用:非必需对象的,强度更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

  3. 对象死亡的标记过程: 即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

    如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。 finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在 finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。 下面的代码演示了两点:

    1.对象可以在被GC时自我拯救。

    2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 PS : finalize()的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,应该尽量避免使用。

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

    永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。 回收废弃常量与回收Java堆中的对象非常类似。

     以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。 
     判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:
     虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,需要虚拟机的参数进行控制。
     在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
         该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
         加载该类的ClassLoader已经被回收。
         该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    复制代码

三、垃圾收集算法 ( 如何回收?) 由于垃圾收集算法的实现涉及大量的程序细节,而且各个平台的虚拟机操作内存的方法又各不相同,以下只是介绍几种算法的思想及其发展过程。

标记-清除算法:
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点:

(1)效率问题,标记和清除两个过程的效率都不高;

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



复制算法:
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

(1)优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

(2)缺点:算法的代价是将内存缩小为了原来的一半,未免太高了一点。





现在的商业虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。

当然,90%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
标记-整理算法:
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。



分代收集算法:
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
(1)在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

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

方法区永久代 新生代对象转为老年代对象: (1)在survivor中的对象有一个计数器,用来记录经过的垃圾回收次数,当计数器值超过指定数值(默认是15)时采用复制算法复制到老年代中 (2)当新生代剩余的超过10%,超出的部分放入老年代

垃圾回收器: 并发:多个垃圾回收线程并行工作,用户线程等待 并行:用户线程不停止,和回收线程同时执行

新生代GC(Minor GC):指发生在新生代的垃圾收集动作 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。比minor 慢10倍以上

Gc收集器: serial: 复制算法,单线程,新生代收集器,会停止工作线程 stop the world 适用:client 模式下的 新生代收集器 优点:简单高效,单个CPU,没有线程交互开销,效率更高

ParNew serial的多线程版, 适用:server模式下的新生代收集器,能和cms配合 优点:能与cms 收集器配合工作 比较:在单个CPU的情况下 不一定有serial好,因为有线程交互 为什么可以与cms 配合: 因为parallel 和g1 都没有用传统的gc收集器框架

Parallel Scavenge,吞吐量收集器 并行的多线程收集器,复制算法,新生代,“吞吐量优先” 目标则是达到一个可控制的吞吐量,而不是缩短用户线程停顿的时间 适用:多个CPU上,对暂停时间没有特别高的要求,在后台处理不需要太多交互的任务 区别:parnew:JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,gc自适应的调节策略

Parallel Scavenge vs cms 吞吐量优先

Parallel Scavenge vs parnew Parallel Scavenge 自适应调节策略:Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。打开之后虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

Serial Old serial的老年版,单线程,用整理标记算法 适用:client模式,

server 模式:和Parallel Scavenge搭配 或者作为cms的后预案

Parallel Old parallel的老年版,多线程,标记整理算法 适用:server模式,多cpu,注重吞吐量

CMS收集器,低停顿收集器(Concurrent Mark Swee) 获取最短回收停顿时间为目标的收集器.希望响应时间最短的适用。:并发收集、低停顿 CMS收集器的内存回收过程是与用户线程一起并发执行的。 特点: 老年代, 标记-清除算法(内存碎片) 并发收集、低停顿 Cms的流程 :初始标记(标记GC Roots能到的,需要stop the word )-并发标记(GC Roots Tracing的过程)-重新标记(修正标记期间用户线程运作产生的影响的记录,停顿比初始标记长 )-并发清除 1).初始标记阶段 暂停所有的其他线程,并记录下直接与root相连的对象。 2).并发标记阶段 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 3).最终确认标记阶段 将上一阶段做了指针更新的区域和root合并为一个伪root集合,并对其做tracing。从而可以保证真正可达的对象一定被标记了。但同时也会产生一部分被标记为可达,但其实已经是不可达的区域,由于已经没有了到达这个区域的路径,所以并没有办法将它的标志位置为0,则造成了一个暂时的内存泄漏,但这部分空间会在下一次收集阶段被清扫掉。 4).并发清扫阶段 开启用户线程,同时GC线程开始对为标记的区域做清扫。这个过程要注意不要清扫了刚被用户线程分配的对象。一个小trick就是在这个阶段,将所有新分配的对象置为可达的。
5) 并发重制

清理数据结构,为下一个并发收集做准备. 适用:与用户交互较多的场景 第一次实现了垃圾收集线程和用户线程同时工作

缺点: 1.对cpu 敏感,占用资源 ,降低吞吐率 2.不能清理浮动垃圾(浮动垃圾:当次gc,因用户线程而产生的垃圾,本次gc不会回收 所以叫做浮动垃圾) 3.空间碎片

G1 收集器 用来替代cms, 特点: 1.利用多个cpu缩短stop-the-world的时间,并发让用户线程执行 2.分代收集:整个gc堆,不需配合。划分region而不是绝对的新生代老年代 3.不产生空间碎片:(分配为多个region,从整体看类似标记-整理,从局部(两个region)看类似复制算法) 4.可预测的停顿,低停顿高吞吐量:优先回收价值最大的Region

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
复制代码

其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。

每个区域大小相等,在1M~32M之间。JVM最多支持2000个区域,可推算G1能支持的最大内存为2000*32M=62.5G。 它们是逻辑的,使用一些非连续的区域(Region)组成的。

适用场景:服务器端应用,大内存,多处理器,应用是为需要低GC延迟,并具有大堆

比cms更好的场景: (1)超过50%的Java堆被活动数据占用; (2)GC停顿时间过长(长于0.5至1秒)。

过程: 初始标记(关联对象,修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对 象,stop-the-word 较短)- 并发标记(找出存活对象,可并发)- 最终标记(修正用户线程产生的影响)- 筛选回收(region 回收价值筛选)

G1 收集器在老年代堆内存中执行下面的这些阶段. 注意有些阶段也是年轻代垃圾收集的一部分. 阶段

说明 (1) 初始标记(Initial Mark)

(Stop the World Event,所有应用线程暂停) 此时会有一次 stop the world(STW)暂停事件. 在G1中, 这附加在(piggybacked on)一次正常的年轻代GC. 标记可能有引用指向老年代对象的survivor区(根regions). (2) 扫描根区域(Root Region Scanning)

在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。 (3) 并发标记(Concurrent Marking)

在整个堆中查找活着的对象. 此阶段应用程序的线程正在运行. 此阶段可以被年轻代GC打断(interrupted). (4) 再次标记(Remark)

(Stop the World Event,所有应用线程暂停) 完成堆内存中存活对象的标记. 使用一个叫做 snapshot-at-the-beginning(SATB, 起始快照)的算法, 该算法比CMS所使用的算法要快速的多. (5) 清理(Cleanup)

(Stop the World Event,所有应用线程暂停,并发执行)

在存活对象和完全空闲的区域上执行统计(accounting). (Stop the world)

擦写 Remembered Sets. (Stop the world)

重置空heap区并将他们返还给空闲列表(free list). (Concurrent, 并发) (*) 拷贝(Copying)

(Stop the World Event,所有应用线程暂停) 产生STW事件来转移或拷贝存活的对象到新的未使用的heap区(new unused regions). 只在年轻代发生时日志会记录为 [GC pause (young)]. 如果在年轻代和老年代一起执行则会被日志记录为 [GC Pause (mixed)].

-XX:+UseG1GC

对象内存分配处理策略: Serial / Serial Old收集器下(ParNew / Serial Old收集器组合的规则也基本一致) 分配:在堆上分配,如果开启了本地线程缓冲,有现在tlab上,少量也会在老年代 1.对象优先在eden分配 可以设置java堆大小, 2.大对象直接进入老年代: 虚拟机提供了一个-XX:PretenureSizeThreshold参数,大于这个直接进入老年代(参数只对Serial和ParNew两款收集器有效) 3.晋升老年代的 阀值,默认15次,参数-XX:MaxTenuringThreshold 4.动态年龄判断 survivor空间相同年龄的对象占一半,大于这个年龄的进入老年代 5.空间分配担保 minor gc之前,会检查老年代的最大可用连续空间 是否大于新生代所有对象,如果大于, 安全 不大于:是否大于历次平均值,大于可以尝试,但是冒险 担保:如果剩余空间 surveyor 不够装,会进入老年代 如果担保失败,会进行full gc

还有一个问题是,垃圾回收动作何时执行?

当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载
】
复制代码

Q:为什么崩溃前垃圾回收的时间越来越长? A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据 Q:为什么Full GC的次数越来越多? A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收

Q:为什么年老代占用的内存越来越大? A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代

jvm调优:

java -Xms128m -Xmx2g MyApp JVM的初始和最大堆内存大小,(然而JVM可以在运行时动态的调整堆内存的大小,所以理论上来说我们有可能会看到堆内存的大小小于初始化堆内存的大小。但是即使在非常低的堆内存使用下,我也从来没有遇到过这种情况。) 等同于:$ java -XX:InitialHeapSize=128m -XX:MaxHeapSize=2g MyApp

-XX:+HeapDumpOnOutOfMemoryError 让JVM在发生内存溢出时自动的生成堆内存快照 -XX:OnOutOfMemoryError 当内存溢发生时,我们甚至可以可以执行一些指令

$ java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError ="sh ~/cleanup.sh" MyApp Perm不属于堆内存,有虚拟机直接分配,但可以通过 -XX:PermSize and -XX:MaxPermSize 永久带的初始大小和最大

(4)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集

(4)可以通过下面的参数打Heap Dump信息

-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt

通过下面参数可以控制OutOfMemoryError时打印堆的信息

-XX:+HeapDumpOnOutOfMemoryError
复制代码

jvm启动流程
配置JVM装载环境 Java代码执行时需要一个JVM环境,JVM环境的创建包括两部分:JVM.dll文件的查找和装载。 虚拟机参数解析 装载完JVM环境之后,需要对启动参数进行解析,其实在装载JVM环境的过程中已经解析了部分参数 设置线程栈大小 执行main方法 jvm调优 -Xms 初始堆大小 -Xmx 最大堆大小 -Xmn 新生代大小 -XX:NewRatio 新生代与老年代的比例 =2,新生代1/3 老年代2/3 -XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8 -XX:PermSize 永久代(方法区)的初始大小 -XX:MaxPermSize 永久代(方法区)的最大值 -XX:+HeapDumpOnOutOfMemoryError 内存溢出时的快照

Jvm老年代: 大对象(需要大量连续内存空间的java对象,比如很长的字符串or数组)会直接分配在老年代   Full gc触发条件:     1、程序调用System.gc()     2、老年代空间不足、永久代(jdk8之后不存在永久代)空间不足     3、CMS GC时出现promotion failed和concurrent mode failure,对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。应对措施为:增大survivor space、老年代空间或调低触发并发GC的比率

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值