该视频主要讲解的内容如下所示:
1、虚拟机的内存结构
1、每一个线程都有一个虚拟机栈,线程中每调用一个方法都会开启一个栈帧,栈帧里面保存方法中的局部变量。
2、方法区在java8以后改名为永久区域perment,存在的class 文件 字符串常量等信息,存储类相关的信息
2、堆 heap
对象分配的方式:new 一个对象,如果该对象很大,就直接分配到老年区,如果不是很大就分配带新生代的eden区域,第一次GC的时候,会把eden区域没有被回收的对象(有引用)拷贝到s0区域,第二次内存回收的时候会把eden区域没有被回收的和s0区域中的对象拷贝
到s1区域,并且情况s0区域。
再次内存回收的时候,会把eden区域没有被内存回收的对象和s1区域的对象拷贝到s0区域,然后情况s1区域,一直这样s0区域和s1区域交替使用
如果一个对象在GC的过程中,经过很多次都没有被GC,最终会被移动到老年区,这个次数可以通过参数来进行配置
eden里面的对象大部分都会被GC回收,例如100个对象,GC回收异常98个对象都会被回收。
老年代tenured中的对象都是经过很多次GC没有被回收的对象,通常配置eden:s0:s1区域的内存比例是8:1:1,new新生代区域:old区域的内存比例是1:3
3、垃圾回收器
垃圾:
1. 什么样的对象是垃圾?一般来说,所有指向对象的引用都已失效,不可能再有程序能调用到这个对象,那么这个对象就成了垃圾,应该被回收。
1.1 根据这个思路,很容易就能想到用《引用计数》的办法来确定一个对象是否是垃圾。即每当多一个引用指向对象时,引用计数加一,每当少一个引用指向对象时,引用计数减一,引用计数减到零,对象就可以被回收了。
1.2 然而引用计数有一个致命问题不好解决,就是循环引用的问题。比如说一个循环链表,他们循环引用者,引用计数永远不会为零,但是实际上程序已经不能访问他们了,他们应该被回收。
1.3 所以Java实际上是使用基于GC Roots的可达性分析,什么是GC Roots?所有类的静态变量,每个线程调用栈上的本地变量。(实际上我们编程时也是要从这些地方开始访问数据),所有这些对象,以及被这些对象所指向的对象,都是活的对象。活的对象所指向的对象也是活的对象。
1.4 所以只要在GC的时刻,让程序暂停运行,然后从GC Roots开始分析,最后没有被标记为活对象的对象就是垃圾了。
1.引用计数算法(已被淘汰的算法)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象
之间相互循环引用的问题。尽管该算法执行效率很高。
public class Main { public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.object = object2; object2.object = object1; object1 = null; object2 = null; } }
最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。
2.可达性分析算法
目前主流的编程语言(java,C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
强可及对象永远不会被gcc垃圾回收器回收
软可及对象当系统内存不足的时候,gcc会把软可及对象回收掉
弱可及对象当gcc发现这个对象是弱可及对象就马上将对象回收
1、标记清除算法 黑色的部分就是可以清除的。但是存在内存碎片化问题,未使用的内存存在不连续的情况
2、第二种算法 复制压缩算法
我们把上图中前两行标记为A区域,后面两行标记为B语言,把内存分成两个部分,一个4G的内存,有2G不能使用浪费内存
第一次GC的时候,A区域中经过垃圾可达性算法得到垃圾对象和存活对象,在GC的时候,将灰色的存活对象拷贝到B区域,在B区域中也就是第三行,存活对象在内存第三行中是连续的,然后把A区域的垃圾对象全部回收,A区域全部变成未使用的
在第二次回收的时候,B区域中的对象经过垃圾可达性算法得到垃圾对象和存活对象,在GC的时候,将灰色的存活对象拷贝到A区域,例如在A区域中也就是第一行,存活对象在第一行内存中是连续的,然后把B区域的垃圾对象全部回收,B区域全部变成未使用的
这样A和B区域交互使用
在新生代中就是采用复制压缩的算法
该算法缺点是比较浪费内存,通常配置eden:s0:s1区域的内存比例是8:1:1,这样就可以节约内存也可以保证效率
在老年代中使用,因为老年代中可回收的垃圾比较少,所以也能保证效率,但是效率比复制压缩算法较低,但是节约内存资源。
年轻代(Young Generation)
1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)
年老代(Old Generation)
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(Permanent Generation)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。在java 1.7之前也叫方法区
参看博客:https://www.cnblogs.com/andy-zcx/p/5522836.html
Java有四种类型的垃圾回收器:
串行垃圾回收器(Serial Garbage Collector)
并行垃圾回收器(Parallel Garbage Collector)
并发标记扫描垃圾回收器(CMS Garbage Collector)
G1垃圾回收器(G1 Garbage Collector)
ImportNew
首页所有文章资讯Web架构基础技术书籍教程Java小组工具资源
Java GC系列(3):垃圾回收器种类
2014/11/19 | 分类: 基础技术, 教程 | 0 条评论 | 标签: GC, 垃圾回收教程
分享到: 24
本文由 ImportNew - 好好先生 翻译自 javapapers。欢迎加入翻译小组。转载请见文末要求。
目录
垃圾回收介绍
垃圾回收是如何工作的?
垃圾回收的类别
垃圾回收监视和分析
在这篇教程中我们将学习几种现有的垃圾回收器。在Java中,垃圾回收是一个自动的进程可以替代程序员进行内存的分配与回收这些复杂的工作。这篇是垃圾回 收教程系列的第三篇,在前面的第2部分我们看到了在Java中垃圾回收是如何工作的,那是篇有意思的文章,我推荐你去看一下。第一部分介绍了Java的垃 圾回收,主要有JVM体系结构,堆内存模型和一些Java术语。
Java有四种类型的垃圾回收器:
串行垃圾回收器(Serial Garbage Collector)
并行垃圾回收器(Parallel Garbage Collector)
并发标记扫描垃圾回收器(CMS Garbage Collector)
G1垃圾回收器(G1 Garbage Collector)
每种类型都有自己的优势与劣势。重要的是,我们编程的时候可以通过JVM选择垃圾回收器类型。我们通过向JVM传递参数进行选择。每种类型在很大程度上有 所不同并且可以为我们提供完全不同的应用程序性能。理解每种类型的垃圾回收器并且根据应用程序选择进行正确的选择是非常重要的。
1、串行垃圾回收器
串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。
通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
2、并行垃圾回收器
并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候
3、并发标记扫描垃圾回收器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
当标记的引用对象在tenured区域;
在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
4、G1垃圾回收器
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器
不清楚的看博客:
http://www.importnew.com/13827.html
java中-表示标准参数 -xx表示jdk 1.5存在,可能jdk 1.6就不存在了。
java对象的分配:
生成一个对象的时候,如何开启了jvm优化开启了栈上分配的优化,如果是小对象就会在栈上分配,栈上的对象就不用进行垃圾回收,当栈帧中方法结束之后会自动回收。
TLAB本质上如下所示:在java多线程中对应ThreadLocal这个类
逃逸行为:在方法中分配的对象,在方法销毁的时候对象没有在方法栈中被销毁,而被其他全局变量引用,导致对象不能被回收。jvm进行优化的时候在栈上分配的对象是不能存在逃逸行为的,在栈上分配对象不能存在逃逸行为,
-XX:+DoEscapeAnalysis -XX:+PrintGC 是开启逃逸分析,如果写成-XX:-DoEscapeAnalysis 禁止逃逸分析,对象就不能分配在栈上。默认情况下是开启逃逸分析的
package com.weiyuan.test; import java.lang.management.ManagementFactory; public class Test01 { /** * 逃逸分析优化-栈上分配 * 栈上分配,意思是方法内局部变量(未发生逃逸)生成的实例在栈上分配,不用在堆中分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。 * 一般生成的实例都是放在堆中的,然后把实例的指针或引用压入栈中。 * 虚拟机参数设置如下,表示做了逃逸分析 消耗时间在10毫秒以下 * -server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC * * 虚拟机参数设置如下,表示没有做逃逸分析 消耗时间在1000毫秒以上 * -server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC * @author 734621 * */ public static class User{ private int i ; public User(){ } } public static void alloc(){ User usr = new User(); } public static void main(String [] args){ long b = System.currentTimeMillis(); for(int i=0;i<1000000000;i++){ alloc(); } long e = System.currentTimeMillis(); System.out.println("消耗时间为:" + (e - b)); } }
开启逃逸分析,允许在栈上分配:运行的时间是消耗时间为:2
关闭在栈上分配,对象不能分配在栈上分配,也不允许在TLAB上分配,只允许在堆上分配 -server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:-UseTLAB -XX:+PrintGC
命令: -XX:-DoEscapeAnalysis 禁止在栈上分配,-XX:-UseTLAB禁止在TLAB上分配,所以只能在堆上分配
[GC 3080K->520K(10240K), 0.0002258 secs]
[GC 3080K->520K(10240K), 0.0002543 secs]
[GC 3080K->520K(10240K), 0.0002729 secs]
[GC 3080K->520K(10240K), 0.0002425 secs]
[GC 3080K->520K(10240K), 0.0003005 secs]
[GC 3080K->520K(10240K), 0.0002861 secs]
[GC 3080K->520K(10240K), 0.0003239 secs]
[GC 3080K->520K(10240K), 0.0002813 secs]
[GC 3080K->520K(10240K), 0.0002765 secs]
[GC 3080K->520K(10240K), 0.0003053 secs]
[GC 3080K->520K(10240K), 0.0002210 secs]
[GC 3080K->520K(10240K), 0.0002335 secs]
[GC 3080K->520K(10240K), 0.0002101 secs]
[GC 3080K->520K(10240K), 0.0002367 secs]
[GC 3080K->520K(10240K), 0.0002810 secs]
[GC 3080K->520K(10240K), 0.0002447 secs]
[GC 3080K->520K(10240K), 0.0003416 secs]
[GC 3080K->520K(10240K), 0.0003339 secs]
[GC 3080K->520K(10240K), 0.0002489 secs]
[GC 3080K->520K(10240K), 0.0002415 secs]
[GC 3080K->520K(10240K), 0.0002794 secs]
[GC 3080K->520K(10240K), 0.0002149 secs]
[GC 3080K->520K(10240K), 0.0002691 secs]
[GC 3080K->520K(10240K), 0.0002916 secs]
[GC 3080K->520K(10240K), 0.0002502 secs]
[GC 3080K->520K(10240K), 0.0002839 secs]
[GC 3080K->520K(10240K), 0.0066899 secs]
[GC 3080K->520K(10240K), 0.0003018 secs]
[GC 3080K->520K(10240K), 0.0003053 secs]
[GC 3080K->520K(10240K), 0.0002319 secs]
[GC 3080K->520K(10240K), 0.0003124 secs]
消耗时间为:13922
运行的时间是栈上运行的几千倍
逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
public static StringBuffer craeteStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; }
StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。
甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
上述代码如果想要StringBuffer sb不逃出方法,可以这样写:
public static String createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。
如果能证明一个对象不会逃逸到方法或线程外,则可能为这个变量进行一些高效的优化。
1. 栈上分配
我们都知道Java中的对象都是在堆上分配的,而垃圾回收机制会回收堆中不再使用的对象,但是筛选可回收对象,回收对象还有整理内存都需要消耗时间。如果能够通过逃逸分析确定某些对象不会逃出方法之外,那就可以让这个对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
在一般应用中,如果不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了。
Java程序中,每个线程都有自己的Stack Space(堆栈)。这个Stack Space不是来自Heap的分配。所以Stack Space的大小不会受到-Xmx和-Xms的影响,这2个JVM参数仅仅是影响Heap的大小。
Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆 栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一 个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
package com.weiyuan.test; import java.lang.management.ManagementFactory; public class Test02 { static int count = 0; static void alloc(){ //无限制递归,会导致栈空间溢出 System.out.println("alloc is called"+count); count = count +1; alloc(); } public static void main(String [] args){ try{ System.out.println("count ="+count); alloc(); }catch(Exception e){ System.out.println(""+count); } } }
先将栈大小设置成512K
日志打印如下:
alloc is called5996
alloc is called5997
Exception in thread "main" java.lang.StackOverflowError
最大可以递归5997次
如果改成1024K
alloc is called12548
alloc is called12549
Exception in thread "main" java.lang.StackOverflowError
我们可以设置栈的大小达到我们的需求