您的位置:首页 > 编程语言 > Java开发

Java性能优化系列之四--Java内存管理与垃圾回收机制详解

2014-03-31 20:36 471 查看
1、JVM运行时数据区域。

(1)、程序计数器:每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令。此内存区域是唯一一个在JVM Spec中没有规定任何OutOfMemoryError情况的区域。

(2)、Java虚拟机栈:该块内存描述的是Java方法调用的内存模型,每个方法在被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。

(3)、本地方法栈。本地方法调用的内存模型。

(4)、Java堆。Java中的对象以及类的静态变量的存放地方。

(5)、方法区:方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等

(6)、运行时常量池:Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。

(7)、本机直接内存(Direct Memory)

在JDK1.4中新加入了NIO类,引入一种基于渠道与缓冲区的I/O方式,它可以通过本机Native函数库直接分配本机内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和本机堆中来回复制数据。

2、Java类加载机制的特点:

(1)基于父类的委托机制:运行一个程序时,总是由AppClass Loader(系统类加载器)开始加载指定的类,在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载,Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。如果父类加载器找不到给定的类名,则交由子加载器去加载,如果最低一层的子加载器也无法找到,则抛出异常。

(2)全盘负责机制:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class锁依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。

(3)缓存机制:缓存机制将会保证所有加载过的Class对象都会被缓存,当程序中需要使用某个Class时,类加载器会先从缓冲区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转化为Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。同时,往们比较A.getClass()与B.getClass()是否相等时,直接使用==比较,因为缓存机制保证类的字节码在内存中只可能存在一份。

(4)类加载器的三种方法以及其区别:

1)、命令行启动应用时候由JVM初始化加载

2)、通过Class.forName()方法动态加载

3)、通过ClassLoader.loadClass()方法动态加载 //使用Class.forName()来加载类,默认会执行初始化块 , //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块。

4)区别:使用ClassLoader.loadClass()来加载类,不会执行初始化块,

3、类的主动引用

    什么情况下需要开始类加载过程的第一个阶段,也即类的初始化阶段。Java虚拟机规定了有且只有5种情况下必须立即对类进行初始化:

(1)、遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要触发其初始化。(而且初始化的时候按照先父后子的顺序)。这四条指令最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰,已在编译时期把结果放入常量池的静态字段除外)、调用一个类的静态方法的的时候。

