问:String,StringBuffer,StringBuilder的区别
答:
- String是java中的字符串类型,被final关键字修饰,内部使用final修饰的char[] value存放字符,和final修饰的count来存储数组长度。因为是final修饰的类,所以不能被继承。因为final修饰的 value和count,所以不可被改变,所以String是不可变数据,可以被线程安全的共享。所以总结下来,String本质是字符数组,不可继承切不可改变。对String进行操作时,每次都是生成新的String对象。
- 因为String的特性,java的字符串运算比较耗性能,所以提供了可变的字符串实现,也就是StringBuilder。StringBuilder继承了AbstractStringBuilder,AbstractStringBuilder也是通过一个char类型的数组value和count实现的。value和count是可变的,所以append方法中会把新的字符添加到value中并且改变count的值。当value放不下的时候,会进行扩容操作。扩容是将value容量变成count+1再乘以2,如果还小于所需的最小长度,就扩容的所需的最小长度。扩容之后用Arrays.copyOf复制字符。
- StringBuffer跟StringBuilder的实现原理一样,只是所有的方法都加了同步操作,所以效率会低,但是线程安全。
问: 下面一行代码到底做了什么呢?
String s = new String("ddd")
答:
- 在编译期,编译器到常量池里面寻找有没有"ddd"这个字符串,如果没有则在常量池中开辟空间存放"ddd";在运行时,在堆内存中开辟空间存放String类型的对象,然后在栈中开辟空间存放变量s。
问:既然讲到常量池,堆内存,栈,那就讲一下这些都是什么
答:
堆内存,栈内存这些都是jvm的逻辑内存模型。java的运行期数据区分为5部分,分别是方法区,java堆,java栈,程序计数器和本地方法栈。其中方法区和java堆是所有线程共享的,其他的都是线程私有的。
程序计数器,是一块很小的内存空间,可以看作是当前线程执行的字节码的行号。存储的是正在执行的java方法的虚拟机字节码指令地址。如果线程在执行native方法,则存储0.此内存区域是唯一没有OutOfMemoryError的区域。
-
方法区,jvm内部存储类型信息的地方。类型信息是由类加载器加载类的时候生成的。一个类要被使用会由java虚拟机对.class文件进行装载,连接(验证,准备,解析)和初始化。方法区会存储类型信息,字段信息,方法信息和其他信息。其他信息中就包含上一个问题说到的常量池。jvm为每一个已加载的类分配一个常量池,包含实际的常量(String,Integer和floating pointer常量)和对类型,字段和方法的符号引用。
- 方法区是所有线程共享的,所以数据必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待
- 方法区大小可以动态调整,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
- 方法区也会发生gc,主要是针对常量池的回收和对类型的卸载
- HotSpot会用永久代来实现方法区
-
java栈,也叫虚拟机栈,是描述java方法执行的内存模型的。每个方法被执行的时候,都会生成一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。java栈是线程私有的,随线程的生命周期。每一个方法被调用到执行完成,就对应着一个栈帧从入栈到出栈。对于执行引擎来说,只有栈顶的栈帧是有效的,被称作当前栈帧。
- 局部变量表,用于存放方法参数和方法内部定义的局部变量。局部变量表以变量槽(slot)为单位,32位系统中,一个slot可以存放32位以内的数据类型,包括(boolean,byte,short,int,float,reference和returnAddress)。returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
- 操作数栈,和局部变量表一样被设计成以字长为单位的数组。但操作只能按照栈的方式来操作。也是操作一样的数据类型。byte,short和char在入栈之前会转换成int类型的。虚拟机的大部分工作都是基于操作数栈。比如加法操作,就是先往栈里压入两个int类型的数据,再弹出两个变量执行iadd操作,然后将计算的值存的局部变量表里。伪指令如下:
<pre>
begin
iload_0 // push the int in local variable 0 ontothe stack
iload_1 //push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
</pre>- 动态链接,虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用。如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法。如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
- 返回地址,方法返回有两种情况,1是正常退出,2是异常退出。正常退出要根据方法定义看是否返回数据给上层调用者,调用者的pc计数器的值就可以作为返回地址;异常退出要根据异常处理表来确定返回地址。方法退出时要恢复上层方法的局部变量表和操作数栈,如果有返回值的话,就把返回值压入调用者栈帧的操作数栈,并把pc计数器的值调整为调用入口的下一条指令
本地方法栈,与虚拟机栈类似。只不过虚拟机栈为执行java方法服务,本地方法栈为执行native方法服务。
-
java堆,堆是jvm管理的最大的内存块,所有的线程共享,用于存放对象实例。垃圾回收也主要发生在这块区域中。
- 堆的大小可以通过 -Xms和-Xmx配置
- 从内存回收的角度看,现在收集器都采用分代收集算法。将内存分为新生代和老年代。新生代又分为Eden区和Survivor区,Survivor区又分为from区和to区
- 老年代用于存放经过新生代GC后还存活的对象和大对象
- 通过-Xmn指定新生代大小,通过-XX:PretenureSizeThreshold设置多大的对象直接分配在老年代中
- 内存分配过程
- JVM 会试图为相关Java对象在Eden Space中初始化一块内存区域。
- 当Eden空间足够时,内存申请结束;否则到下一步。
- JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收)。释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。
- Survivor区被用来作为Eden及Old的中间交换区域,当Old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区。
- 当Old区空间不够时,JVM 会在Old区进行完全的垃圾收集(0级)。
- 完全垃圾收集后,若Survivor及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现“outofmemory”错误。
问:刚刚讲到java内存中的垃圾回收,可以说一下你理解的垃圾回收吗
答:
- 垃圾回收机制最早出现在lisp语言中,后来java借鉴了过来。垃圾回收主要是让程序自己管理内存的回收,而不用程序员手动去回收内存。一般来说,java程序员可以不重视java内存分配和垃圾回收,但充分了解jvm的GC机制可以让程序员更好的利用计算机资源。
- gc要为程序员管理内存,就要确定哪些内存需要回收,什么时候回收和怎么回收。
- 确定哪些内存需要回收,有两种方法:
- 引用计数法,创建对象的时候,为这个对象在堆栈中分配内存,同时产生一个引用计数器并加1,对象的一个引用销毁的时候,引用计数器减1,引用计数器为0的时候,表示这个对象可以回收了。jdk1.2之前用的这种方法。但是这样会有循环引用问题,就是A保留B的引用,B保留A的引用,虽然都没有外部引用了,但引用计数器不为0,所以不能回收。
- 根搜索算法,利用图论中的理论,从一个节点GC ROOT根开始搜索,所有子引用节点搜索完成后,剩余未被搜索到的节点即无用节点,可以被回收。
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
- 两种判定算法都用到了引用,去判断对象是否可以回收。一般引用分为4种类型,强引用,软引用,弱引用和虚引用。
- 强引用就是我们平时代码中,指向新建对象的变量。只要强引用在,对象就不会被回收。
- 软引用描述一些还有用但非必要存在的对象,内存不足时才会被回收,一般和引用队列(ReferenceQueue)共同使用做内存敏感的高速缓存
- 弱引用也是描述非必要对象,不管内存足不足都会回收
- 虚引用不影响对象的生命周期,虚引用必须和引用队列关联使用,程序可以判断引用队列中是否加入虚引用来判断对象是否要被回收
- 解决了哪些内存要回收的问题之后,再就是怎么回收的问题了。垃圾回收的策略有四种,分别是标记-清除算法,复制算法,标记-整理算法和分代收集算法
- 标记-清除算法分两个阶段,先标识哪些内存需要清除,然后统一回收要清除的对象。这样有两个问题,标记和清除的效率都不高,而且会产生内存碎片问题
- 复制算法就是把内存平均分配成两块,每次使用其中一块,内存回收时,把存活的对象复制到另一块上。特点是实现简单效率高,适用于短生命周前对象,但可用内存少了一半
- 标记-整理算法,对于存活率较高的对象,如果采用复制算法就会导致多次复制,效率十分低。老年代对象就有这种特点。标记-整理算法是一种老年代回收算法,过程跟标记-清除算法类似。不同的是在第二阶段不会直接清除,而是将对象向一端移动,然后直接清理掉端边界以外的内存
- 分代算法就是将内存分为不同的块,根据每块内存的特点选择使用合适的回收算法。一般新生代采用复制算法,老年代采用标记整理算法
- 回收算法是理论基础,jvm提供了多种算法的实现,也就是我们说的垃圾回收器
- Serial回收器,采用复制算法,回收时暂停所有用户线程。适合单核cpu。是新生代垃圾回收器
- ParNew,Serial的多线程版本。适合多cpu场景。出了Serial,只有它可以和CMS配合使用
- Parallel Scavenge,也是新生代收集器,目标是提高吞吐量,适应主要适合在后台运算而不需要太多交互的任务。
- Serial Old,是老年代的单线程收集器,作为CMS收集器的后备预案
- Parallel Old,是ParallelScavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
- CMS是一种以获取最短回收停顿时间为目标的收集器,优点是并发收集,停顿低。缺点是对cpu敏感,没法处理浮动垃圾,会产生内存碎片问题
- G1,可以参考深入理解G1
- G1对空间压缩有优势
- 通过将内存分成区域(region)的方式避免内存碎片
- Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
- G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
- G1会在Young GC中使用、而CMS只能在O区使用
- 触发GC
- Minor GC(新生代回收)的触发条件比较简单,Eden空间不足就开始进行Minor GC回收新生代。
- 而Full GC(老年代回收,一般伴随一次Minor GC)则有几种触发条件:
- 老年代空间不足
- PermSpace空间不足
- 统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- 确定哪些内存需要回收,有两种方法: