堆与栈
栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储问题,即数据怎么放、放在哪儿。
在 Java 中一个线程就会相应有一个线程栈与之对应,返点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享。栈因为是运行单位,因此里面存储 的信息都是跟当前线程(或程序)相关信息。包括局部变量、程序运行状态、方法返回值等等;而堆叧负责存储对象信息。
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
- 从软件设计角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清 晰。这种隔离、模块化的思想在软件设计的方方面面都有体现。
- 堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。 这种共享收益,一方面提供了一种有效的数据交互方式(如:共享内存);另一方面,堆中的共享常量呾和缓存可以被所有栈访问,节省了空间。
- 面向对象就是堆与栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题癿思考方式发生了改变,而更接近与自然方式癿的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法), 就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写了处理数据的逻辑。
堆中存什么?栈中存什么?
堆堆=中存的是对象。是一个运行时数据区,类的对象从中分配空间。这些对象通过 new、newarray、anewarray 和 multianewarray 等 指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事 先告诉编译器,因为它是在运行时 动态分配内存的,Java 的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在 运行时动态分配内存,存取速度较慢。
栈栈中存的是基本数据类型和堆中对象的引用。优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的, 缺乏灵活性。栈中主要存放一些基本类 型的变量(,int, short, long, byte, float, double, boolean, char)和对象句柄。
String 是一个特殊的包装类数据。可以用:
String str = new String("abc");
String str = "abc";
两种的形式来创建,第一种是用 new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对 象。
而第二种是先在栈中创建一个对 String 类的对象引用变量 str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令 str 指向”abc”, 如果已经有”abc” 则直接令 str 指向“abc”。
比较类里面的数值是否相等时,用 equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。 String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
可以看出 str1 和 str2 是指向同一个对象的。
String str1 =new String ("abc");
String str2 =new String ("abc"); System.out.println(str1==str2); // false
用 new 的方式是生成不同的对象。每一次生成一个。
因此用第一种方式创建多个”abc”字符串,在内存中其实只存在一个对象而已. 这种写法有利与节省内存空间. 同时它可以在一定程度上提高程序的 运行速度,因为 JVM 会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于 String str = new String("abc");的代码,则一概在堆 中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。
Java 中的参数传递时传值呢?还是传引用?
Java 在方法调用传递参数时,因为没有指针,所以它都是运行传值调用。
当进入被调用方法时,被传递的是这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是堆中的数据。
堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享内存。
垃圾回收
为什么要分代?
分代的垃圾回收策略,是基于:不同的对象的生命周期是不一样的。因此,不同生命周
期的对象可以采取不同的收集方式,以便提高回收效率。
在 Java 程序运行过程中,会产生大量对象,其中有些对象是与业务信息相关,比如 Http 请求中的Session对象、线程、Socket 连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,返些对象生命周期会比较短,比如:String 对象, 由于其不改变类的特性,系统会产生大量的这些对象,有些对象甚至叧用一次即可回收。
试想,在不进行对象存活时间区分的情冴下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
如何分代
虚拟机中癿共划分为三个代:
- 年轻代(Young Generation)
- 年老点(Old Generation)
- 持久代(Permanent Generation)
其中持久代主要存放的是 Java 类的类信息,与垃圾收集要收集的 Java 对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大。
年轻代
所有新生成的对象首先都是放在年轻代。
年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
年轻代分三个区:一个 Eden 区,两个 Survivor 区(一般而言)。
大部分对象在 Eden 区中生成。 当 Eden 区满时,还存活的对象将被复制到 Survivor 区(两个中的一个),当这个 Survivor 区满时, 此区的存活对象将被复制到另外一个 Survivor 区,当这个 Survivor 区也满了的时候,从第一个 Survivor 区复制过来的并且此时迓还存活的对象,将被复制到“年老区(Tenured)”。
需要注意,Survivor 癿两个区是对称的,没先后关系,所以同一个区中可能同时存在从 Eden 复制过来 对象,和从前一个 Survivor 复制过来的对象,而复制到年老区的只有从第一个 Survivor 去过来的对象。而且, Survivor 区总有一个是空的。同时,根据程序需要,Survivor 区是可以配置为多个的(多与两个), 这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代
在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代
用与存放静态文件,如今 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。
什么情况下触发垃圾回收
GC 有两种类型:Scavenge GC 和Full GC
Scavenge GC
一般情冴下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对 Eden区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区。这种方式的GC 是对年轻代Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的GC 会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。
Full GC
对整个堆进行整理,包括 Young、Tenured 和Perm。Full GC 因为需要对整个对进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对亍 FullGC 的调节。
Java 的垃圾回收确实带来了很多好处,为开发带来了便利。但是在一些高性能、高并发情冴下,垃圾回收确成为了制约 Java 应用的瓶颈。目前 JDK 的垃圾回收算法,始终无法解决垃圾回收时的暂停问题,因为这个暂停严重影响了程序的相应时间,造成拥塞或堆积。
调优
堆信息查看
- 查看堆空间大小分配(年轻代、年老代、持久代分配)
- 即使查看垃圾回收情况
- 长时间监控回收情况
- 查看堆中的类/对象信息
- 查看对象引用情况
通过堆信息查看方面的功能,可以顺利解决一下方面的问题:
- 年老代/年轻代划分是否合理
- 内存泄漏
- 垃圾回收算法设置是否合理
线程监控
- 线程信息监控:系统线程数量
- 线程状态监控:各个线程都处在什么样的状态下
- Dump线程详细信息:查看线程内部运行情况
- 死锁检查
热点分析
- CPU热点:检查系统哪些方法占用大量的CPU时间
- 内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)
可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化, 依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题。
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。
内存泄漏检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,而线程、热点方面的问题则是具体问题具体分析了。
内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情冴下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。
内存泄漏对系统危害比较大,因为他可以直接导致系统崩溃。
需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。
1. 年老代堆空间被占满
异常: java.lang.OutOfMemoryError: Java heap space
说明:是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。
如上图所示,这是非常典型的内存泄漏垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的移动,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。
解决:一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
2. 持久代堆空间被占满
异常: java.lang.OutOfMemoryError: PermGen space
说明:Perm 空间被占满。无法为新的 class 分配存储空间而引发异常。返个异常以前是没有的,但是在 Java 反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载, 最终导致 Perm 区被占满。更可怕的是,不同的 classLoader 即便使用了相同的类,但是都会对其进行加载,相当与同一个东西, 如果有 N 个 classLoader 那么它将会被加载 N 次。因此,某些情况下,个个问题基本规为无解。当然,存在大量 classLoader 和大量反射类的情况其实也不多。
解决:
- -XX:MaxPermSize=16m
- 换用 JDK。比如 JRocket
3. 堆栈溢出
异常: java.lang.StackOverflowError
说明:一般就是递归没返回,或者循环调用造成
4. 线程堆栈满
异常:Fatal: Stack size too small
说明:java 中一个线程的空间大小是有限制的。JDK5.0 以后这个值是 1M。与这个个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
5. 系统内存被占满
异常:java.lang.OutOfMemoryError: unable to create new native thread
说明:这个异常是由亍操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在 Java 堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后, 堆中或许还有空间,但是操作系统分配不出资源来了,就出现返个异常了。分配给 Java 虚拟机的内存越多,系统剩余的资源就越少,因此,当系统内存固定时,分配给 Java 虚 拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比关系。同时,可以通过修 改-Xss 来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。
解决:
- 重新设计系统减少线程数量。
- 线程数量不能减少的情冴下,通过-Xss 减小单个线程大小。以便能生产更多的线程。