Java内存区域
JVM主要组成部分以及作用
JVM主要由两个子系统:类加载、执行引擎;和两个组件:运行时数据区,本地接口组成
- 类加载器:将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区的数据结构
- 执行引擎:执行classes中的指令
- 本地接口:与native libraries交互,是其他编程语言交互的接口
- 运行时数据区:JVM的内存
作用:首先编译器将代码转换成字节码,类加载器把字节码中的二进制数据加载到运行时数据区的方法区中,执行引擎需要把字节码翻译成底层系统指令交给CPU执行,在这个过程中需要调用其他语言的本地库接口来实现
JVM运行时数据区
运行时数据区分为以下五个部分:
- 程序计数器 当前线程所执行的字节码的行号指示器,分支,循环,跳转,异常处理都需要依赖这个计数器完成
- 堆 所有线程共享的,几乎所有的对象实例都在这里分配内存
- 虚拟机栈 用于储存局部变量表、操作数栈、动态链接、方法出口等信息
- 本地方法栈 与虚拟机栈作用一样,只不过本地方法栈是为虚拟机调用Native方法服务的
- 方法区 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据
深拷贝和浅拷贝
浅拷贝只是增加了一个指针指向已存在的内存地址 深拷贝是增加了一个指针并申请了一个新的内存地址,使这个指针指向了新的内存地址
说一下堆栈的区别
- 物理地址
- 堆的物理地址分配是不连续的,因此性能较慢
- 栈的物理地址分配是连续的,性能快
- 内存分别
- 堆因为不连续,所以分配内存是在运行期确认的,大小不固定
- 栈连续的,分配的内存大小在编译期就确认,大小固定
- 程序可见度
- 堆对整个应用程序都是共享可见的
- 栈只对于线程可见,线程私有的生命周期和线程相同
- 存放的内容
- 堆存放的是对象的实例和数组,更关注数据的存储
- 栈存放的是局部变量、操作数栈、返回结果,更关注程序方法的执行
注意:静态变量放在方法区,静态对象还是放在堆
队列和栈的区别
队列和栈都是用来预存储数据的
- 操作名称不同
- 队列的插入叫入队,删除叫出队
- 栈的插入叫入栈,删除叫出栈
- 可操作的方式不同
- 队列是在队尾入队,队头出队
- 栈的入栈出栈都是在栈顶进行
- 操作方法不同
- 队列总是先进先出
- 栈是先进后出
HotSpot虚拟机对象探秘
对象的创建
虚拟机遇到一条new指令时,会检查常量池是否已加载对应的类如果没有,先执行对应的类加载,类加载以后检查队中内存是否规整,如果规整,使用指针碰撞的方式分配内存,如果不是规整的,使用空闲列表的方式分配内存,划分内存时还要考虑并发问题,有两种解决方案,CAS同步处理或本地线程分配缓冲(TLAB),然后内存空间初始化操作,做一些必要的对象设置(元信息、哈希码等)最后执行方法
为对象分配内存
类加载完成后,会在堆中分配一块内存给对象,根据内存是否规整有两种分配方式:
- 指针碰撞 如果堆内存是规整的,即用过的内存放在一边,没有用过的内存放在另一边,分配内存时将中间的指针指示器向空闲的内存移动一段与对象大小相等的距离
- 空闲列表 如果堆内存不是规整的,需要由虚拟机维护一个列表来记录哪些内存是可用的,在分配的时候从列表查询足够的内存分配给对象,并在分配后更新列表记录
注意:内存分配方式取决于堆内存是否规整,而堆内存是否规整取决于所用的垃圾收集器是否带有压缩处理功能决定
处理并发安全问题
为了解决分配内存时的并发问题,有两种解决方案:
- 对分配内存的动作进行同步处理(采用CAS+失败重试来保障更新操作的原子性)
- 每个线程在堆中预先分配一块内存,称为本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的TLAB上分配,只有当TLAB用完并分配新的TLAB才需要同步锁,通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB
对象的访问定位
java程序需要通过栈上的引用访问堆上的对象,对象访问的方式取决于JVM虚拟机的实现,目前主流的访问方式有句柄和直接指针
句柄访问
堆中划分一块内存来作为句柄池,引用中存放对象的句柄地址,句柄中包含了对象实例数据和对象类型数据的具体地址信息,构造如下
优势:引用存放的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,引用本身不需要修改
直接指针
引用中存储的直接是对象地址
优势:节省了一次指针定位的时间开销,速度更快,HotSpot中采用的就是这种方式
内存溢出异常
Java中会存在内存泄露吗 内存泄漏指不再被使用的对象或变量一直占据在内存中,理论上来说,JAVA有GC垃圾回收机制,不再被使用的对象,会被GC自动回收。即使这样还是会存在内存泄漏的情况,就是长生命周期的对象持有短生命周期的对象的引用,导致短生命周期的对象不能被回收
垃圾收集器
简述垃圾回收机制
在java中,程序员不需要关心对象的内存释放,在虚拟机中,有一个低优先级的垃圾回收线程,只有在虚拟机空闲或堆内存不足时,才会触发执行,扫描那些没有被引用的对象,将它们添加到要回收的集合中,进行回收
垃圾回收机制的优点
有效的防止了内存泄漏,有效的使用可使用的内存
垃圾回收器的基本原理
对GC来说,当程序员创建对象时,GC就开始监控这个对象的大小、地址以及使用情况,通常,GC采用有向图的方式记录和管理堆中的所有对象,通过这种方式确认哪些对象是"可达的",哪些对象是"不可达的",当GC确定一些对象是不可达时,GC就有责任回收这些内存空间
有什么办法主动通知虚拟机进行垃圾回收
可以手动执行System.gc(),通知GC运行,但Java语言规范并不保证GC一定执行
Java中有哪些引用类型
- 强引用 发送gc时不会被回收
- 软引用 有用但不是必须的对象,在发生内存溢出时会被回收
- 弱引用 有用但不是必须的对象,在下一次GC时会被回收
- 虚引用 无法通过虚引用获得对象,用PhantomReference实现虚引用,用途是在gc时返回一个通知
怎么判断对象是否可以被回收
引用计数法 为每个对象创建一个引用计数,有对象引用时,计数器加1,引用被释放时,计数器减1,当计数器为0时就可以被回收,但无法解决循环引用的问题 可达性分析算法 从GCRoots开始向下搜索,搜索走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连时,则该对象可以被回收
JVM中的永久代中会发生垃圾回收吗
垃圾回收不会发生在永久代,当永久代满了或超过临界值,会触发完全垃圾回收(Full GC),正确的永久代大小对避免FullGC是非常重要的(Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)
说一下JVM的垃圾收集算法
- 标记-清除算法
- 标记无用对象,然后进行清除回收
- 缺点:效率不高,无法清除垃圾碎片
- 复制算法
- 按照容量划分两个相等的内存区域,当一块用完时将活着的对象复制到另一块上,把已使用的内存空间一次性清理掉
- 缺点:内存使用率低,只有原来的一半
- 标记-整理算法
- 标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存
- 分代算法
- 根据对象存活周期的不同将内存划分为不同的区域,一般是新生代和老年代,新生代采用复制算法,老年代采用标记整理算法(因为老年代的对象存活率高,会有较多的复制操作,如果采用复制算法效率会变低)
说一下JVM有哪些垃圾回收器
垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现,下图展示了作用于不同分代的垃圾收集器,其中用于回收新生代的垃圾收集器包括Serial、ParNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有作用于整个Java堆的G1收集器,不同收集器之间连线代表它们可以搭配使用
- Serial收集器(复制算法)
新生代单线程收集器,标记和清除都是单线程,优点是简单高效 - ParNew收集器(复制算法)
新生代并行收集器,是Serial收集器的多线程版本,在多核CPU情况下比Serial表现更好 - Parallel Scavenge收集器 (复制算法)
新生代并行收集器,追求高吞吐量,高效利用CPU,高吞吐量=用户线程时间/(用户线程时间+GC线程时间),,可以高效利用CPU,适合对交互要求不高的场景 - Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本 - Parallel Old收集器(标记-整理算法)
老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本 - CMS(Concurrent Mark Sweep)收集器(标记-清除算法)
老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间 - G1(Garbage First)收集器(标记-整理算法)
Java堆并行收集器,JDK1.7以后提供的,基于标记-整理算法,不会产生内存碎片
详细介绍一下CMS垃圾收集器
CMS(Concurrent Mark-Sweep),以牺牲吞吐量为代价获得最短回收停顿时间的收集器,对于要求服务器响应速度的应用上,非常适合,使用参数"-XX:+UseConcMarkSweepGC"来指定使用CMS垃圾回收器。 由于CMS使用的是标记-清除算法,会产生内存碎片,所以当剩余内存不满足程序运行要求时,就会出现Concurrent Mode Failure,会临时采用Serial Old回收器进行垃圾收集,此时性能会被降低
简述分代垃圾回收器是怎么工作的
分代收集器有两个分区:新生代和老年代,新生代默认占比1/3,老年代默认占比2/3 新生代使用的是复制算法,新生代有三个分区:Eden、To Survivor、From Survivor,默认占比是8:1:1。执行流程如下:
- 把Eden+From Surivivor存活的对象放入To Survivor区
- 清空Eden和From Survivor区
- From Survivor和To Survivor分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor
每次在From Survivor到To Survivor移动时都存活的对象,年龄加1,当年龄达到15(默认配置是15)时升级为老年代,大对象也会直接进入老年代,老年代当空间占用达到某个值就会触发全局垃圾回收(Full GC),一般使用标记整理的执行算法
内存分配策略
简述java内存分配与回收策略以及Minor GC和Major GC
所谓自动内存管理,要解决的就是内存分配和内存回收的问题,前面介绍了内存回收,这里介绍一下内存分配。
对象的内存分配通常在Java堆上(随着JVM虚拟机优化技术的诞生,某些场景也会在栈上分配),对象主要分配在新生代的Eden区,如果启动了本地线程缓冲,则按照线程优先在TLAB上分配,少数情况下也会直接在老年代上分配,总的来说,分配规则不是百分百固定的,其细节取决于哪一种垃圾收集组合以及虚拟机相关参数有关,但是对于内存分配还是会遵循以下几种普世规则:
- 对象优先在Rden区分配
多数情况,对象在新生代Eden区分配,当Eden区分配没有足够空间,会触发一次Minor GC,如果本次GC后还是没有足够空间,将启用分配担保机制在老年代中分配内存,这里我们提到了Minor GC,还有Major GC/Full GC。- Minor GC指发生在新生代的GC,由于对象大多是朝生夕死,因此Minor GC会很频繁,一般回收速度也非常快
- Major GC/Full GC是指发生在老年代的GC,发生Major GC通常会伴随至少一次Minor GC。Major GC的速度通常比MinorGC慢10倍以上
- 大对象直接进入老年代
如果大对象直接在Eden区分配,由于新生代使用的是复制算法,可能会导致Eden区和两个Survivor区发生大量内存复制。 - 长期存活对象将进入老年代
虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在Eden区出生,并能够被Survivor容纳,将被移动到Survivor区中,这时设置对象年龄为1,对象每在Survivor区中熬过一次Minor GC,年龄就加1,当年龄达到15(默认15)就会被晋升到老年代