Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。
Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程。
Java具有简单性、面向对象、分布式、健壮性、安全性、平台独立与可移植性、多线程、动态性等特点。
Java可以编写桌面应用程序、Web应用程序、分布式系统和嵌入式系统应用程序等
现实生活中的事物被抽象成对象,把具有相同属性和行为的对象被抽象成类,再从具有相同属性和行为的类中抽象出父类。
隐藏对象的属性和实现细节,仅仅对外公开接口。
封装具有一下优点:
便于使用者正确、方便的使用系统,防止使用者错误修改系统属性;
有助于建立各个系统之间的松耦合关系;
提高软件的可重用性;
降低了大型系统的风险,即便整个系统不成功,个别独立的子系统有可能还有价值。
封装的两大原则:
把尽可能多的东西藏起来,对外提供简洁的接口;
把所有的属性封装起来。
子类和父类之间的继承关系,子类可以获取到父类的属性和方法。
注:关于子类能否继承父类的私有方法?
从语言角度上说:JDK官方文档明确说明子类不能继承父类的私有方法;
但从内存角度来说,jvm在实例化子类对象之前,会先在内存中创建一个父类对象,然后在父类对象外部放上子类独有的属性,两者合起来形成一个子类对象。所以子类确实拥有父类所有的属性和方法,但是父类中的私有方法子类无法访问。
java语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换。
参考:https://www.cnblogs.com/langtianya/p/4441206.html
metaspace:https://www.cnblogs.com/duanxz/p/3520829.html
程序计数器,栈(虚拟机栈,本地方法栈),堆,方法区,运行时常量池,直接内存。
Hotspot:堆,栈,持久代,codecache,从1.7 开始类常量放在heap中,从1.8 开始持久代变成metaspace
Full GC会进行持久代的回收。
根据上面的各种原因,永久代最终被移除,方法区移至Metaspace,字符串常量移至Java Heap。
元空间报错:
java.lang.OutOfMemoryError: Metaspace
如图1,java对象在内存中占用的空间分为3类,
1. 对象头(Header);
2. 实例数据(Instance Data);
3. 对齐填充(Padding)。
而我们常说的基础数据类型大小主要是指第二类实例数据。
// 直接调用静态方法即可使用
ObjectSizeCalculator.getObjectSize(obj)
最后一块对齐填充空间并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
这是由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。
System.out.println(0b101);//二进制:5 (0b开头的)
System.out.println(0e1011);//0.0
System.out.println(011);//八进制:9 (0开头的)
System.out.println(11);//十进制:11
System.out.println(0x11C);//十六进制:284 (0x开头的)
按位或|
按位与&
按位异或运算符(^)是二元运算符,要化为二进制才能进行计算,在两个操作数中,如果两个相应的位相同,则运算结果为0,否则1;例如:
java中有三种移位运算符
<< : 左移运算符,num << 1,相当于num乘以2
>> : 右移运算符,num >> 1,相当于num除以2
>>> : 无符号右移,忽略符号位,空位都以0补齐
无符号右移位操作符,无论符号是正负,都在高位插入0(都变成了正数)。
-1>>>1得到的结果是2147483647,是Integer的最大值。
-1的二进制补码:
1111 1111 1111 1111 1111 1111 1111 1111
右移一位,在最左边空出的一位插入0,变成了32位整数的最大值:
0111 1111 1111 1111 1111 1111 1111 1111
结果是2147483647
1、通过Unsafe类可以分配内存,可以释放内存;
2、可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的;
3、挂起与恢复
4、CAS操作
Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。例如,一个包含两个属性的对象:int和byte,这个对象需要占用8+4+1=13个字节,这时就需要加上大小为3字节的padding进行8字节对齐,最终占用大小为16个字节。
注意:以上对64位操作系统的描述是未开启指针压缩的情况,关于指针压缩会在下文中介绍。
这里说明一下32位系统和64位系统中对象所占用内存空间的大小:
在32位系统下,存放Class Pointer的空间大小是4字节,MarkWord是4字节,对象头为8字节;
在64位系统下,存放Class Pointer的空间大小是8字节,MarkWord是8字节,对象头为16字节;
64位开启指针压缩的情况下,存放Class Pointer的空间大小是4字节,MarkWord是8字节,对象头为12字节;
如果是数组对象,对象头的大小为:数组对象头8字节+数组长度4字节+对齐4字节=16字节。其中对象引用占4字节(未开启指针压缩的64位为8字节),数组MarkWord为4字节(64位未开启指针压缩的为8字节);
静态属性不算在对象大小内。
从JDK 1.6 update14开始,64位的JVM正式支持了 -XX:+UseCompressedOops 这个可以压缩指针,起到节约内存占用的新参数。
其实之前在讲Lambda表达式的时候提到过,所谓的函数式接口,当然首先是一个接口,然后就是在这个接口里面只能有一个抽象方法。
这种类型的接口也称为SAM接口,即Single Abstract Method interfaces。
它们主要用在Lambda表达式和方法引用(实际上也可认为是Lambda表达式)上。
如定义了一个函数式接口如下:
@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
default void doSomeMoreWork1()
{
// Method body
}
}
提醒:加不加@FunctionalInterface对于接口是不是函数式接口没有影响,该注解知识提醒编译器去检查该接口是否仅包含一个抽象方法
那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):
GreetingService greetService1 = message -> System.out.println("Hello " + message);
wait,notify,notifyAll 是定义在Object类的实例方法,用于控制线程状态。可以说是线程通信的手段吧。
阻塞队列BlockingQueue就是为线程之间共享数据而设计的
Object.wait():释放当前对象锁,并进入阻塞队列
Object.notify():唤醒当前对象阻塞队列里的任一线程(并不保证唤醒哪一个)
Object.notifyAll():唤醒当前对象阻塞队列里的所有线程
为什么这三个方法要与synchronized一起使用呢?
每一个对象都有一个与之对应的监视器
每一个监视器里面都有一个该对象的锁和一个等待队列和一个同步队列
值得提的一点是,synchronized是一个非公平的锁,如果竞争激烈的话,可能导致某些线程一直得不到执行。
volatile的作用就是当一个线程更新某个volatile声明的变量时,会通知其他的cpu使缓存失效,从而其他cpu想要做更新操作时,需要从内存重新读取数据。
具体的通知方式,一种是通过某种协议,比如MESI;再就是对总线加锁,控制变量的读取。
volatile只能保证变量的可见性、有序性,但是不能保证原子性。
因此可以用它来做double-check,但是不能来做i++的操作。
如果想要实现i++的可靠性,必须依赖于synchronized、lock或者atomicXXX来实现。
参考: https://mp.weixin.qq.com/s/QggmWkrgYrNtVkdSKYuRfg
HashMap是非线程安全的,在并发场景中如果不保持足够的同步,就有可能在执行HashMap.get时进入死循环,将CPU的消耗到100%。
大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,
所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
get操作遇到rehash,同一个segment的put,remove发生时也是不安全的。
concurrencyLevel: 并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,
所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。
这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
Java8版本的ConcurrentHashMap相对于Java7有什么优势:
Java7中锁的粒度为segment,每个segment中包含多个HashEntry,而Java8中锁的粒度就是HashEntry(首节点);
Java7中锁使用的是ReentrantLock,而Java8中使用的是synchronized;
为什么使用内置锁synchronized来代替重入锁ReentrantLock?
在低粒度的加锁方式中,synchronized的性能不比ReentrantLock差;
Java8中ConcurrentHashMap的锁粒度更低了,发生冲突的概率更低,
JVM对synchronized进行了大量的优化(自旋锁、偏向锁、轻量级锁等等),
只要线程在可以在自旋过程中拿到锁,那么就不会升级为重量级锁,就避免了线程挂起和唤醒的上下文开销。
但使用ReentrantLock不会自旋,而是直接被挂起,当然,也可以使用tryLock(),但是这样又出现了一个问题,你怎么知道tryLock的时间呢?
在时间范围里还好,假如超过了呢?
所以在低粒度的加锁方式中,synchronized是最好的选择。
Synchronized和ReentrantLock他们的开销差距是在释放锁时唤醒线程的数量,
Synchronized是唤醒锁池里所有的线程+刚好来访问的线程,
而ReentrantLock则是当前线程后进来的第一个线程+刚好来访问的线程。
synchronized内置锁使用起来更加简便、易懂、程序可读性好;
ReentrantLock需要消耗更多的内存
Java8中使用链表+红黑树的数据结构,代替Java7中的链表,当链表长度比较长时,红黑树的查找速度更快;
Jinfo -flag * pid ,查看jvm参数
打印类加载器的统计信息(取代了在JDK8之前打印类加载器信息的permstat)。jmap -dump:live,file=odb.map 25916
可以使用户连接到存活的JVM,转储Java类元数据的详细统计
转储dump文件
java debug tools
metaspace大全:http://www.cnblogs.com/duanxz/p/3520829.html
打印默认参数:
java -XX:+PrintFlagsInitial | grep UseCompressedClassPointers
bool UseCompressedClassPointers = false {lp64_product}
打印最终参数:
java -XX:+PrintFlagsFinal -version |grep manageable
package org.codefx.demo.javadoc8tags;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* The lottery.
* <p>
* "Here's something to think about: How come you never see a headline like 'Psychic Wins Lottery'?" (Jay Leno)
*
* @since 1.0
*/
public interface Lottery {
/**
* Picks the winners from the specified set of players.
* <p>
* The returned list defines the order of the winners, where the first prize goes to the player at position 0. The
* list will not be null but can be empty.
*
* @apiNote This method was added after the interface was released in version 1.0. It is defined as a default method
* for compatibility reasons. From version 2.0 on, the method will be abstract and all implementations of
* this interface have to provide their own implementation of the method.
* @implSpec The default implementation will consider each player a winner and return them in an unspecified order.
* @implNote This implementation has linear runtime and does not filter out null players.
* @param players
* the players from which the winners will be selected
* @return the (ordered) list of the players who won; the list will not contain duplicates
* @since 1.1
*/
default List<String> pickWinners(Set<String> players) {
return new ArrayList<>(players);
}
}
$ jstat -gcutil 21719 1s
S0 S1 E O P YGC YGCT FGC FGCT GCT
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
需要做的调整为-XX:PermSize=64m -XX:MaxPermSize=128m 变成 -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m 否则起不来
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。
例如说,这些引用可能包括:
1.所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
2.VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
3.JNI handles,包括global handles和local handles
(看情况)所有当前被加载的Java类
(看情况)Java类的引用类型静态变量
(看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
(看情况)String常量池(StringTable)里的引用
注意,是一组必须活跃的引用,不是对象。
Tracing GC的根本思路就是:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,其余对象(也就是没有被遍历到的)就自然被判定为死亡。注意再注意:tracing GC的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。
强引用
在程序代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用
用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用
也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用
也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。
即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。
第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。猿们还跟的上吧,嘿嘿。
方法区存储内容是否需要回收的判断可就不一样咯。
方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。
标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。
它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。
它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。
老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区,具体的回收可参见上文2.5节。
下面一张图是HotSpot虚拟机包含的所有收集器,图是借用过来滴:
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
https://www.zybuluo.com/jewes/note/57352
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
一般来讲,排除主动的调用GC操作外,JVM会在以下几种情况发生Full GC。
a) 年老代(Tenured)被写满;
b) 持久代(Perm)被写满;
c) System.gc()被显示调用;
d) 上一次GC之后Heap的各域分配策略动态变化;
另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
对象的内存分配往大方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区上
少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,
其细节取决于当前使用的是哪一种垃圾收集器组合,
还有虚拟机中与内存相关的参数设置
1.对象优先在Eden分配
2.大对象直接进入老年代
3.长期存活的对象将进入老年代
4.动态对象年龄判定
5.空间分配担保
CMS收集器(ConcurrentMarkandSweep),是一个关注系统停顿时间的收集器。它的主要思想是把收集器分成了不同的阶段,其中某些阶段是可以用户程序并行的,从而减少了整体的系统停顿时间。它主要分成了以下几个阶段:
- 初始标记 initial mark
- 并发标记 concurrent mark
- 重新标记 remark
- 并发清理 concurrent clean
- 并发重置 concurrent reset
凡是名字以并发开头的阶段都是可以和用户线程并行的,其他阶段也是要暂停用户程序线程。
CMS虽然能减少系统的停顿时间,但是它也有其缺点:
1. 从它的名字可以看出,它是一个标记-清除收集器,也就说运行了一段时间后,内存会产生碎片,从而导致无法找到连续空间来分配大对象。
2. CMS收集器在运行过程中会占用一些内存,同时系统还在运行,如果系统产生新对象的速度比CMS清理的速度快的话,会导致CMS运行失败。
当上面的任何一种情况发生的时候,JVM就会触发一次Full GC,会导致JVM停顿较长时间。
它的相关选项如下:
--XX:+UseConcMarkSweepGC 表示老年代开启CMS收集器,而新生代默认会使用并行收集器。
--XX:ConcGCThreads 指定用多少个线程来执行CMS的并非阶段。
--XX:CMSInitiatingOccupancyFraction 指定在老生代用掉多少内存后开始进行垃圾回收。与吞吐量优先的回收器不同的是,吞吐量优先的回收器在老生代内存用尽了以后才开始进行收集,这对CMS来讲是不行的,因为吞吐量优先的垃圾回收器运行的时候会停止所有用户线程,所以不会产生新的对象,而CMS运行的时候,用户线程还有可能产生新的对象,所以不能等到内存用光后才开始运行。比如-XX:CMSInitiatingOccupancyFraction=75表示老生代用掉75%后开始回收垃圾。默认值是68。
--XX:+ExplicitGCInvokesConcurrent 如果在代码里面显式调用System.gc(),那么它还是会执行Full GC从而导致用户线程被暂停。采用这个选项使得显式触发GC的时候还是使用CMS收集器。
--XX:+DisableExplicitGC 一个相关的选项,这个选项是禁止显式调用GC
原文:https://blog.csdn.net/jewes/article/details/42174893
包含了如下参数或行为:
打印所有参数:
java -XX:+PrintFlagsFinal -version | grep :
java -XX:+PrintCommandLineFlags
查看算法:
List<GarbageCollectorMXBean> l = ManagementFactory.getGarbageCollectorMXBeans();
for(GarbageCollectorMXBean b : l) {
System.out.println(b.getName());
}
定义 | 参数 | 描述 |
---|---|---|
堆内存空间 | -Xms | Heap area size when starting JVM 启动JVM时的堆内存空间。 |
- | -Xmx | Maximum heap area size 堆内存最大限制 |
新生代空间 | -XX:NewRatio | Ratio of New area and Old area 新生代和老年代的占比 |
- | -XX:NewSize | New area size 新生代空间 |
- | -XX:SurvivorRatio | Ratio ofEdenarea and Survivor area 伊甸园空间和幸存者空间的占比 |
分类 | 参数 | 描述 |
---|---|---|
Serial GC | -XX:+UseSerialGC | |
Parallel GC | -XX:+UseParallelGC -XX:ParallelGCThreads=value | |
CMS GC | -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled | |
G1 | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC | 在JDK6中这两个参数必须同时使用 |
JVM-GC: G1回收器和JVM(1)
JVM-GC:G1回收器和JVM(2)
参考资料:http://www.importnew.com/3146.html
程序顺序规则:在一个线程中,前面的操作happens-before后面的操作
锁规则:对同一个锁,解锁happens-before加锁。
传递性规则:A happens-before B,B happens-before C,则A happens-before
Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,
所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
通俗来讲就是:
1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
在Java的ReentrantLock构造函数中提供了两种锁:创建公平锁和非公平锁(默认)。代码如下:
public ReentrantLock() {
sync = new NonfairSync();
}
在公平的锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁上,则允许‘插队’:当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁.
非公平的ReentrantLock 并不提倡 插队行为,但是无法防止某个线程在合适的时候进行插队。
重入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。ReentrantLock和Synchronize都是可重入的。
原子性,有序性,可见性
锁子与资源
死锁条件
如何避免死锁
参考资料:https://zhuanlan.zhihu.com/p/51616796
红黑树是一个严格满足插入,搜索,删除的时间复杂度最坏为O(log(n))的数据结构
HashMap是数组加链表的数据结构,从jdk1.8开始,链表变成红黑树结构,HashSet基于HashMap实现的。
1)发挥多核CPU的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
2)防止阻塞
从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
3)便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
只有调用了start()方法,才会表现出多线程的特性
两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。
3) CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。
所以在开发的时候需要注意一下。
内存占用问题:
因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
数据一致性问题:
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
Exchanger(交换者)是一个用于线程间协作的工具类。api(exchange,阻塞的)
CyclicBarrier、执行屏障,(account,await)
CountDownLatch、只能一次,执行完了减1,如健康检查,检测等所有的run方法执行完,才走后面的逻辑。cdl.wait(),cdl.countDown();
Semaphore 、信号量控制线程协作,限流,api(acquire,release)
CompletableFuture 回调,线程错误处理,编排
CompletionService 返回最新执行完成的线程结果
Executors.newWorkStealingPool 使用forkjoin框架,只是封装了一个api
FutureTask.done 回调成功,异常管理也可以
又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
1)不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。
要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类,点击这里了解为什么不安全。
Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着"某条线程"指的是当前线程。
二者的锁机制其实也是不一样的。
Synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。
独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
synchronized操作的应该是对象头中mark word,从1.6开始优化了,引入了偏向锁,轻量级锁(自旋),重量级锁。
ReentrantLock使用AOS,维护了一个Node队列,可以是公平和非公平锁,默认是非公平的。
可以实现共享式或者独占式,
如果是独占式,调用LockSupport的park方法阻塞住了当前的线程。
如果是共享式,底层是CAS操作(Compare and Swap),compareAndSetState。不断重试。
AbstractQueuedSynchronizer是神一样的男人Doug Lea写的。
如何使用:
Jdk1.6之前,ReentrantLock性能优于synchronized,不过1.6之后,synchronized做了大量的性能调优
synchronized能实现的用优先用synchronize,竞争不激烈情况下。
public class LocationHolder {
private final LocationHolder INSTANCE = new LocationHolder();
private Map<Long, Location> locations;
private LocationHolder() {
this.locations = new ConcurrentHashMap<>();
}
public LocationHolder getInstance() {
return INSTANCE;
}
public Location getLocation(long id) {
return locations.get(id);
}
public void addLocation(long id, String latitude, String longitude) {
Location location = new Location(id, latitude, longitude);
locations.put(id, location);
}
public Map<Long, Location> getLocations() {
return Collections.unmodifiableMap(locations);
}
}
这个问题和上面那个问题是相关的,我就连在一起了。由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
而 JVM 中,默认的新生代和老生代的比例是1:2,所以大量的老生代被浪费了,新生代不够用。
通过调整 -XX:NewRatio 后,Old GC 有了显著的降低。
System.out.println(JSONObject.class.getProtectionDomain().getCodeSource().getLocation());
BOOT_CLASSPATH="-Xbootclasspath/a:$JAVA_HOME/lib/tools.jar"
解决过程:
1,根据top命令,发现PID为2633的Java进程占用CPU高达300%,出现故障。
2,找到该进程后,如何定位具体线程或代码呢,首先显示线程列表,并按照CPU占用高的线程排序:
[root@localhost logs]# ps -mp 2633 -o THREAD,tid,time | sort -rn
显示结果如下:
USER %CPU PRI SCNT WCHAN USER SYSTEM TID TIME
root 10.5 19 - - - - 3626 00:12:48
root 10.1 19 - - - - 3593 00:12:16
找到了耗时最高的线程3626,占用CPU时间有12分钟了!
将需要的线程ID转换为16进制格式:
[root@localhost logs]# printf "%x\n" 3626
e18
最后打印线程的堆栈信息:
[root@localhost logs]# jstack 2633 |grep e18 -A 30
一键输出 javaCPU消耗高的线程:
https://github.com/oldratlee/useful-scripts/blob/master/docs/java.md#-show-busy-java-threads
//间隔1秒(-d 1),输出一次(-n 1)
top -Hp pid -d 1 -n 1
//打印System_Server进程各个线程的Java调用栈,根据线程状态及调用栈来更进一步定位问题点
kill -3 pid
# 查看进程活的类分布
jmap -histo:live 29956|grep com.hellobike
jmap -dump:format=b,file=./my.dump 4557 -F
jmap -dump:format=b,file=outfile 4557
jhat -J-Xmx512m <heap dump file>
jhat tomcat.bin
jconsole
jmc
jvisualvm
jprofile
google perf tools
strace -e trace=signal -o /workspace/carkey/AppMobileConfigApi/latest/strace.log -p 3381 &
OOM Killer 的一次问题定位
https://github.com/qiniu/logkit
graphite
Resilience4j
尽量重用对象,不要循环创建对象,比如:for循环字符串拼接
容器类初始化的时候指定长度
List<String> list = new ArrayList(32)
ArrayList随机便利快,LinkedList增加删除快
使用Entry遍历Map快
大数据复制使用System.arraycopy
尽量使用基本类型而不是包装类型
及时消除过期对象胡引用,防止内存泄露
尽量使用局部变量,减小变量胡作用域
尽量使用非同步的容器,减少同步作用范围
ThreadLocal缓存线程不安全的对象,比如simpleFormalDate
尽量使用延迟加载
尽量减少使用反射,加缓存
尽量使用连接池、线程池、对象池、缓存
慎用异常,不要用抛异常来表示正常在业务逻辑
String 操作减少使用正则表达式
日志中参数拼接使用占位符
单例模式、Future模式
Nio、减少上下文切换
压测算法
捕获Java线程池执行任务抛出的异常
https://www.cnblogs.com/549294286/p/4618798.html