Java手撸HttpClient
先将代码贴上,后面再整理下
1 | import javax.net.ssl.SSLSocketFactory; |
先将代码贴上,后面再整理下
1 | import javax.net.ssl.SSLSocketFactory; |
1 | /** |
1 | /** |
下面提供一份更全的编码字节表,使用这个表可以实现Base256等
1 | private static final char[] symbolTable = { |
1 | public class IpMatcher { |
推荐一个工具:https://github.com/jenv/jenv
Follow the steps below to get a working jenv
installation with knowledge of your java
environment. Read all the code you execute carefully: a $
symbol at the beginning of a line should be omitted, since it’s meant to show you entering a command into your terminal and observing the response after the command.
jenv
On OSX, the simpler way to install jEnv is using Homebrew
1 | brew install jenv |
Alternatively, and on Linux, you can install it from source :
1 | git clone https://github.com/jenv/jenv.git ~/.jenv |
Restart your shell by closing and reopening your terminal window or running exec $SHELL -l
in the current session for the changes to take effect.
To verify jenv
was installed, run jenv doctor
. On a macOS machine, you’ll observe the following output:
1 | jenv doctor |
Observe that jenv
is correctly loaded but Java is not yet installed.
To make sure JAVA_HOME
is set, make sure to enable the export
plugin:
1 | jenv enable-plugin export |
Problem? Please visit the Trouble Shooting Wiki page.
Continue to the next section to install java.
Untested: While this fork has improved fish
shell support, it has not been tested by this maintainer. To install jenv
for Fish according to the contributor’s instructions:
1 | echo 'set PATH $HOME/.jenv/bin $PATH' >> ~/.config/fish/config.fish |
Use jenv add
to inform jenv
where your Java environment is located. jenv
does not, by itself, install Java.
For example, on macOS, use brew
to install the latest Java (OpenJDK 11) followed by the appropriate jenv add PATH_TO_JVM_HOME
command to recognize it.
1 | brew install --cask java |
With macOS OpenJDK 11.0.2 installed, for example, either of these commands will add /Library/Java/JavaVirtualMachines/openjdk-11.0.2.jdk/Contents/Home
as a valid JVM. Your JVM directory may vary!
Observe now that this version of Java is added to your java versions
command:
1 | jenv versions |
By default, the latest version of Java is your system
Java on macOS.
We’ll now set a jenv local VERSION
local Java version for the current working directory. This will create a .java-version
file we can check into Git for our projects, and jenv
will load it correctly when a shell is started from this directory.
1 | jenv local 11.0.2 |
Is JAVA_HOME
set?
1 | echo ${JAVA_HOME} |
Yes! Observe that JAVA_HOME
is set to a valid shim directory. Unlike the main repository’s documentation we helpfully installed the export
plugin, and we now have the most important jenv
features covered.
If you executed this commands inside your $HOME
directory, you can now delete .java-version
:
1 | rm .java-version |
Use jenv global VERSION
to set a global Java version:
1 | jenv global 11.0.2 |
When you next open a shell or terminal window, this version of Java will be the default.
On macOS, this sets JAVA_HOME
for GUI applications on macOS using jenv macos-javahome
. Integrates this tutorial to create a file that does not update dynamically depending on what local or shell version of Java is set, only global.
Use jenv shell VERSION
to set the Java used in this particular shell session:
1 | jenv shell 11.0.2 |
These common workflows demonstrate how to use jenv
to solve common problems.
Our goal is to have both the latest version of Java and JDK 8 installed at the same time. This is helpful for developing Android applications, whose build tools are sensitive to using an exact Java version.
We’ll resume where we left off with Java 11.0.2 installed. Let’s install Java 8 now:
1 | brew install --cask adoptopenjdk8 |
This will install the latest version of Java 8 to a special directory in macOS. Let’s see which directory that is:
1 | ls -1 /Library/Java/JavaVirtualMachines |
Observe the adoptopenjdk-8.jdk
directory. Your exact version may vary. We cannot retrieve this using /usr/libexec/java_home
, unfortunately. We’ll add the Java home directory using jenv
so that it shows up in our jenv versions
command:
1 | jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/ |
Go 语言(或 Golang)起源于 2007 年,并在 2009 年正式对外发布。Go 是非常年轻的一门语言,它的主要目标是“兼具Python等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。
数据类型 | 说明 |
---|---|
bool | 布尔 |
string | 字符串 |
int | uint8,uint16,uint32,uint64,int8,int16,int32,int64 |
float | float32,float64 |
byte | byte |
参考:https://www.runoob.com/go/go-data-types.html
在线运行示例:https://play.golang.org/p/-4RylAqUV36
1 | package main |
我们来执行一下:
1 | go run main.go # main.go 为刚刚创建的那个文件的名称 |
在线运行示例:https://play.golang.org/p/zPqCkRZgrgp
1 | package main |
在线运行示例:https://play.golang.org/p/0My8veBvtJ8
1 | package main |
try cache
、throw
,Go语言是通过将error
作为返回值来处理异常。下面我们通过一个示例来了解一下,在线运行示例:https://play.golang.org/p/PYy3ueuPFS6
1 | package main |
该示例输出结果为:
1 | execute func log1 |
但函数有多个返回值的时候,有时你只关注其中一个返回值,这种情况下你可以将其他的返回值赋值给空白符:_
,如下:
1 | _, err := Add2(1, 2) |
空白符特殊在于实际上返回值并没有赋值,所以你可以随意将不同类型的值赋值给他,而不会由于类型不同而报错。
Go语言不是像Java那样的面向对象的语言,他没有对象和继承的概念。也没有class
的概念。在Go语言中有个概念叫做结构体(struct
),结构体和Java中的class
比较类似。下面我们定义一个结构体:
1 | type User struct { |
上面我们定义了一个结构体User
,并为该结构体分别设置了三个公有属性:Name/Gender/Age,下面我们来创建一个User对象。
1 | user := User{ |
结构体的属性可以在结构体内直接声明,那么如何为结构体声明函数(即Java中的方法)呢,我们来看下下面的示例:在线运行示例:https://play.golang.org/p/01_cTu0RzdH
1 | package main |
Java中值类型和引用类型都是定死的,int、double、float、long、byte、short、char、boolean为值类型,其他的都是引用类型,而Go语言中却不是这样。
在Go语言中:
&
表示取地址,例如你有一个变量a
那么&a
就是变量a
在内存中的地址,对于Golang指针也是有类型的,比如a是一个string那么&a是一个string的指针类型,在Go里面叫&string。*
表示取值,接上面的例子,假设你定义b := &a
如果你打印b
,那么输出的是&a
的内存地址,如果要取值,那么需要使用:*b
下面我们来看下例子,在线运行:https://play.golang.org/p/jxAKyVMjnoy
1 | package main |
Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。
Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。
下面我们来看一个例子(在线演示:https://play.golang.org/p/U9U-qjuY0t1)
1 | package main |
执行结果说明fuck函数中的sleep三秒并没有影响喜特
的输出。
如果说 goroutine 是Go语言程序的并发体的话,那么 channel 就是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channel 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。
下面我们利用goroutine+channel来实现一个生产消费者模型,示例代码如下:(在线执行:https://play.golang.org/p/lqUBugLdU-I)
1 | package main |
tag
)1 | private static final double EARTH_RADIUS = 6372.796924; |
Github:https://github.com/blindpirate/report-of-build-tools-for-java-and-golang
In January 2017, the usage of build tools in Github’s top 1000 Java repositories is as follows:
Tool Name | Reference Count |
---|---|
Gradle | 627 |
Maven | 264 |
Ant | 52 |
Npm | 4 |
Bazel | 3 |
Make | 1 |
And the trending over the past 8 years is:
Tool Name | Identity Files |
---|---|
Gradle | build.gradle |
Maven | pom.xml |
Ant | build.xml |
Npm | package.json |
Bazel | BUILD |
Make | Makefile/makefile |
groovy GithubTopRankCrawler.groovy -l java -d <path to store the 1000 repos>
to clone all repositories locally. groovy JavaBuildToolScanner.groovy -d <path to store the 1000 repos>
to analyze these repos.There are various package management tools for golang as listed here. But which one is the most popular?
The usage of package manage tools in Github’s top 1000 Go repositories is as follows:
Tool Name | Url | Reference Count (Feb 2017) | Reference Count (Nov 2017) |
---|---|---|---|
Makefile | Makefile | 199 | 181 |
dep | dep | N/A | 94 |
godep | godep | 119 | 90 |
govendor | govendor | 65 | 84 |
glide | glide | 64 | 77 |
gvt | gvt | 25 | 16 |
trash | trash | 7 | 13 |
submodule | submodule | 8 | 6 |
gpm/johnny-deps | gpm johnny-deps | 7 | 6 |
glock | glock | 5 | 4 |
gom | gom | 4 | 2 |
gopack | gopack | 3 | 2 |
gopm | gopm | 3 | 1 |
goop | goop | 1 | 1 |
gvend | gvend | 2 | 0 |
dep had a first release in May 2017, did not exist for first stats.
Technically, make
is not a package management tool, here it is just for comparison.
Submodule refers to a set of tools which use git submodule to manage dependencies such as manul and Vendetta and so on.
Tool Name | Identity Files |
---|---|
godep | Godeps/Godeps.json |
govendor | vendor/vendor.json |
gopm | .gopmfile |
gvt | vendor/manifest |
gvend | vendor.yml |
glide | glide.yaml or glide.lock |
trash | vendor.conf |
gom | Gomfile |
bunch | bunchfile |
goop | Goopfile |
goat | .go.yaml |
glock | GLOCKFILE |
gobs | goproject.json |
gopack | gopack.config |
nut | Nut.toml |
gpm/johnny-deps | Godeps |
Makefile | makefile or Makefile |
submodule | .gitmodules |
groovy GithubTopRankCrawler.groovy -l go -d <path to store the 1000 repos>
to clone all repositories locally. You can use -s
to do the shallow clone and decrease disk usage.groovy GoBuildToolScanner.groovy <path to store the 1000 repos>
to analyze these repos.前几天突然有个姑娘加我的QQ(不知道哪儿来的我的QQ),让我参加他们免费的公开课,然后给我分享Java学习资料,我以为是会给我发几本书,就参加了,没想到是一个txt文件😂,内容如下:
Allen-架构师必备技能-分库分表应对数据量过大
链接:https://pan.baidu.com/s/1OF4RUHvRk98pBRdUiifH2g 密码:n4ev
Allen-互联网安全话题-使用https保障你的敏感数据不再裸奔
链接:https://pan.baidu.com/s/1qz23y-3ahaGua4YH02KTyw 密码:fgh0
Tony-多线程Future模式-写出支撑海量并发连接的服务端代码
链接:https://pan.baidu.com/s/1NwzNRxUB0_DPNQo2IW_Xhg 密码:0fpw
Tony-前后端分离架构分析与实现
链接:https://pan.baidu.com/s/1b7XnTibtqW26YCHfuAXkyA 密码:ah24
Tony-高并发系统架构之负载均衡全方位解析
链接:https://pan.baidu.com/s/1a87EH1Xe20O4XYZaNRo-hw 密码:p52e
Tony-学会举一反三-从Redis看RPC原理
链接:https://pan.baidu.com/s/1disSAbJo-01ESCu6_rTHYQ 密码:ih47
-Mike-分布式系统架构技能—zookeeper实现分布式锁
链接:https://pan.baidu.com/s/1adhFuoUsz1sMQTnWNGoKPA 密码:gjzh
Tony-数据库连接池原理源码分析
链接:https://pan.baidu.com/s/1uBiBBt-tJVSz_5t5p4jG3A 密码:jqo6
Allen-深入SpringMVC原理老司机带你手写自己的MVC框架
链接:https://pan.baidu.com/s/1rlhZCSqXaZpXWM_V5CA7EQ 密码:vysj
Tony-JVM类加载机制之JAVA热部署实战开发
链接:https://pan.baidu.com/s/1JSLGrG0k44um7weQcq5rvg 密码:twn3
Tony-实战高并发系统缓存雪崩场景重现及解决方案
链接:https://pan.baidu.com/s/1i8Q7sPNEcUIPYBuqFerRwQ 密码:lwgj
Mike-解密spring-boot-starter
链接:https://pan.baidu.com/s/12-1N3RTb68l3QfUOxSG1jQ 密码:sodb
Tony-细说springcloud微服务架构之客户端负载均衡
链接:https://pan.baidu.com/s/1VK3mMTkKYzRU4G9YXwlOLg 密码:achq
JavaCV 是一款开源的视觉处理库,基于GPLv2协议,对各种常用计算机视觉库封装后的一组jar包,封装了OpenCV、ffmpeg、videoInput…等计算机视觉编程人员常用库的接口。
1 | <properties> |
1 | /** |
1 | private static class VideoRecorder implements Closeable { |
这段代码是我四年前写的,当时的使用场景为使用tesseract做图片的预处理。功能包含图片二值化、移除杂色、横向切分、水平切分等。
1 | import java.awt.Color; |
1 | Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class org.bytedeco.javacpp.avutil |
解决办法:
1 | mvn package exec:java -Dplatform.dependencies -Dexec.mainClass=Demo |
出现这个警告是因为ffmpeg要求视频的宽度必须是32的倍数,高度必须是2的倍数,按要求修改下宽高就好了。
1 | import com.google.common.collect.Lists; |
1 | private static class VideoRecorder implements Closeable { |
我在实际使用中只用到了ffmpeg
,但是打包的时候却将flycapture、libdc1394、libfreenect、artoolkitplus、tesseract…等包都打进来了,这些都是我不需要的,下面贴出我的maven配置示例。
1 | <properties> |
1 | import org.slf4j.Logger; |
1 |
|
概念 | 表示范围 | guava对应功能方法 |
---|---|---|
(a..b) | {x | a < x < b} | open(C, C) |
[a..b] | {x | a <= x <= b} | closed(C, C) |
[a..b) | {x | a <= x < b} | closedOpen(C, C) |
(a..b] | {x | a < x <= b} | openClosed(C, C) |
(a..+∞) | {x | x > a} | greaterThan(C) |
[a..+∞) | {x | x >= a} | atLeast(C) |
(-∞..b) | {x | x < b} | lessThan(C) |
(-∞..b] | {x | x <= b} | atMost(C) |
(-∞..+∞) | all values | all() |
1 | mvn deploy:deploy-file -DgroupId=com.tencent -DartifactId=xinge -Dversion=1.1.8 -Dpackaging=jar -DrepositoryId=nexus -Dfile=/Users/gaoyoubo/xinge-push.jar -Durl=http://xxx.xxx.com:8081/nexus/content/repositories/thirdparty/ -DgeneratePom=false |
1 | 转载自:http://ifeve.com/%E5%B9%B6%E5%8F%91%E9%98%9F%E5%88%97-%E6%97%A0%E7%95%8C%E9%98%BB%E5%A1%9E%E5%BB%B6%E8%BF%9F%E9%98%9F%E5%88%97delayqueue%E5%8E%9F%E7%90%86%E6%8E%A2%E7%A9%B6/ |
DelayQueue队列中每个元素都有个过期时间,并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队列。
如图DelayQueue中内部使用的是PriorityQueue存放数据,使用ReentrantLock实现线程同步,可知是阻塞队列。另外队列里面的元素要实现Delayed接口,一个是获取当前剩余时间的接口,一个是元素比较的接口,因为这个是有优先级的队列。
插入元素到队列,主要插入元素要实现Delayed接口。
1 | public boolean offer(E e) { |
首先获取独占锁,然后添加元素到优先级队列,由于q是优先级队列,所以添加元素后,peek并不一定是当前添加的元素,如果(2)为true,说明当前元素e的优先级最小也就即将过期的,这时候激活avaliable变量条件队列里面的线程,通知他们队列里面有元素了。
获取并移除队列首元素,如果队列没有过期元素则等待。
1 | public E take() throws InterruptedException { |
第一次调用take时候由于队列空,所以调用(2)把当前线程放入available的条件队列等待,当执行offer并且添加的元素就是队首元素时候就会通知最先等待的线程激活,循环重新获取队首元素,这时候first假如不空,则调用getdelay方法看该元素海剩下多少时间就过期了,如果delay<=0则说明已经过期,则直接出队返回。否者看leader是否为null,不为null则说明是其他线程也在执行take则把该线程放入条件队列,否者是当前线程执行的take方法,则调用(5)await直到剩余过期时间到(这期间该线程会释放锁,所以其他线程可以offer添加元素,也可以take阻塞自己),剩余过期时间到后,该线程会重新竞争得到锁,重新进入循环。
(6)说明当前take返回了元素,如果当前队列还有元素则调用singal激活条件队列里面可能有的等待线程。leader那么为null,那么是第一次调用take获取过期元素的线程,第一次调用的线程调用设置等待时间的await方法等待数据过期,后面调用take的线程则调用await直到signal。
获取并移除队头过期元素,否者返回null
1 | public E poll() { |
1 | class DelayedEle implements Delayed { |
TimerQueue的内部实现
ScheduledThreadPoolExecutor中DelayedWorkQueue是对其的优化使用
之前写的,总结成代码片段,留备后用。
1 | import java.math.BigDecimal; |
在各种并发编程模型中,生产者-消费者模式大概是最常用的了。在实际工作中,对于生产消费的速度,通常需要做一下权衡。通常来说,生产任务的速度要大于消费的速度。一个细节问题是,队列长度,以及如何匹配生产和消费的速度。
一个典型的生产者-消费者模型如下:
在并发环境下利用J.U.C提供的Queue实现可以很方便地保证生产和消费过程中的线程安全。这里需要注意的是,Queue必须设置初始容量,防止生产者生产过快导致队列长度暴涨,最终触发OutOfMemory。
对于一般的生产快于消费的情况。当队列已满时,我们并不希望有任何任务被忽略或得不到执行,此时生产者可以等待片刻再提交任务,更好的做法是,把生产者阻塞在提交任务的方法上,待队列未满时继续提交任务,这样就没有浪费的空转时间了。阻塞这一点也很容易,BlockingQueue就是为此打造的,ArrayBlockingQueue和LinkedBlockingQueue在构造时都可以提供容量做限制,其中LinkedBlockingQueue是在实际操作队列时在每次拿到锁以后判断容量。
更进一步,当队列为空时,消费者拿不到任务,可以等一会儿再拿,更好的做法是,用BlockingQueue的take方法,阻塞等待,当有任务时便可以立即获得执行,建议调用take的带超时参数的重载方法,超时后线程退出。这样当生产者事实上已经停止生产时,不至于让消费者无限等待。
于是一个高效的支持阻塞的生产消费模型就实现了。
等一下,既然J.U.C已经帮我们实现了线程池,为什么还要采用这一套东西?直接用ExecutorService不是更方便?
我们来看一下ThreadPoolExecutor的基本结构:
可以看到,在ThreadPoolExecutor中,BlockingQueue和Consumer部分已经帮我们实现好了,并且直接采用线程池的实现还有很多优势,例如线程数的动态调整等。
但问题在于,即便你在构造ThreadPoolExecutor时手动指定了一个BlockingQueue作为队列实现,事实上当队列满时,execute方法并不会阻塞,原因在于ThreadPoolExecutor调用的是BlockingQueue非阻塞的offer方法:
1 | public void execute(Runnable command) { |
这时候就需要做一些事情来达成一个结果:当生产者提交任务,而队列已满时,能够让生产者阻塞住,等待任务被消费。
关键在于,在并发环境下,队列满不能由生产者去判断,不能调用ThreadPoolExecutor.getQueue().size()来判断队列是否满。
线程池的实现中,当队列满时会调用构造时传入的RejectedExecutionHandler去拒绝任务的处理。默认的实现是AbortPolicy,直接抛出一个RejectedExecutionException。
几种拒绝策略在这里就不赘述了,这里和我们的需求比较接近的是CallerRunsPolicy,这种策略会在队列满时,让提交任务的线程去执行任务,相当于让生产者临时去干了消费者干的活儿,这样生产者虽然没有被阻塞,但提交任务也会被暂停。
1 | public static class CallerRunsPolicy implements RejectedExecutionHandler { |
但这种策略也有隐患,当生产者较少时,生产者消费任务的时间里,消费者可能已经把任务都消费完了,队列处于空状态,当生产者执行完任务后才能再继续生产任务,这个过程中可能导致消费者线程的饥饿。
参考类似的思路,最简单的做法,我们可以直接定义一个RejectedExecutionHandler,当队列满时改为调用BlockingQueue.put来实现生产者的阻塞:
1 | new RejectedExecutionHandler() { |
这样,我们就无需再关心Queue和Consumer的逻辑,只要把精力集中在生产者和消费者线程的实现逻辑上,只管往线程池提交任务就行了。
相比最初的设计,这种方式的代码量能减少不少,而且能避免并发环境的很多问题。当然,你也可以采用另外的手段,例如在提交时采用信号量做入口限制等,但是如果仅仅是要让生产者阻塞,那就显得复杂了。
今天网百度网盘上上传文件提示单个文件大小超限,让我升级VIP。作为一个有逼格的程序猿怎么可能被这点小事难倒呢。
1 | import com.google.common.collect.Lists; |
收藏一篇文章,这两天被驾校之家CPU占用过高的问题弄的寝食难安。马上用下面的方法监控一下。
参考文章:
1. http://blog.csdn.net/blade2001/article/details/9065985
2. http://blog.csdn.net/jiangguilong2000/article/details/17971247
问题描述:
生产环境下的某台tomcat7服务器,在刚发布时的时候一切都很正常,在运行一段时间后就出现CPU占用很高的问题,基本上是负载一天比一天高。
问题分析:
1,程序属于CPU密集型,和开发沟通过,排除此类情况。
2,程序代码有问题,出现死循环,可能性极大。
问题解决:
1,开发那边无法排查代码某个模块有问题,从日志上也无法分析得出。
2,记得原来通过strace跟踪的方法解决了一台PHP服务器CPU占用高的问题,但是通过这种方法无效,经过google搜索,发现可以通过下面的方法进行解决,那就尝试下吧。
解决过程:
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
将输出的信息发给开发部进行确认,这样就能找出有问题的代码。
通过最近几天的监控,CPU已经安静下来了。
转载-原文地址:http://www.taobaotest.com/blogs/2294
在性能测试过程中,FULL GC频繁是比较常见的问题,FULL GC 产生的原因有很多,这里主要针对meta压测过程中分析FULL GC问题的一些思路进行分享,供大家参考
1.如何发现是否发生FULL GC和FULL GC是否频繁
使用JDK自带的轻量级小工具jstat
语法结构:
Usage: jstat -help|-options
jstat -
参数解释:
Options — 选项,我们一般使用 -gcutil 查看gc情况
vmid — VM的进程号,即当前运行的java进程号
interval– 间隔时间,单位为秒或者毫秒
count — 打印次数,如果缺省则打印无数次
比如 /opt/taobao/java/bin/jstat –gcutil pid 5000
输出结果:
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 90.63 100.00 58.82 3.51 183 2.059 0 0.000 2.059
0.00 15.48 7.80 60.99 3.51 185 2.092 1 0.305 2.397
0.00 15.48 18.10 47.90 3.51 185 2.092 2 0.348 2.440
S0 — Heap上的 Survivor space 0 区已使用空间的百分比
S1 — Heap上的 Survivor space 1 区已使用空间的百分比
E — Heap上的 Eden space 区已使用空间的百分比
O — Heap上的 Old space 区已使用空间的百分比
P — Perm space 区已使用空间的百分比
YGC — 从应用程序启动到采样时发生 Young GC 的次数
YGCT– 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC — 从应用程序启动到采样时发生 Full GC 的次数
FGCT– 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
通过FGC我们可以发现系统是否发生FULL GC和FULL GC的频率
2. FULL GC分析和问题定位
a. GC log收集和分析
(1)在JVM启动参数增加:”-verbose:gc -Xloggc:<file_name> -XX:+PrintGCDetails -XX:+PrintGCDateStamps”
PrintGCTimeStamp只能获得相对时间,建议使用PrintGCDateStamps获得full gc 发生的绝对时间
(2)如果采用CMS GC,仔细分析jstat FGC输出和GC 日志会发现, CMS的每个并发GC周期则有两个stop-the-world阶段——initial mark与final re-mark,使得CMS的每个并发GC周期总共会更新full GC计数器两次,initial mark与final re-mark各一次
b. Dump JVM 内存快照
/opt/taobao/java/bin/jmap -dump:format=b,file=dump.bin pid
这里有一个问题是什么时候进行dump?
一种方法是前面提到的用jstat工具观察,当OLD区到达比较高的比例如60%,一般会很快触发一次FULL GC,可以进行一次DUMP,在FULL GC发生以后再DUMP一次,这样比较就可以发现到底是哪些对象导致不停的FULL GC
另外一种方法是通过配置JVM参数
-XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC分别用于指定在full GC之前与之后生成heap dump
c. 利用MAT((Memory Analyzer Tool)工具分析dump文件
关于MAT具体使用方法网上有很多介绍,这里不做详细展开,这里需要注意的是:
(1) MAT缺省只分析reachable的对象,unreachable的对象(将被收集掉的对象)被忽略,而分析FULL GC频繁原因时unreachable object也应该同时被重点关注。如果要显示unreachable的对象细节必须用mat 1.1以上版本并且打开选项“keep unreachable object”
(2) 通常dump文件会好几个G,无法在windows上直接进行分析,我们可以先把dump文件在linux上进行分析,再把分析好的文件拷贝到windows上,在windows上用MAT打开分析文件。
下面是Meta2.0压测曾遇到的FULL GC频繁问题的分析结果,比较明显,DispatchRequest对象有4千多万个,一共超过2G,并最终导致OOM
在一个下雨的夜晚,我在思考Java中内存管理的问题,以及Java集合对内存使用的效率情况。我做了一个简单的实验,测试在16G内存条件下,Java的Map可以插入多少对象。
这个试验的目的是为了得出集合的内部上限。所以,我决定使用很小的key和value。所有的测试,都是在64w位linux环境下进行的,操作系统是ubuntu12.04。JVM版本为Oracle Java 1.7.0_09-bo5 (HotSpot 23.5-b02)。在这个JVM中,压缩指针(compressed pointers(-XX:+UseCompressedOops))的选项是默认打开的。
首先是简单的针对java.util.TreeMap的测试。不停向其中插入数字,直到其抛出内存溢出异常。JVM的设置是-xmx15G
import java.util.*;
Map m = new TreeMap();
for(long counter=0;;counter++){
m.put(counter,"");
if(counter%1000000==0) System.out.println(""+counter);
}
这个用例插入了1 7200 0000条数据。在接近结束的时候,由于高负荷的GC插入效率开始降低。第二次,我用HashMap代替TreeMap,这次插入了182 000 000条数据。
Java默认的集合并不是最高效利用内存的。所以,这回我们尝试最后化内存的测试。我选择了MapDB中的LongHashMap,其使用原始的long key并且对封装的内存占用进行了优化。JVM的设置仍然是-Xmx15G。
import org.mapdb.*
LongMap m = new LongHashMap();
for(long counter=0;;counter++){
m.put(counter,"");
if(counter%1000000==0) System.out.println(""+counter);
}
这次,计数器停止在了276 000 000。同样,在插入接近结束的时候,速度开始减慢。看起来这是基于堆的结合的限制所在。垃圾回收带来了瓶颈 。
现在是时候祭出杀手锏了:-)。我们可以采用非基于堆的方式存储,这样GC就不会发现我们的数据。我来介绍一下MapDB,它提供了基于数据库引擎的并发同步的TreeMap和HashMap。它提供了多样化的存储方式,其中一个就是非堆内存的方式。(声明:我是MapDB的作者)。
现在,让我们再跑一下之前的用例,这次采用的是非堆的Map。首先是配置并打开数据库,它打开的直接基于内存存储并且关闭事物的模式。接下来的代码是在这个db中创建一个新的map。
import org.mapdb.*
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.make();
Map m = db.getTreeMap("test");
for(long counter=0;;counter++){
m.put(counter,"");
if(counter%1000000==0) System.out.println(""+counter);
}
这是非堆的Map,所以我们需要不同的JVM配置: -XX:MaxDirectMemorySize=15G -Xmx128M。这次测试在达到980 000 000条记录的时候出现内存溢出。
但是,MapDB还可以优化。之前样例的问题在于记录的破碎分散,b-tree的节点每次插入都要调整它的容量。变通的方案是,将b-tree的节点在其插入前短暂的缓存起来。这使得记录的分散降到最低。所以,我们来改变一下DB的配置:
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.make();
Map m = db.getTreeMap("test");
这次记录数达到了 1 738 000 000。速度也是达到了惊人的31分钟完成了17亿数据的插入。
MapDB还能继续优化。我们把b-tree的节点容量从32提升到120并且打开透明(OneCoder理解为对用户不可见的)压缩:
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.compressionEnable()
.make();
Map m = db.createTreeMap("test",120, false, null, null, null);
这个用例在大约3 315 000 000条记录时出现内存溢出。由于压缩,他的速度 有所降低,不过还是在几个小时内完成。我还可以进行一些优化(自定义序列化等等) ,使得数据量达到大约40亿。
也许你好奇所有这些记录是怎么存储的。答案就是,delta-key压缩。当然,向B-Tree插入已经排好序的递增key是最佳的使用场景,并且MapDB也对此进行了一些小小的 优化。最差的情形就是key是随机的.
后续更新:很多朋友对压缩有一些困惑。在这些用例中,Delta-key 压缩默认都是启用的。在下面的用例中,我又额外开启了zlib方式的压缩:
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.make();
Map m = db.getTreeMap("test");
Random r = new Random();
for(long counter=0;;counter++){
m.put(r.nextLong(),"");
if(counter%1000000==0) System.out.println(""+counter);
}
即使在随机序列情况下,MapDB也可以存储652 000 000条记录,大概4倍于基于堆的集合。
这个简单的试验没有太多的目的。这仅仅是我对MapDB的一种优化。也许,更多的惊喜在于插入效率确实不错并且MapDB可以抗衡基于内存的集合。
1 | import static java.lang.Thread.sleep; |