特点:
- 线程私有。
- 不会存在内存溢出。
多线程通过cpu的调度器分配,每个线程有不同的时间片,在一个线程的时间片用完后,程序计数器记住下一次轮到此线程执行的指令的内存地址,继续执行。
2.Java Virtual Machine Stacks (Java 虚拟机栈)
定义:
- 每个线程运行需要的内存空间,称为虚拟机栈。
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧(线程正在执行的方法对应的栈帧),对应着每次方法调用时所占用的内存。
1.垃圾回收是否涉及栈内存?
栈内存:一次次方法调用所产生的栈帧内存。栈帧内存:栈帧在每一次方法调用后都会弹出栈,也就是栈帧内存会被自动回收掉。所以不需要垃圾回收管理栈内存。
垃圾回收值会回收堆内存中的无用对象,栈内存不需要。
2.栈内存分配越大越好吗?
栈内存可以通过运行虚拟机代码时,通过一个参数指定。-Xss size
方法内的局部变量是否线程安全?
线程安全:局部变量对多个线程是共享的,还是属于某个线程私有的。
某个变量是否线程安全判断方法:1、是否是方法内的局部变量 2、是否逃离了方法的作用范围(有可能被别的线程访问到,不再线程安全)。
1.方法内的局部变量没有逃离方法的作用范围,那么它就是线程安全的。
2.局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全。
2.局部变量引用的是基本类型变量,不考虑线程安全。
场景1:有两个线程同时调用这个方法m1,会不会造成x值得混乱呢。不会:x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1的栈帧中有对应的x变量是线程1私有的,线程2的栈帧中有对应的x变量是线程2私有的,各自加5000,互不影响。
场景2:有两个线程同时调用这个方法m2,会不会造成x值得混乱呢。会:x是类变量,线程共享的,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1和线程2的栈帧中是对同一个变量x操作。
场景3:针对下面程序Demo1,有两个线程同时调用这个方法m1,会不会造成sb值得混乱呢。不会:x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1的栈帧中有对应的sb变量是线程1私有的,线程2的栈帧中有对应的sb变量是线程2私有的,互不影响。
针对方法m2, sb作为参数传入,其他的线程就有可能访问到它,sb不再是线程私有的,是线程共享的。如下示例:
主线程中创建了一个StringBuilder对象sb,又在新线程里面把sb作为参数传给了m2方法,主线程和新线程都在修改sb,sb是主线程和新线程共享的,线程不安全。
场景4:m3方法中的sb对象是否线程安全呢?答:不安全。
sb虽然是方法内的局部变量,但是作为方法的返回值返回了,这样其他的线程有可能拿到sb的引用对象,并发的修改它。
2.2 栈内存溢出
- 栈帧过多导致栈内存溢出
两个类循环引用导致栈内存溢出 - 栈帧过大导致栈内存溢出
2.3线程运行中断
案例1:cpu占用过多
定位
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep pid (用ps命令进一步定位是哪个线程引起的cpu占用过高)输出的线程编号是10进制的
- jstack pid 将此进程下jvm使用的线程和用户定义的线程都列出详情, 输出的线程id是十六进制的,可以将上一步得到的十进制线程id进行转换,对应后,可以根据线程id找到有问题的线程,进一步找到引起问题的代码行数
案列2:程序运行很长时间没有结果
- jstack pid 输出的最后有 Found one Java-level deadlock:…
- 定位到发生死锁的位置
3.本地方法栈
jvm调用本地方法时,给本地方法分配的内存。
本地方法:不是由java代码编写的方法,是有此C或者C++语言编写的代码。java方法不方便与底层操作系统打交道,而是通过调用本地方法与底层操作系统打交道。所有对象的父类Object类里面的方法声明就是用native声明的,是没有方法实现的,java通过native方法接口间接调用C或者C++的接口实现。
从下面说道的堆和方法区是线程共享的
4. Heap 堆
通过new关键字,创建对象都会使用堆内存
特点:
1)它是线程共享的,堆中对象都需要考虑线程安全问题 2)有垃圾回收
4.2 堆内存溢出
OutOfMemoryError Java heap space
堆空间参数:-Xmx
4.3堆内存诊断
1.jps工具
查看当前系统中有哪些java进程
2.jmap工具
查看堆内存使用情况 jmap -head pid
3.jconsole工具
图形界面的,多功能检测工具,可以连续检测
案例:垃圾回收后,内存占用仍然很高
- 方法区
定义
组成
5.3 方法区内存溢出
1.8以前会导致永久代内存溢出
java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize=8m
1.8以后会导致元空间内存溢出
java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspace=8m
类加载器:用来加载类的二进制字节码
ClassWriter 用来生成类的二进制字节码
场景:动态产生class,并加载这些类的场景是很多的
Spring:用到字节码技术,都会用到Cglib,生成的代理类,是spring中aop的核心。
myBatis:用到字节码技术,都会用到Cglib。 产生map接口的实现类。
5.4 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
- 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
5.5 StringTable
特性
- 常量池中的字符串仅是符号,第一次用到时才变成变量
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
1)1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。
2)1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份,放入串池,会把串池中的对象返回。
5.6 StringTable的位置
1.6方法区中永久代中,常量池的一部分
1.7-8 堆中
永久代回收效率很低,只有fullGC才会回收,等到老年代空间不足时,才会触发FullGC。java应用程序中大量的字符串常量,会分配到StringTable中,如果StringTable回收效率不高,会占用大量的内存。
基于这样的缺点,StringTable被转移到堆中。MinorGC就可以触发,大大减轻对内存的压力。
5.7 StringTable 垃圾回收
当内存空间不足时,StringTable中没有被引用到的字符串常量仍然会被垃圾回收。
5.8 StringTable 性能调优
- 调整-XX:StringTableSize=桶个数 hash查找会变快
- 考虑将字符串对象是否入池
直接内存溢出
OutOfMemory: Direct buffer memory
6.3 直接内存分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferrenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
直接内存禁用显示回收对直接内存的影响。
垃圾回收
1.如何判断对象可以回收
1.1引用计数法,循环引用导致垃圾回收
1.2可达性分析算法
确定一系列根对象,根对象:肯定不能被当成垃圾回收的对象。
垃圾回收前,对堆内存中的所有对象进行一遍扫描,确定每一个对象是不是被根对象做直接或者间接的引用,如果是,此对象就不能被垃圾回收,否则,就被作为垃圾将来被回收。
-
java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
-
扫描堆中的对象,看是否能够沿着GC Root对象为七点的引用链找到该对象,找不到表示可以回收。
-
哪些对象可以作为GC Root?
-
Memory Analyzer: 快速的多功能的java堆分析工具.
-先用jmap获取堆内存快照。jmap -dump:format=b,live,file=1.bin pid
1)System Class(系统类):启动类加载器加载的类,是一些核心类,代码运行过程中都会用到的
2)Native Stack jvm执行操作时候会调用操作系统的方法,操作系统在执行时引用的java对象也是可以作为根对象。
3)Thread 活动线程中使用的对象不能被当成垃圾回收,线程运行时由一次次的方法调用组成,每次方法调用都会产生栈帧,栈帧内使用的对象(局部变量引用的对象,方法参数引用的对象)被作为根对象。
4)Busy Monitor synchronized正在加锁的对象不能被回收,因为后面还要释放锁。
1.3 五种引用
1)强引用–只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
new 一个对象通过复制运算符赋值给了一个变量,此变量就强引用了创建的对象。只要沿着GC Root链能够找到此对象,就不会被垃圾回收。
2)软引用(SoftReference) --仅有软引用引用该对象时,在垃圾回收后,内存仍不足会再次触发垃圾回收,回收软引用对象。可以配合引用队列来释放软引用自身。
当没有强引用引用对象,只有软引用引用对象,当发生垃圾回收,并且内存不够时候,会将软引用引用的对象回收掉。
应用:不重要的资源可以使用软引用
3)弱引用(WeakReference)–
当没有强引用引用对象,只有弱引用引用对象,当发生垃圾回收,不管内存是否充足时候,会将弱引用引用的对象回收掉。可以配合引用队列来释放弱引用自身。
软弱引用配合引用队列进行工作:软弱引用自身也是一个对象,如果在创建软弱引用时给它们分配了队列,当软弱引用引用的对象被回收的时候,软弱引用就会进入对应的队列。
软弱引用被回收时,需要通过引用队列找到它们,再进行处理。
4)虚引用
必须配合引用队列使用,创建ByteBuffer的实现类对象时,就会创建一个Cleaner的虚引用对象,ByteBuffer会分配一个直接内存,然后将直接内存地址给虚引用对象。当ByteBuffer被回收时,锁使用的直接内存不会被回收,让虚引用对象进入引用队列,虚引用所在的引用队列由一个叫ReferenceHandler的线程定时寻找是否有新入队的Cleaner,如果有就会调用Cleaner的虚方法。虚方法根据前面记录的直接内存地址,调用Unsafe.freeMemory将直接内存释放掉。
虚引用引用的对象被垃圾回收时,虚引用对象本身就会被放入引用队列,从而间接的用一个线程来调用虚引用对象的方法,然后调用Unsafe.freeMemory将直接内存释放掉。
5)终结器引用
当对象重写了终结方法finalize,并且没有强引用引用它时,就可以被垃圾回收。当没有强引用引用对象时,虚拟机为我们创建对象对应的终结器引用,当对象没有被强引用引用时,终结器引用对象也加入一个引用队列,由优先级很低的一个线程finalizeHandler,查看队列中队列中是否有终结器引用,如果有就找到对应的对象并调用其finalize方法,调用完后,等真正发生垃圾回收,就可以将此对象占用的内存回收掉。效率很低,线程有限地低 -
垃圾回收算法
标记清除算法(Mark Sweep):速度较快,会造成内存碎片。
标记整理:速度慢,没有内存碎片。
复制:不会有内存碎片,需要占用双倍内存空间。 -
分代垃圾回收
堆内存将内存区域分成新生代(朝生夕死)和老年代(长时间存活的对象)。
新生代又分成伊甸园、缓存区From和幸存区To。
新生代垃圾回收:Minor GC
老年代垃圾回收:Full GC
对象首先分配在伊甸园区域
1)新生代空间不足时,触发Minor GC,伊甸园和缓存区From存活的对象使用复制算法复制到缓存区To中,存活的对象年龄加一,并交换From和To。
2)Minor GC会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
3)当对象寿命超过阈值时,会晋升到老年代,最大寿命是15(4bit)
4)当老年代空间不足,会先触发Monir GC后,空间仍然不足,会触发Full GC (STW时间更长)
3.1 相关VM参数
堆初始大小:-Xms
堆最大大小:-Xmx或-XX:MaxHeapSize
新生代大小:-Xmn或-XX:NewSize=size+ -XX:MaxNewSize=size
幸存区比例(动态):-XX:InitialSurvivorRatio=ratio 和-XX:+UseAdptiveSizePolicy
幸存区比例:-XX:SurvivorRatio=ratio
晋升阈值:-XX:MaxTenuringDistribute=threshold
晋升详情:-XX:+PrintTenuringDistribution
GC详情:-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC -
卡表与RememberedSet
-
在引用变更时通过post-write barrier + dirty card queue
-
concurent refinement threads更新Remembered Set
7)Remark
pre-wrire barrier + satb_mark_queue
当对象的引用发生改变时,jvm给其加入一个写屏障。经对象加入一个队列中,变成灰色,表示还没有处理完,等并发标记结束后,进入最终标记,从队列中取出再进行一次检查,再变成黑色。
8)JDK 8u20字符串去重
优点:节省大量内存
缺点:略微多占用了cpu时间,新生代回收时间略微增加
-xx:+UseStringDeduplication
String s1 = new String(“hello”);
String s2 = new String(“hello”);
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复。
- 如果它们值一样,让它们引用同一个char[]
- 注意,与String.intern()不一样
-String.intern()关注的是字符串对象
-而字符串去重关注的是char[]
-在jvm内部,使用了不同的字符串表
9)JDK 8u40并发标记类卸载
所有对象经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它它所加载的所有类
-xx:+classUnloadingWithConcurentMark默认启用
很多框架程序都使用了自定义的类加载器,很有用,jdk的启动、扩展、应用程序类加载器不会使用的
10)DK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象。
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。
11)JDK 9并发标记起始时间的调整
- 并发标必须在堆空间占满前完成,否则退化为FullGC
- JDK 9之前需要使用 -xx:InitiatingHeapOccupancyPercent
- jdk 9可以动态调整
-xx:InitiatingHeapOccupancyPercent用来设置初始值
进行数据采样并动态调整-xx:InitiatingHeapOccupancyPercent=percent
总会添加一个安全的空档空间
减少并发垃圾回收退化为fullGC
垃圾回收调优
类加载与字节码技术
-
多态原理小结
当执行invokevirtual指令时,
(1).先通过栈帧中的对象引用找到对象
(2). 分析对象头,找到对象的实际Class
(3). Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
(4).查表得到方法的具体地址
(5).执行方法的字节码 -
字节码指令
6.运行期优化
6.1即时编译
分层编译
JVM将字节码执行状态分成了5个层次:
0层:解释执行(Interpreter)
字节码被加载到虚拟机后,靠解释器一个字节一个字节的解释执行,解释成一个个真正的机器码。当字节码被反复调用次数达到一定程度后,启用编译器(C1和C2编译器)对字节码进行编译执行
1层:使用C1即时编译器编译执行(不带profiling)
2层:使用C1即时编译器编译执行(带基本profiling)
3层:使用C1即时编译器编译执行(带完全的profiling)
4层:使用C2即时编译器编译执行
profiling是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等
即时编译器和和解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
解释器是将字节码解释为针对所有平台都通用的机器码
JIT会根据平台类型,生成平台特定的机器码