2.1 定义java
Java Virtual Machine Stacks
(Java虚拟机栈)linux
垃圾回收是否涉及栈内存?shell
不涉及。栈内存无非就是一次次的方法调用产生的栈帧内存,栈帧内存在每一次方法调用后都会被弹出栈,也就是这部份内存会被自动的回收掉,因此并不须要垃圾回收来回收栈内存。windows
栈内存分配越大越好吗?数组
不是。安全
-Xss size
。不指定的话,除了windows系统,默认都是1M,windows系统是依据虚拟内存大小分配。方法内的局部变量是否线程安全?多线程
变量是不是线程安全的,取决于这个变量被多线程共享时,每次运行结果和单线程运行的结果是不是同样的。app
示例代码1jvm
static void m1() { int x=0; for(int i=0;i<500;i++){ x++; } System.out.println(x); }
x
这个局部变量是线程安全的。每一个线程对应一个栈,而后线程内每次方法调用都会产生一个新的栈帧,因此x
变量处于不一样线程的栈的栈帧中,互不影响,也就是线程安全的。工具
示例代码2
public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; }
m1
方法中sb
是线程安全的,由于其是线程内的局部变量。m2
方法中sb
是线程不安全的,由于其是做为方法的参数传递进来,那么就有可能有其余线程可以访问到这个变量,那么这个变量就是多个线程共享的,可能形成值不一致,也就是线程不安全的。m3
方法中sb
是线程不安全的,虽然其是线程中的局部变量,可是其做为返回值返回了,那么就有可能被其余线程使用,也就是多线程共享,可能形成值不一致,线程不安全。2.2 栈帧
栈帧存在于 Java 虚拟机栈中,是 Java 虚拟机栈中的单位元素,每一个线程中调用同一个方法或者不一样的方法,都会建立不一样的栈帧(能够简单理解为,一个线程调用一个方法建立一个栈帧),因此,调用的方法链越多,建立的栈帧越多(例如:递归)。每调用一个新的方法,被调用方法对应的栈帧就会被放到栈顶(入栈),也就是成为新的当前栈帧。当一个方法执行完成退出的时候,此方法对应的栈帧也相应销毁(出栈)。
2.2.1 局部变量表(Local
2.2.1 局部变量表(Local Variable Table)
每一个栈帧中都包含一组称为局部变量表的变量列表,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译成 Class 文件时,在 Class 文件格式属性表中 Code 属性的 max_locals(局部变量表所需的存储空间,单位是 Slot) 数据项中肯定了须要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,不过 Java 虚拟机规范中并无明确规定每一个 Slot 所占据的内存空间大小,只是有导向性地说明每一个 Slot 都应该存放的8种类型: byte、short、int、float、char、boolean、reference(对象引用就是存到这个栈帧中的局部变量表里的,这里的引用指的是局部变量的对象引用,而不是成员变量的引用。成员变量的对象引用是存储在 Java 堆(Heap)中)、returnAddress(虚拟机数据类型,returnAddress 类型的值就是指向特定指令内存地址的指针,JVM支持多线程,每一个线程有本身的程序计数器(pc register),而 pc 中的值就是当前指令所在的内存地址,即 returnAddress 类型的数据,当线程执行 native 方法时,pc 中的值为 undefined)类型的数据,这8种类型的数据,均可以使用32位或者更小的空间去存储。Java 虚拟机规范容许 Slot 的长度能够随着处理器、操做系统或者虚拟机的不一样而发生变化。对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。即 long 和 double 两种类型。作法是将 long 和 double 类型速写分割为32位读写的作法。不过因为局部变量表创建在线程的堆栈上,是线程的私有数据,不管读写两个连续的 Slot 是不是原子操做,都不会引发数据安全问题。
Java 虚拟机经过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的 Slot 数量。若是是32位数据类型的数据,索引 n 就表示使用第 n 个 Slot,若是是64位数据类型的变量,则说明要使用第 n 和第 n+1 两个 Slot。
在方法执行过程当中,Java 虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程。若是是实例方法(非 static
方法),那么局部变量表中的第0位索引的 Slot 默认是用来传递方法所属对象实例的引用,在方法中能够经过关键字 this
来访问这个隐含的参数。其他参数按照参数表的顺序来排列,占用从1开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和做用域分配其他的 Slot。
局部变量表中的 Slot 是可重用的,方法体中定义的变量,其做用域并不必定会覆盖整个方法体,若是当前字节码程序计数器的值已经超过了某个变量的做用域,那么这个变量相应的 Slot 就能够交给其余变量去使用,节省栈空间,但也有可能会影响到系统的垃圾收集行为。
局部变量无初始值(实例变量和类变量都会被赋予初始值),类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另一次在初始化阶段,赋予开发者定义的值。所以即便在初始化阶段开发者没有为类变量赋值也没有关系,类变量仍然具备一个肯定的默认值。但局部变量就不同了,若是一个局部变量定义了但没有赋初始值是不能使用的。
使用一段代码说明一下局部变量表:
// java 代码 public int test() { int x = 0; int y = 1; return x + y; } // javac 编译后的字节码,使用 javap -v 查看 public int test(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iconst_1 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: ireturn LineNumberTable: line 7: 0 line 8: 2 line 9: 4 LocalVariableTable: Start Length Slot Name Signature 0 8 0 this Lcom/alibaba/uc/TestClass; 2 6 1 x I 4 4 2 y I
对应上面的解释说明,经过 LocalVariableTable 也能够看出来: Code 属性: stack(int x(1个栈深度)+ int y(1个栈深度))=2, locals(this(1 Slot)+ int x(1 Slot)+ int y(1 Slot))=3, args_size(非 static 方法,this 隐含参数)=1
验证 Slot 复用,运行如下代码时,在 VM 参数中添加 -verbose:gc
:
public void test() { { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; // 当这段代码注释掉时,System.gc() 执行后,也并不会回收这64MB内存。当这段代码执行时,内存被回收了 System.gc(); }
局部变量表中的 Slot 是否还存在关于 placeholder 数组对象的引用。当 int a = 0;
不执行时,代码虽然已经离开了 placeholder 的做用域,可是后续并无任何对局部变量表的读写操做,placeholder 本来所占用的 Slot 尚未被其余变量所复用,因此 placeholder 做为 GC Roots(全部 Java 线程当前活跃的栈帧里指向 Java 堆里的对象的引用) 仍然是可达对象。当 int a = 0;
执行时,placeholder 的 Slot 被变量 a 复用,因此 GC 触发时,placeholder 变成了不可达对象,便可被 GC 回收。
2.2.2 操做数栈(Operand Stack)
操做数栈是一个后入先出(Last In First Out)栈,方法的执行操做在操做数栈中完成,每个字节码指令往操做数栈进行写入和提取的过程,就是入栈和出栈的过程。
同局部变量表同样,操做数栈的最大深度也是Java 程序编译成 Class 文件时被写入到 Class 文件格式属性表的 Code 属性的 max_stacks 数据项中。
操做数栈的每个元素能够是任意的 Java 数据类型,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2,在方法执行的任什么时候候,操做数栈的深度都不会超过在 max_stacks 数据项中设定的最大值(指的是进入操做数栈的 “同一批操做” 的数据类型的栈容量的和)。
当一个方法刚刚执行的时候,这个方法的操做数栈是空的,在方法执行的过程当中,经过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操做数栈中,也提供一些指令向操做数栈中写入和提取值,及结果入栈,也用于存放调用方法须要的参数及接受方法返回的结果。例如,整数加法的字节码指令 iadd
(使用 iadd
指令时,相加的两个元素也必须是 int 型) 在运行的时候将操做数栈中最接近栈顶的两个 int 数值元素出栈相加,而后将相加结果入栈。
2.2.3 动态链接(Dynamic Linking)
每一个栈帧都包含一个指向运行时常量池(JVM 运行时数据区域)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态连接。
在 Class 文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用(1.类的全限定名,2.字段名和描述符,3.方法名和描述符),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用(指向目标的指针、相对偏移量或者是一个可以直接定位到目标的句柄),这种转化称为静态解析。另一部分将在每一次的运行期期间转化为直接引用,这部分称为动态链接。
看看如下代码的 Class 文件格式的常量池:
// java 代码 public Test test() { return new Test(); } // 字节码指令 // Class文件的常量池 Constant pool: #1 = Methodref #4.#19 // java/lang/Object."<init>":()V #2 = Fieldref #3.#20 // com/alibaba/uc/Test.i:I #3 = Class #21 // com/alibaba/uc/Test #4 = Class #22 // java/lang/Object #5 = Utf8 i #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/alibaba/uc/Test; #14 = Utf8 test #15 = Utf8 ()I #16 = Utf8 <clinit> #17 = Utf8 SourceFile #18 = Utf8 Test.java #19 = NameAndType #7:#8 // "<init>":()V #20 = NameAndType #5:#6 // i:I #21 = Utf8 com/alibaba/uc/Test #22 = Utf8 java/lang/Object public int test(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: getstatic #2 // Field i:I 3: areturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Lcom/alibaba/uc/Test;
从上面字节码指令看出 0: getstatic #2 // Field i:I
这行字节码指令指向 Constant pool 中的 #2,而 #2 中指向了 #3 和 #20 为符号引用,在类加载过程的解析阶段会被转化为直接引用(指向方法区的指针)。
2.2.4 方法返回地址
areturn
),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。athrow
字节码指令产生的异常,只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会致使方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。2.3 栈内存溢出
栈帧过多致使栈内存溢出
例如递归方法,当方法调用层级过多,产生大量的栈帧,却没有出栈,就会致使栈内存溢出,抛出StackOverflowError异常
示例代码
public class Demo { private static int count; public static void main(String[] args) { try { method1(); } catch (Throwable e) { e.printStackTrace(); System.out.println(count); } } private static void method1() { count++; method1(); } }
执行结果
java.lang.StackOverflowError at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) 14602
能够看到方法总共执行了14602
次就致使了栈溢出,能够在虚拟机运行参数中设置-Xss128k
调整栈内存大小,结果执行次数就会变小
若是 Java 虚拟机栈能够动态扩展,而且在尝试扩展的时候没法申请到足够的内存,或者在建立新的线程时没有足够的内存去建立对应的虚拟机栈,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常
2.4 线程运行诊断
2.4.1 cpu占用太高
示例代码
public class Demo { public static void main(String[] args) { new Thread(null, () -> { System.out.println("1..."); while(true) { } }, "thread1").start(); new Thread(null, () -> { System.out.println("2..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread2").start(); new Thread(null, () -> { System.out.println("3..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread3").start(); } }
linux上运行此代码
javac Demo.java nohup java Demo &
查看输出
tail -f nohup.out
1... 2... 3... 1... 2... 3...
查看cpu情况
top
能够看到刚才运行的java程序占用cpu很高,进程号为10526
查询该进程下全部线程的运行状态
top -Hp 10526
能够看到占用cpu最高的线程是10536
使用jstack工具获取10526进程中全部线程运行信息
jstack 10526
2019-12-17 17:24:00 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.11-b03 mixed mode): "Attach Listener" #12 daemon prio=9 os_prio=0 tid=0x00007f94d0001000 nid=0x294a waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x00007f94f8008800 nid=0x291f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "thread3" #10 prio=5 os_prio=0 tid=0x00007f94f80e7000 nid=0x292a waiting on condition [0x00007f94fd9e3000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Demo.lambda$main$2(Demo.java:24) at Demo$$Lambda$3/1523554304.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "thread2" #9 prio=5 os_prio=0 tid=0x00007f94f80e5000 nid=0x2929 waiting on condition [0x00007f94fdae4000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Demo.lambda$main$1(Demo.java:15) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "thread1" #8 prio=5 os_prio=0 tid=0x00007f94f80e3800 nid=0x2928 runnable [0x00007f94fdbe5000] java.lang.Thread.State: RUNNABLE at Demo.lambda$main$0(Demo.java:6) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f94f80a8800 nid=0x2926 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f94f80a5800 nid=0x2925 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f94f80a3000 nid=0x2924 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f94f80a1000 nid=0x2923 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f94f8071800 nid=0x2922 in Object.wait() [0x00007f94fe1eb000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142) - locked <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f94f806d800 nid=0x2921 in Object.wait() [0x00007f94fe2ec000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3521030> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157) - locked <0x00000000e3521030> (a java.lang.ref.Reference$Lock) "VM Thread" os_prio=0 tid=0x00007f94f8068800 nid=0x2920 runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007f94f80ad800 nid=0x2927 waiting on condition JNI global references: 152
其中thread1
、thread2
、thread3
是咱们本身建立的线程,其余都是jvm
的线程
获取占用cpu最高的线程号的十六进制
printf '%x\n' 10536
2928
匹配jstack
获得的线程信息
"thread1" #8 prio=5 os_prio=0 tid=0x00007f94f80e3800 nid=0x2928 runnable [0x00007f94fdbe5000] java.lang.Thread.State: RUNNABLE at Demo.lambda$main$0(Demo.java:6) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745)
最后,匹配代码的第6行,得出占用cpu太高的缘由的在线程中无限循环执行致使。
2.4.2 程序执行很长时间没有结果
示例代码
public class Demo { static A a = new A(); static B b = new B(); public static void main(String[] args) throws InterruptedException { new Thread(()->{ synchronized (a) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("我得到了 a 和 b"); } } }).start(); Thread.sleep(1000); new Thread(()->{ synchronized (b) { synchronized (a) { System.out.println("我得到了 a 和 b"); } } }).start(); } } class A { } class B { }
linux运行此代码
javac Demo.java nohup java Demo &
进程号为10633,查看输出时,发现一直没有反应
根据进程号获取全部线程信息
jstack 10633
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.11-b03 mixed mode): "Attach Listener" #11 daemon prio=9 os_prio=0 tid=0x00007fc3a8001000 nid=0x299f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" #10 prio=5 os_prio=0 tid=0x00007fc3d0008800 nid=0x298a waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Thread-1" #9 prio=5 os_prio=0 tid=0x00007fc3d00dd800 nid=0x2994 waiting for monitor entry [0x00007fc3ad8d2000] java.lang.Thread.State: BLOCKED (on object monitor) at Demo.lambda$main$1(Demo.java:21) - waiting to lock <0x00000000e3539580> (a A) - locked <0x00000000e3539590> (a B) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0" #8 prio=5 os_prio=0 tid=0x00007fc3d00db800 nid=0x2993 waiting for monitor entry [0x00007fc3ad9d3000] java.lang.Thread.State: BLOCKED (on object monitor) at Demo.lambda$main$0(Demo.java:13) - waiting to lock <0x00000000e3539590> (a B) - locked <0x00000000e3539580> (a A) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fc3d00a8800 nid=0x2991 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fc3d00a5800 nid=0x2990 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fc3d00a3000 nid=0x298f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fc3d00a1000 nid=0x298e runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fc3d0071800 nid=0x298d in Object.wait() [0x00007fc3c05fc000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142) - locked <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fc3d006d800 nid=0x298c in Object.wait() [0x00007fc3c06fd000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3521030> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157) - locked <0x00000000e3521030> (a java.lang.ref.Reference$Lock) "VM Thread" os_prio=0 tid=0x00007fc3d0068800 nid=0x298b runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007fc3d00ad800 nid=0x2992 waiting on condition JNI global references: 151 Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007fc3b4003778 (object 0x00000000e3539580, a A), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007fc3b40062c8 (object 0x00000000e3539590, a B), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at Demo.lambda$main$1(Demo.java:21) - waiting to lock <0x00000000e3539580> (a A) - locked <0x00000000e3539590> (a B) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0": at Demo.lambda$main$0(Demo.java:13) - waiting to lock <0x00000000e3539590> (a B) - locked <0x00000000e3539580> (a A) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock.
Found one Java-level deadlock
能够在底部看到这句话,意思是死锁,根据后面具体信息可知,在代码29行,Thread-1
锁住对象b,在等待对象a的锁,而在代码13行,Thread-0
锁住对象a,在等待对象b的锁,从而形成死锁,程序无反应。
欢迎关注公众号,后续文章更新通知,一块儿讨论技术问题 。