(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先对其进行初始化。

(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。但是一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会被初始化。

(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

(5)当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有初始化过,则需要先触发其初始化。

4、类的被动引用

1、对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

2、通过数组定义来引用类,不会触发类的初始化SuperClass[]sca=new SuperClass[10].

3、常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发常量的类的初始化。

class A{
static{
System.out.println("A");
}
public final static int x=100;//x为常量,调用结果不会输出A(如果去掉final,调用结果会输出A)

}
public class Test{
void print(){
System.out.println(A.x);//由于在编译的时候,A.x存入到Test调用类的常量池中,因此在调用的时候不会直接引用到A类,因此不会触发A类的初始化

}
public static void main(String[]args){
new Test().print();
}
}

5、Java对象的创建过程以及如何保证对象创建的多线程的安全性:

    虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有则进行类加载过程。

    在类加载通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存的大小在类加载完成后便可完全确定。为对象分配空间的任务等价于把一块确定大小的内存从Java堆中划分出来。

    保证多线程的安全性。有两种方案,一种是对分配内存的动作进行同步操作,实际上虚拟机采用CAS加上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间中进行。即为每个线程在Java堆中预先分配一小块内存,成为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要分配新的TLAB。

6、什么时候判断一个对象可以被回收?

    用可达性分析算法。这个算法的基本思路就是通过一系列的成为“GC roots”的对象作为起始点,从这些节点开始向下搜索,如果一个对象到GCroots没有任何引用链相连,则证明此对象是不可用的。可作为GCroots的对象包括虚拟机栈中引用的对象、方法区中常量引用的对象、方法区中静态属性引用的对象或者本地方法栈中JNI引用的对象,这些对象的共同点都是生命周期与程序的生命周期一样长,一般不会被GC。判断一个对象死亡,至少经历两次标记过程:如果对象在进行可达性算法后,发现没有与GC Roots相连接的引用链,那他将会被第一次标记,并在稍后执行其finalize()方法。执行是有机会,并不一定执行。稍后GC进行第二次标记,如果第一次标记的对象在finalize()方法中拯救自己,比如把自己赋值到某个引用上,则第二次标记时它将被移除出“即将回收”的集合,如果这个时候对象还没有逃脱,那基本上就会被GC了。

7、关于finalize()方法的作用的说明:

    finalize()方法的工作原理理论上是这样的:一旦垃圾回收器准备好释放占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存,所以使用finalize()的目的就是在垃圾回收时刻做一些重要的清理工作。我们知道,使用GC的唯一原因就是回收程序不再使用的内存,所以对于与垃圾回收有关的任何行为来说,包括finalize()方法,它们也必须同内存及其回收有关。个人认为Java对象的finalize()方法有两个作用(1)回收通过创建对象方式以外的方式为对象分配了存储空间。比如,比如在Java代码中采用了JNI操作,即在内存分配时,采用了类似C语言中的malloc函数来分配内存,而且没有调用free函数进行释放。此时就需要在finalize()中用本地方法调用free函数以释放内存。(2)对象终结条件的验证,即用来判定对象是否符合被回收条件。比如,如果要回收一个对象,对象被清理时应该处于某种状态,比如说是一个打开的文件,在回收之前应该关闭这个文件。只要对象中存在没有被适当清理的部分,finalize()就可以用来最终法相这种情况。因为对象在被清理的时候肯定处于生命周期的最后一个阶段,如果此时还含有一些未释放的资源,则有能力释放这些资源。这个不是C/C++里面的析构函数,它运行代价高昂,不确定性大,无法保证各个对象的调用顺序。需要关闭外部资源之类的事情,基本上它能做的使用try-finally可以做的更好。 

8、一个类被回收的条件。

(1)、该类所有的实例都已经为GC,也就是说JVM中不存在该Class的任何实例。

(2)、加载该类的ClassLoader已经被GC。

(3)该类对应的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问类的方法。

9、垃圾回收算法:

(1)、标记-清除算法:标记阶段根据根节点标记所有从根节点开始的可达对象。则未被标记的对象就是未被引用的垃圾对象,然后在清除阶段,清楚所有未被标记的对象。其最大缺点是空间碎片。

(2)、复制算法:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清楚正在使用的内存快中的所有对象,然后交换两个内存的角色。完成垃圾回收。这种算法比较适合新生代,因为在新生代,垃圾对象通常会多于存活对象,复制算法效果较好。Java的新生代串行GC中,就使用了复制算法的思想。新生代分为eden空间、from空间和to空间三个部分。From和to空间可以视为用于复制的两块大小相同、地位相等、且可以进行角色互换的空间块。From和to空间也成为survivor空间,即幸存者空间,用于存放未被回收的对象。

(3)、标记-压缩算法:标记过程与标记清楚算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。适合老年代的回收。

(4)、分代收集算法。

10、垃圾回收器。

(1)、Serial收集器 

单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World,下称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。 

(2)、ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。 

(3)Parallel Scavenge收集器(下称PS收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。 

(4)4.Serial Old收集器 Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器

(5) Parallel Old收集器 

    老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。

(6)CMS(Concurrent Mark Sweep)收集器 

CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要。

(7)、G1收集器。

11、内存分配与回收策略:

(1)、规则一:通常情况下,对象在eden中分配。当eden无法分配时,触发一次Minor GC。 

(2)、规则二:配置了PretenureSizeThreshold的情况下,对象大于设置值将直接在老年代分配。 

(3)、规则三:在eden经过GC后存活,并且survivor能容纳的对象,将移动到survivor空间内,如果对象在survivor中继续熬过若干次回收(默认为15次)将会被移动到老年代中。回收次数由MaxTenuringThreshold设置。 

(4)、规则四:如果在survivor空间中相同年龄所有对象大小的累计值大于survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代,无需达到MaxTenuringThreshold中要求的年龄。 

(5)、规则五:在Minor GC触发时,会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,改为直接进行一次Full GC,如果小于则查看HandlePromotionFailure设置看看是否允许担保失败,如果允许,那仍然进行Minor GC,如果不允许,则也要改为进行一次Full GC。 

11、关于Minor GC与Full GC

  Java堆,分配对象实例所在空间,是GC的主要对象。分为新生代(Young Generation/New)和老年代(Tenured Generation/Old)。新生代又划分成Eden Space、From Survivor/Survivor 0、

To Survivor/Survivor 1。

    新生代要如此划分是因为新生代使用的GC算法是复制收集算法。新生代使用赋值收集算法,但是为了内存利用率,只使用一个Survivor空间来作为轮转备份(之所以把该空间分为FromSpace和ToSpace两部分是为了在Minor GC的时候把一些age大的对象从新生代空间中复制到老年代空间中)这种算法效率较高,而GC主要是发生在对象经常消亡的新生代,因此新生代适合使用这种复制收集算法。由于有一个假设:在一次新生代的GC(Minor GC)后大部分的对象占用的内存都会被回收,因此留存的放置GC后仍然活的对象的空间就比较小了。这个留存的空间就是Survivor space:From Survivor或To Survivor。这两个Survivor空间是一样大小的。例如,新生代大小是10M(Xmn10M),那么缺省情况下(-XX:SurvivorRatio=8),Eden Space 是8M,From和To都是1M。

    在new一个对象时,先在Eden Space上分配,如果Eden Space空间不够就要做一次Minor GC。Minor GC后,要把Eden和From中仍然活着的对象们复制到To空间中去。如果To空间不能容纳Minor GC后活着的某个对象,那么该对象就被promote到老年代空间。从Eden空间被复制到To空间的对象就有了age=1。此age=1的对象如果在下一次的Minor GC后仍然存活,它还会被复制到另一个Survivor空间(如果认为From和To是固定的,就是又从To回到了From空间),而它的age=2。如此反复,如果age大于某个阈值(-XX:MaxTenuringThreshold=n),那个该对象就也可以promote到老年代了。

    如果Survivor空间中相同age(例如,age=5)对象的总和大于等于Survivor空间的一半,那么age>=5的对象在下一次Minor GC后就可以直接promote到老年代,而不用等到age增长到阈值。

    在做Minor GC时,只对新生代做回收,不会回收老年代。即使老年代的对象无人索引也将仍然存活,直到下一次Full GC。

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果经过Minor GC后仍有大量对象存活的情况,则需要老年代进行分配担保,把Survior无法容纳的对象直接进入老年代。

13、四种引用类型:

(1)、强引用:直接关联,虚拟机永远不会回收。

(2)、软引用:描述一些还有用但并非必须的对象,虚拟机会在抛出内存溢出异常之前会对  这些对象进行第二次回收。

(3)弱引用:虚拟机一定会回收的对象

(4)虚引用:为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

14、关于Java中生成对象的4种方式与区别:

 (1)、使用new操作符,这是最普遍的一种(会调用相应的构造函数):

     如:String s=new String("abc");

 (2)使用反射动态生成(会调用相应的构造函数):

    利用Class,ClassLoader,Constructor中的方法可以动态的生成类实例

    如:Object o=Class.forName("java.lang.String").newInstance();

        Object o=String.class.getClassLoader.loadClass("java.lang.String").newInstance();

        以上的方式需要目标类拥有公有无参构造函数

    以下使用Constructor进行动态生成

    class User{

       public User(String user,Integer id){}

    }

    Constructor c=User.class.getConstructor(new Class[]{String.class,Integer.class});

    User user=(User)c.newInstance(new Object[]{"zhang san",123});

(3)使用克隆生成对象(不会调用构造函数)

   例如使用一个实现了Cloneable接口的对象,调用其clone()方法获得该对象的一份拷贝,使用Java序列化方式实现深拷贝。

(4)利用反序列化从流中生成对象(不会调用构造函数):

   利用ObjectInptuStream的readObject()方法生成对象

15、Java关键字new和newInstance的区别方法.

  (1)在初始化一个类,生成一个实例的时候,newInstance()方法和new关键字除了一个是方法,一个是关键字外,最主要有什么区别?它们的区别在于创建对象的方式不一样,前者是使用类加载机制,后者是创建一个新类。那么为什么会有两种创建对象方式?这主要考虑到软件的可伸缩、可扩展和可重用等软件设计思想。

  (2)Java中工厂模式经常使用newInstance()方法来创建对象,因此从为什么要使用工厂模式上可以找到具体答案。 例如:

    Class c = Class.forName(“Example”);

  factory = (ExampleInterface)c.newInstance();

或者:String className = readfromXMlConfig;//从xml 配置文件中获得字符串

  class c = Class.forName(className);

  factory = (ExampleInterface)c.newInstance();

    上面代码已经不存在Example的类名称,它的优点是,无论Example类怎么变化,上述代码不变,甚至可以更换Example的兄弟类Example2 , Example3 , Example4……,只要他们继承ExampleInterface就可以。

  (3)从JVM的角度看,我们使用关键字new创建一个类的时候,这个类可以没有被加载。但是使用newInstance()方法的时候,就必须保证:1、这个类已经加载;2、这个类已经连接了。而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载java API的那个加载器。现在可以看出,newInstance()实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。 这样分步的好处是显而易见的。我们可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。

  (4)最后用最简单的描述来区分new关键字和newInstance()方法的区别:

  newInstance: 弱类型。低效率。只能调用无参构造。

  new: 强类型。相对高效。能调用任何public构造
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息